230 lines
7.7 KiB
Swift
230 lines
7.7 KiB
Swift
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
|
||
}
|
||
|
||
/// 文本页元数据,记录分页时的上下文信息
|
||
/// 用于调试分页问题和优化分页质量
|
||
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]
|
||
/// 本页末尾的 fragment ID(用于恢复位置)
|
||
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 {
|
||
/// 对应的 spine 索引
|
||
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
|
||
}
|
||
}
|
||
|
||
/// 页面模型,描述单页的完整信息
|
||
/// 由 RDEPUBReadingSession 根据分页结果构建,是翻页容器层的数据单元
|
||
public struct EPUBPage: Codable, Equatable {
|
||
/// 对应的 spine 索引
|
||
public var spineIndex: Int
|
||
/// 章节索引(从 0 开始的章节序号)
|
||
public var chapterIndex: Int
|
||
/// 页在章节中的索引(从 0 开始)
|
||
public var pageIndexInChapter: Int
|
||
/// 该章节的总页数
|
||
public var totalPagesInChapter: Int
|
||
/// 章节标题
|
||
public var chapterTitle: String
|
||
/// 固定版式 Spread 数据(仅 fixed layout 书籍有值)
|
||
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
|
||
}
|
||
}
|
||
|
||
/// 固定版式 Spread 中的单个资源
|
||
public struct EPUBFixedSpreadResource: Codable, Equatable {
|
||
/// 对应的 spine 索引
|
||
public var spineIndex: Int
|
||
/// 资源路径
|
||
public var href: String
|
||
/// 资源标题
|
||
public var title: String
|
||
/// 在 spread 中的位置偏好
|
||
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
|
||
}
|
||
}
|
||
|
||
/// 固定版式 Spread(双页展开)模型
|
||
/// 将 1-2 个 spine 资源组合为一个显示单元
|
||
public struct EPUBFixedSpread: Codable, Equatable {
|
||
/// 该 spread 包含的资源列表(通常 1-2 个)
|
||
public var resources: [EPUBFixedSpreadResource]
|
||
|
||
public init(resources: [EPUBFixedSpreadResource]) {
|
||
self.resources = resources
|
||
}
|
||
|
||
/// 主资源(列表中的第一个),用于标识和定位
|
||
public var primaryResource: EPUBFixedSpreadResource {
|
||
resources.first ?? EPUBFixedSpreadResource(spineIndex: 0, href: "", title: "")
|
||
}
|
||
|
||
/// 判断指定 href 的资源是否属于此 spread
|
||
/// - Parameters:
|
||
/// - normalizedHref: 标准化后的 href
|
||
/// - normalizer: href 标准化函数
|
||
/// - Returns: 是否包含该资源
|
||
public func contains(normalizedHref: String, normalizer: (String) -> String?) -> Bool {
|
||
resources.contains { resource in
|
||
normalizer(resource.href) == normalizedHref
|
||
}
|
||
}
|
||
|
||
/// 根据 spine 列表和 spread 设置生成 Spread 数组
|
||
/// 根据 pageSpread 属性(left/right/center)决定资源配对
|
||
/// - Parameters:
|
||
/// - spine: spine 项列表
|
||
/// - spreadEnabled: 是否启用 spread 模式
|
||
/// - Returns: spread 数组
|
||
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
|
||
}
|
||
|
||
/// 便捷方法:从 parser 构建 spread
|
||
public static func makeSpreads(parser: RDEPUBParser, spreadEnabled: Bool) -> [EPUBFixedSpread] {
|
||
makeSpreads(spine: parser.spine, spreadEnabled: spreadEnabled)
|
||
}
|
||
|
||
/// 判断两个资源是否应该配对为一个 spread
|
||
/// center 类型不参与配对,left+right 或 right+left 配对
|
||
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
|
||
}
|
||
}
|
||
}
|
||
|