import Foundation public enum RDEPUBTextPageBreakReason: String, Codable, Equatable { case chapterEnd case frameLimit case blockBoundary case attachmentBoundary case semanticBoundary } public enum RDEPUBTextAttachmentKind: String, Codable, Equatable { case image case generic case footnote } public struct RDEPUBTextPageMetadata: Codable, Equatable { public var breakReason: RDEPUBTextPageBreakReason public var blockRange: NSRange? public var attachmentRanges: [NSRange] public var attachmentKinds: [RDEPUBTextAttachmentKind] public var blockKinds: [RDEPUBTextBlockKind] public var semanticHints: [RDEPUBTextSemanticHint] public var attachmentPlacements: [RDEPUBTextAttachmentPlacement] public var trailingFragmentID: String? public var diagnostics: [String] public init( breakReason: RDEPUBTextPageBreakReason, blockRange: NSRange? = nil, attachmentRanges: [NSRange] = [], attachmentKinds: [RDEPUBTextAttachmentKind] = [], blockKinds: [RDEPUBTextBlockKind] = [], semanticHints: [RDEPUBTextSemanticHint] = [], attachmentPlacements: [RDEPUBTextAttachmentPlacement] = [], trailingFragmentID: String? = nil, diagnostics: [String] = [] ) { self.breakReason = breakReason self.blockRange = blockRange self.attachmentRanges = attachmentRanges self.attachmentKinds = attachmentKinds self.blockKinds = blockKinds self.semanticHints = semanticHints self.attachmentPlacements = attachmentPlacements self.trailingFragmentID = trailingFragmentID self.diagnostics = diagnostics } } public struct EPUBChapterInfo: Codable, Equatable { public var spineIndex: Int public var title: String public var pageCount: Int public init(spineIndex: Int, title: String, pageCount: Int) { self.spineIndex = spineIndex self.title = title self.pageCount = pageCount } } public struct EPUBPage: Codable, Equatable { public var spineIndex: Int public var chapterIndex: Int public var pageIndexInChapter: Int public var totalPagesInChapter: Int public var chapterTitle: String public var fixedSpread: EPUBFixedSpread? public init( spineIndex: Int, chapterIndex: Int, pageIndexInChapter: Int, totalPagesInChapter: Int, chapterTitle: String, fixedSpread: EPUBFixedSpread? ) { self.spineIndex = spineIndex self.chapterIndex = chapterIndex self.pageIndexInChapter = pageIndexInChapter self.totalPagesInChapter = totalPagesInChapter self.chapterTitle = chapterTitle self.fixedSpread = fixedSpread } } public struct EPUBFixedSpreadResource: Codable, Equatable { public var spineIndex: Int public var href: String public var title: String public var pageSpread: RDEPUBPageSpread? public init(spineIndex: Int, href: String, title: String, pageSpread: RDEPUBPageSpread? = nil) { self.spineIndex = spineIndex self.href = href self.title = title self.pageSpread = pageSpread } } public struct EPUBFixedSpread: Codable, Equatable { public var resources: [EPUBFixedSpreadResource] public init(resources: [EPUBFixedSpreadResource]) { self.resources = resources } public var primaryResource: EPUBFixedSpreadResource { resources.first ?? EPUBFixedSpreadResource(spineIndex: 0, href: "", title: "") } public func contains(normalizedHref: String, normalizer: (String) -> String?) -> Bool { resources.contains { resource in normalizer(resource.href) == normalizedHref } } public static func makeSpreads(spine: [RDEPUBSpineItem], spreadEnabled: Bool) -> [EPUBFixedSpread] { let resources = spine.enumerated().map { index, item in EPUBFixedSpreadResource( spineIndex: index, href: item.href, title: item.title, pageSpread: item.pageSpread ) } guard spreadEnabled, resources.count > 1 else { return resources.map { EPUBFixedSpread(resources: [$0]) } } var spreads: [EPUBFixedSpread] = [] var cursor = 0 while cursor < resources.count { let current = resources[cursor] guard cursor + 1 < resources.count else { spreads.append(EPUBFixedSpread(resources: [current])) break } let next = resources[cursor + 1] if shouldPair(current: current, next: next) { spreads.append(EPUBFixedSpread(resources: [current, next])) cursor += 2 } else { spreads.append(EPUBFixedSpread(resources: [current])) cursor += 1 } } return spreads } public static func makeSpreads(parser: RDEPUBParser, spreadEnabled: Bool) -> [EPUBFixedSpread] { makeSpreads(spine: parser.spine, spreadEnabled: spreadEnabled) } private static func shouldPair(current: EPUBFixedSpreadResource, next: EPUBFixedSpreadResource) -> Bool { if current.pageSpread == .center || next.pageSpread == .center { return false } switch (current.pageSpread, next.pageSpread) { case (.left, .right), (.right, .left): return true case (.left, .left), (.right, .right): return false default: return true } } }