# 排版管线详解 > 最后更新:2026-06-18 本文档详细描述 ReadViewSDK 文本排版管线(Typesetter Pipeline)的 8 个处理阶段、核心算法和数据流。 --- ## 1. 概述 排版管线是 EPUBTextRendering 层的核心组件,负责将 EPUB 原始 HTML 转换为可分页的 NSAttributedString。它是 textReflowable 渲染路径(本 SDK 核心路径)的关键环节。 **入口**:`RDEPUBTextTypesetterPipeline.makeRequest(from:)` **输入**:`RDEPUBTypesettingInput`(原始 HTML、样式、布局配置) **输出**:`RDEPUBTypesettingOutput`(渲染请求、诊断信息、兼容性报告) **关键文件**:`Sources/RDEpubReaderView/EPUBTextRendering/Typesetter/` --- ## 2. 管线总览 ``` Raw HTML (从 EPUB 解压目录读取) │ ▼ [阶段 1] RDEPUBHTMLNormalizer │ 去 CR、合并空行、规范化附件 HTML │ ▼ [阶段 2] RDEPUBSemanticMarkerInjector │ 注入分页语义标记 ${rd-sem-start/end} │ ▼ [阶段 3] RDEPUBCFIMarkerInjector │ 注入 CFI 路径标记 │ ▼ [阶段 4] RDEPUBStyleSheetComposer │ 内联 、构建 5 层 CSS、注入 │ ▼ [阶段 5] RDEPUBFontNormalizer │ 解析 @font-face、注册嵌入字体 │ ▼ [阶段 6] RDEPUBFragmentMarkerInjector │ 注入 fragment 锚点标记 ${id=xxx} │ ▼ [阶段 7] RDEPUBRenderDiagnosticsCollector │ 收集图片诊断、资源引用检查 │ ▼ 构建 RDEPUBTextChapterRenderRequest │ ▼ [阶段 8] RDEPUBDTCoreTextRenderer HTML → NSAttributedString → 后处理 → 分页 ``` --- ## 3. 阶段 1:HTML 规范化(RDEPUBHTMLNormalizer) **文件**:`RDEPUBHTMLNormalizer.swift` ### 3.1 处理内容 | 规则 | 说明 | |------|------| | CR → LF | 统一换行符 | | 合并连续空行 | 多个空行合并为一个 | | 删除分页标记 | `
分页符` | | 附件 HTML 规范化 | 见下表 | ### 3.2 附件 HTML 规范化规则 | 原始 HTML | 规范化结果 | |-----------|-----------| | `div.qrbodyPic / div.bodyPic` | 合并样式到 `` | | `img.qqreader-footnote` | 行内 1em×1em | | `h1.frontCover > img` | 封面图片 100% 宽度 | ### 3.3 Base URL 注入 在 `` 开头注入 `` 用于相对路径解析: ```swift static func injectBaseHref(into html: String, baseURL: URL?) -> String ``` --- ## 4. 阶段 2:语义标记注入(RDEPUBSemanticMarkerInjector) **文件**:`RDEPUBSemanticMarkerInjector.swift` ### 4.1 标记语法 ```html ${rd-sem-start:id=X;block=...;hints=...;placement=...} ...内容... ${rd-sem-end:id=X} ``` ### 4.2 注入规则 - 遍历所有 HTML 标签,维护开标签栈 - 为有分页语义的标签注入标记: - `block`:块类型(paragraph、heading、list 等) - `hints`:分页提示(avoidPageBreakInside、keepWithNext 等) - `placement`:附件位置(inline、block) - 空标签(img, br, hr)同时注入开始和结束标记 ### 4.3 语义提示类型 | 提示 | 说明 | |------|------| | `avoidPageBreakInside` | 避免在块内分页 | | `keepWithNext` | 与下一个块保持同页 | | `attachmentBlock` | 块级附件(图片等) | --- ## 5. 阶段 3:CFI 标记注入(RDEPUBCFIMarkerInjector) **文件**:`RDEPUBCFIMarkerInjector.swift` 注入 CFI 路径标记,用于后续 CFI 映射构建。在每个文本节点前注入 CFI 路径信息,使渲染后的 NSAttributedString 能够建立 CFI 路径到文本偏移量的映射。 --- ## 6. 阶段 4:CSS 层合成(RDEPUBStyleSheetComposer) **文件**:`RDEPUBStyleSheetComposer.swift` ### 6.1 五层 CSS 架构 按优先级从低到高: | 层 | Kind | 说明 | 注入位置 | |----|------|------|----------| | 1 | `.default` | 基础阅读器样式(隐藏 head/title/style,默认字体大小) | `` 开头 | | 2 | `.replace` | 格式化样式(代码块、标题、引用、列表) | `` 开头 | | 3 | `.dark` | 暗色模式覆盖(仅暗色主题时注入) | `` 开头 | | 4 | `.epub` | EPUB 自带样式(内联后的 ``) | `` 末尾 | | 5 | `.user` | 用户设置(字号、行距、颜色,!important) | `` 末尾 | ### 6.2 语言检测 ```swift static func prefersLatinLanguageCSS( languageCode: String?, sourceHTML: String ) -> Bool ``` 检测逻辑: 1. 检查 `lang` 属性(如 `lang="en"`) 2. 对 HTML 文本采样,统计拉丁字符比例 3. 拉丁语言使用专用 CSS(`wxread-replace-latin.css`) ### 6.3 样式表内联 `RDEPUBRenderDiagnosticsCollector.inlineLinkedStyleSheets()` 负责: 1. 查找 `` 2. 读取 CSS 文件内容 3. 重写 CSS 中的相对 `url()` 引用 4. 内联到 HTML 的 `` 中 --- ## 7. 阶段 5:字体注册(RDEPUBFontNormalizer) **文件**:`RDEPUBFontNormalizer.swift` ### 7.1 处理流程 1. 解析 `@font-face { url(...) }` 块 2. 提取字体文件路径和 family 名称 3. 通过 `CTFontManagerRegisterFontsForURL(.process)` 注册嵌入字体 4. 已注册字体路径缓存在 `registeredFontPaths` 集合中,避免重复注册 ### 7.2 注册结果 ```swift struct RDEPUBFontRegistrationResult { let descriptor: RDEPUBFontDescriptor let didRegister: Bool let errorDescription: String? } ``` 注册失败的字体不会阻断管线,但会记录到兼容性报告中。 --- ## 8. 阶段 6:Fragment 标记注入(RDEPUBFragmentMarkerInjector) **文件**:`RDEPUBFragmentMarkerInjector.swift` 扫描 HTML 中的 `id` 属性,在其前面注入 `${id=xxx}` 标记: ```html

标题

→ ${id=section1}

标题

``` 渲染后这些标记会被转换为 NSAttributedString 属性,用于 fragment 偏移量提取。 --- ## 9. 阶段 7:诊断收集(RDEPUBRenderDiagnosticsCollector) **文件**:`RDEPUBRenderDiagnosticsCollector.swift` ### 9.1 图片诊断 - 扫描 `` 标签 - 解析引用路径,检查文件是否存在 - 收集诊断信息用于调试 ### 9.2 资源引用检查 - 检查 CSS 中的 `url()` 引用 - 验证字体文件是否存在 - 记录缺失资源的诊断信息 --- ## 10. 阶段 8:渲染与后处理(RDEPUBDTCoreTextRenderer) **文件**:`RDEPUBDTCoreTextRenderer.swift` ### 10.1 HTML → NSAttributedString ```swift func renderChapter(request: RDEPUBTextChapterRenderRequest) throws -> RDEPUBRenderedChapterContent ``` 1. 将 HTML 编码为 `Data` 2. 使用 `DTHTMLAttributedStringBuilder` 构建 `NSAttributedString` 3. 在 `willFlushCallback` 中对每个 DOM 元素调用 `RDEPUBAttachmentNormalizer.prepareHTMLElementForReaderRendering()` ### 10.2 后处理 **applyPaginationSemantics()**: - 将 `${rd-sem-start/end}` 标记转为 NSAttributedString 属性 - 属性键:`.rdPageSemanticHints`、`.rdPageBlockKind`、`.rdPageAttachmentPlacement` **extractFragmentOffsets()**: - 提取 `${id=xxx}` 标记 - 生成 fragment ID → 字符偏移量映射 - 删除标记文本 **normalizeReadingAttributes()**: - 规范化字体(应用用户选择的字体) - 调整行距(应用 lineHeightMultiple) - 设置文字颜色(应用主题颜色) - 处理附件(图片缩放、对齐) --- ## 11. 分页计算 ### 11.1 RDEPUBChapterPageCounter **文件**:`Pagination/RDEPUBChapterPageCounter.swift` 使用 CoreText 迭代分页: ```swift func layoutFrames(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] ``` **分页循环**: ``` location = 0 while location < totalLength: 1. 创建 CTFrame(通过 CTFramesetter) 2. 获取可见范围 (CTFrameGetVisibleStringRange) 3. 应用 avoidPageBreakInside 规则(最多移除 3 行尾部) 4. 应用 keepWithNext 规则(最多移除 3 行尾部) 5. 应用 widow/orphan 控制 6. 应用 pageBreakPolicy 调整 7. 记录页面范围 8. location = 调整后的范围末尾 ``` ### 11.2 RDEPUBPageBreakPolicy **文件**:`Pagination/RDEPUBPageBreakPolicy.swift` 分页规则优先级: | 规则 | 说明 | 最大调整行数 | |------|------|-------------| | `avoidPageBreakInside` | 块内不分页(标题、图片等) | 3 行 | | `keepWithNext` | 标题与正文不分离 | 3 行 | | widow control | 段落最后一行不留到下一页 | 1 行 | | orphan control | 段落第一行不单独在上一页 | 1 行 | | attachment boundary | 块级图片前后分页 | 0(精确切分) | **判断方法**: ```swift func lineIsInAvoidPageBreakInsideBlock(_ lineRange: NSRange) -> Bool func lineIsInKeepWithNextBlock(_ lineRange: NSRange) -> Bool func adjustedRange(from:totalLength:lineRanges:factory:) -> (range, breakReason, ...) ``` ### 11.3 RDEPUBChapterTailNormalizer **文件**:`BuildPipeline/RDEPUBChapterTailNormalizer.swift` 三遍处理: 1. **删除空白中间帧**:无可见字符且无附件的帧 2. **删除空白尾部帧**:从末尾开始删除同类空白帧 3. **合并短尾帧**:如果最后一帧 ≤2 个可见字符且前一帧 ≥8 倍长,合并 --- ## 12. 诊断与调试 ### 12.1 兼容性报告 ```swift struct RDEPUBCSSCompatibilityReport { let unsupportedRules: [String] // 不支持的 CSS 规则 let normalizedRules: [String] // 已规范化的规则 let fontFailures: [String] // 字体注册失败 } ``` ### 12.2 资源诊断 ```swift struct RDEPUBTextResourceReferenceDiagnostic { let href: String // 引用路径 let exists: Bool // 文件是否存在 let type: String // 资源类型(image/font/stylesheet) } ``` ### 12.3 语义摘要 `RDEPUBReaderController.nativeTextSemanticSummary()` 返回当前页面的语义摘要,包含: - 页码 - 分页原因(breakReason) - 块类型(blockKinds) - 语义提示(semanticHints) - 附件位置(attachmentPlacements)