Files
ReadViewSDK/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookModels.swift
T
shenleiandClaude Opus 4.7 c76beed03c feat(reader): 实现大书快速进入与增量分页路径
- PaginationCoordinator: 新增 textReflowable 快速进入路径,支持增量章节构建
  与 staged 合并策略,避免整书一次性构建的主线程阻塞
- TextBookBuilder: 重构为支持单章构建与增量合并模式
- ChapterPageCounter: 优化分页计数逻辑,支持章节级分页缓存
- PageFrameFactory: 新增 CoreText 页面帧工厂,提升排版性能
- ReaderContext/ReaderRuntime: 适配增量分页状态管理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:16:57 +08:00

194 lines
7.7 KiB
Swift

import UIKit
// MARK: - 章节分页诊断数据
/// 单个章节的分页诊断信息,用于调试和质量检测。
/// 记录页数、分页原因、附件/块级元素统计等。
public struct RDEPUBTextChapterPaginationDiagnostic: Equatable {
public var href: String
public var title: String
public var pageCount: Int
/// 每页的分页原因列表
public var breakReasons: [RDEPUBTextPageBreakReason]
/// 包含附件的页数
public var attachmentPageCount: Int
/// 因块级/附件边界调整而分页的页数
public var blockAdjustedPageCount: Int
public var blockKinds: [RDEPUBTextBlockKind]
public var semanticHints: [RDEPUBTextSemanticHint]
public var attachmentPlacements: [RDEPUBTextAttachmentPlacement]
/// 采样诊断日志(最多 4 条)
public var sampleNotes: [String]
}
// MARK: - 章节构建结果
/// 单章构建结果,用于大书快速进入阅读器和后台增量补齐。
public struct RDEPUBTextChapterBuildResult {
public var chapter: RDEPUBTextChapter
public var resourceDiagnostics: [RDEPUBTextResourceReferenceDiagnostic]
public var paginationDiagnostic: RDEPUBTextChapterPaginationDiagnostic
public var performanceSample: RDEPUBTextPerformanceSample
public var cacheHit: Bool
}
// MARK: - 页面数据模型
/// 分页后的单页数据,包含全书绝对页码、所属章节、内容范围等。
///
/// 每个 `RDEPUBTextPage` 由 `RDEPUBTextLayoutFrame` 生成,
/// `contentRange` 标记了该页在章节富文本中的字符范围。
public struct RDEPUBTextPage: Equatable {
/// 全书绝对页码(从 0 开始)
public var absolutePageIndex: Int
public var chapterIndex: Int
public var spineIndex: Int
public var href: String
public var chapterTitle: String
/// 该页在所属章节中的相对页码(从 0 开始)
public var pageIndexInChapter: Int
public var totalPagesInChapter: Int
/// 所属章节的完整富文本内容(用于跨页查询)
public var chapterContent: NSAttributedString
/// 该页的富文本片段
public var content: NSAttributedString
/// 该页在章节富文本中的范围
public var contentRange: NSRange
public var pageStartOffset: Int
public var pageEndOffset: Int
/// 页的元数据(分页原因、附件、语义标记等)
public var metadata: RDEPUBTextPageMetadata
}
// MARK: - 章节数据模型
/// 渲染并分页后的单个章节,包含完整富文本和分页结果。
public struct RDEPUBTextChapter: Equatable {
public var chapterIndex: Int
public var spineIndex: Int
public var href: String
public var title: String
/// 章节完整富文本内容
public var attributedContent: NSAttributedString
/// fragment ID → 字符偏移量映射(用于锚点定位)
public var fragmentOffsets: [String: Int]
public var pageBreakReasons: [RDEPUBTextPageBreakReason]
public var pages: [RDEPUBTextPage]
}
// MARK: - 分页书籍模型
/// 整本 EPUB 的分页书籍模型,包含所有章节、页面和全局索引表。
///
/// 这是 EPUBTextRendering 层的最终产物,由 `RDEPUBTextBookBuilder.build()` 生成。
/// 通过 `chapterData(for:)` 或 `chapterData(atChapterIndex:)` 获取 `RDEPUBChapterData` 进行查询。
public struct RDEPUBTextBook {
public var chapters: [RDEPUBTextChapter]
public var pages: [RDEPUBTextPage]
/// 全局索引表:fileIndex/row/column → 绝对字符偏移量
public let indexTable: RDEPUBTextIndexTable
/// 对标 WXRead 的位置转换器(文件位置 <-> 全书字符位置 <-> 页码)
public var positionConverter: RDEPUBTextPositionConverter {
RDEPUBTextPositionConverter(book: self)
}
public init(chapters: [RDEPUBTextChapter], pages: [RDEPUBTextPage]) {
self.chapters = chapters
self.pages = pages
self.indexTable = RDEPUBTextIndexTable(chapters: chapters)
}
public static func == (lhs: RDEPUBTextBook, rhs: RDEPUBTextBook) -> Bool {
lhs.chapters == rhs.chapters && lhs.pages == rhs.pages
}
/// 按 href 获取章节的数据访问层(包含索引表)
public func chapterData(for href: String) -> RDEPUBChapterData? {
guard let chapter = chapters.first(where: { $0.href == href }) else { return nil }
return RDEPUBChapterData(chapter: chapter, indexTable: indexTable)
}
/// 按 spine 索引获取章节的数据访问层。
public func chapterData(forSpineIndex spineIndex: Int) -> RDEPUBChapterData? {
guard let chapter = chapters.first(where: { $0.spineIndex == spineIndex }) else { return nil }
return RDEPUBChapterData(chapter: chapter, indexTable: indexTable)
}
/// 按章节序号获取章节的数据访问层
public func chapterData(atChapterIndex index: Int) -> RDEPUBChapterData? {
guard chapters.indices.contains(index) else { return nil }
return RDEPUBChapterData(chapter: chapters[index], indexTable: indexTable)
}
/// 按绝对页码(从 1 开始)获取章节的数据访问层。
public func chapterData(forPageNumber pageNumber: Int) -> RDEPUBChapterData? {
guard let page = page(at: pageNumber) else { return nil }
return chapterData(forSpineIndex: page.spineIndex)
}
/// 根据持久化位置解析所属章节的数据访问层。
public func chapterData(
for location: RDEPUBLocation,
resolver: RDEPUBResourceResolver,
bookIdentifier: String?
) -> RDEPUBChapterData? {
guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier) else {
return nil
}
return chapterData(for: normalizedLocation.href)
}
/// 全书章节信息快照;对标 WXRead 由章节模型直接提供章节元数据。
public var chapterInfos: [EPUBChapterInfo] {
chapters.map { chapter in
EPUBChapterInfo(
spineIndex: chapter.spineIndex,
title: chapter.title,
pageCount: chapter.pages.count
)
}
}
/// 按页码(从 1 开始)获取对应页面
public func page(at pageNumber: Int) -> RDEPUBTextPage? {
guard pageNumber > 0, pages.indices.contains(pageNumber - 1) else {
return nil
}
return pages[pageNumber - 1]
}
/// 根据持久化位置计算对应页码(从 1 开始)。
///
/// 解析优先级:
/// 1. rangeAnchor 锚点定位
/// 2. fragment 片段 ID
/// 3. navigationProgression 进度百分比回退
public func pageNumber(for location: RDEPUBLocation, resolver: RDEPUBResourceResolver, bookIdentifier: String?) -> Int? {
guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier),
let chapterData = chapterData(for: normalizedLocation.href) else {
return nil
}
if let anchor = normalizedLocation.rangeAnchor?.start,
let page = positionConverter.pageNumber(for: anchor) {
return page
}
if let anchor = indexTable.anchor(for: normalizedLocation),
let page = positionConverter.pageNumber(for: anchor) {
return page
}
return chapterData.pageNumber(for: normalizedLocation)
}
/// 根据页码生成持久化位置(RDEPUBLocation),包含起止锚点
public func location(forPageNumber pageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation? {
guard let chapterData = chapterData(forPageNumber: pageNumber),
let page = page(at: pageNumber) else {
return nil
}
return chapterData.location(forPage: page, bookIdentifier: bookIdentifier)
}
}