Files
ReadViewSDK/Sources/RDReaderView/EPUBCore/Models/RDEPUBPaginationModels.swift
T

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
}
}
}