ReadViewSDK/Doc/TYPESetter_PIPELINE.md
shenlei c65c190b71 feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
2026-06-22 20:26:34 +08:00

9.8 KiB
Raw Blame History

排版管线详解

最后更新2026-06-18

本文档详细描述 ReadViewSDK 文本排版管线Typesetter Pipeline的 8 个处理阶段、核心算法和数据流。


1. 概述

排版管线是 EPUBTextRendering 层的核心组件,负责将 EPUB 原始 HTML 转换为可分页的 NSAttributedString。它是 textReflowable 渲染路径(本 SDK 核心路径)的关键环节。

入口RDEPUBTextTypesetterPipeline.makeRequest(from:)

输入RDEPUBTypesettingInput(原始 HTML、样式、布局配置

输出RDEPUBTypesettingOutput(渲染请求、诊断信息、兼容性报告)

关键文件Sources/RDReaderView/EPUBTextRendering/Typesetter/


2. 管线总览

Raw HTML (从 EPUB 解压目录读取)
    │
    ▼  [阶段 1] RDEPUBHTMLNormalizer
    │  去 CR、合并空行、规范化附件 HTML
    │
    ▼  [阶段 2] RDEPUBSemanticMarkerInjector
    │  注入分页语义标记 ${rd-sem-start/end}
    │
    ▼  [阶段 3] RDEPUBCFIMarkerInjector
    │  注入 CFI 路径标记
    │
    ▼  [阶段 4] RDEPUBStyleSheetComposer
    │  内联 <link stylesheet>、构建 5 层 CSS、注入 <base href>
    │
    ▼  [阶段 5] RDEPUBFontNormalizer
    │  解析 @font-face、注册嵌入字体
    │
    ▼  [阶段 6] RDEPUBFragmentMarkerInjector
    │  注入 fragment 锚点标记 ${id=xxx}
    │
    ▼  [阶段 7] RDEPUBRenderDiagnosticsCollector
    │  收集图片诊断、资源引用检查
    │
    ▼  构建 RDEPUBTextChapterRenderRequest
    │
    ▼  [阶段 8] RDEPUBDTCoreTextRenderer
       HTML → NSAttributedString → 后处理 → 分页

3. 阶段 1HTML 规范化RDEPUBHTMLNormalizer

文件RDEPUBHTMLNormalizer.swift

3.1 处理内容

规则 说明
CR → LF 统一换行符
合并连续空行 多个空行合并为一个
删除分页标记 <hr lang="zh-CN">分页符</hr>
附件 HTML 规范化 见下表

3.2 附件 HTML 规范化规则

原始 HTML 规范化结果
div.qrbodyPic / div.bodyPic 合并样式到 <img>
img.qqreader-footnote 行内 1em×1em
h1.frontCover > img 封面图片 100% 宽度

3.3 Base URL 注入

<head> 开头注入 <base href="..."> 用于相对路径解析:

static func injectBaseHref(into html: String, baseURL: URL?) -> String

4. 阶段 2语义标记注入RDEPUBSemanticMarkerInjector

文件RDEPUBSemanticMarkerInjector.swift

4.1 标记语法

${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. 阶段 3CFI 标记注入RDEPUBCFIMarkerInjector

文件RDEPUBCFIMarkerInjector.swift

注入 CFI 路径标记,用于后续 CFI 映射构建。在每个文本节点前注入 CFI 路径信息,使渲染后的 NSAttributedString 能够建立 CFI 路径到文本偏移量的映射。


6. 阶段 4CSS 层合成RDEPUBStyleSheetComposer

文件RDEPUBStyleSheetComposer.swift

6.1 五层 CSS 架构

按优先级从低到高:

Kind 说明 注入位置
1 .default 基础阅读器样式(隐藏 head/title/style默认字体大小 <head> 开头
2 .replace 格式化样式(代码块、标题、引用、列表) <head> 开头
3 .dark 暗色模式覆盖(仅暗色主题时注入) <head> 开头
4 .epub EPUB 自带样式(内联后的 <link stylesheet> <head> 末尾
5 .user 用户设置(字号、行距、颜色,!important <head> 末尾

6.2 语言检测

static func prefersLatinLanguageCSS(
    languageCode: String?,
    sourceHTML: String
) -> Bool

检测逻辑:

  1. 检查 lang 属性(如 lang="en"
  2. 对 HTML 文本采样,统计拉丁字符比例
  3. 拉丁语言使用专用 CSSwxread-replace-latin.css

6.3 样式表内联

RDEPUBRenderDiagnosticsCollector.inlineLinkedStyleSheets() 负责:

  1. 查找 <link rel=stylesheet href=...>
  2. 读取 CSS 文件内容
  3. 重写 CSS 中的相对 url() 引用
  4. 内联到 HTML 的 <head>

7. 阶段 5字体注册RDEPUBFontNormalizer

文件RDEPUBFontNormalizer.swift

7.1 处理流程

  1. 解析 @font-face { url(...) }
  2. 提取字体文件路径和 family 名称
  3. 通过 CTFontManagerRegisterFontsForURL(.process) 注册嵌入字体
  4. 已注册字体路径缓存在 registeredFontPaths 集合中,避免重复注册

7.2 注册结果

struct RDEPUBFontRegistrationResult {
    let descriptor: RDEPUBFontDescriptor
    let didRegister: Bool
    let errorDescription: String?
}

注册失败的字体不会阻断管线,但会记录到兼容性报告中。


8. 阶段 6Fragment 标记注入RDEPUBFragmentMarkerInjector

文件RDEPUBFragmentMarkerInjector.swift

扫描 HTML 中的 id 属性,在其前面注入 ${id=xxx} 标记:

<h2 id="section1">标题</h2>
→
${id=section1}<h2 id="section1">标题</h2>

渲染后这些标记会被转换为 NSAttributedString 属性,用于 fragment 偏移量提取。


9. 阶段 7诊断收集RDEPUBRenderDiagnosticsCollector

文件RDEPUBRenderDiagnosticsCollector.swift

9.1 图片诊断

  • 扫描 <img src="..."> 标签
  • 解析引用路径,检查文件是否存在
  • 收集诊断信息用于调试

9.2 资源引用检查

  • 检查 CSS 中的 url() 引用
  • 验证字体文件是否存在
  • 记录缺失资源的诊断信息

10. 阶段 8渲染与后处理RDEPUBDTCoreTextRenderer

文件RDEPUBDTCoreTextRenderer.swift

10.1 HTML → NSAttributedString

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 迭代分页:

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精确切分

判断方法

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 兼容性报告

struct RDEPUBCSSCompatibilityReport {
    let unsupportedRules: [String]    // 不支持的 CSS 规则
    let normalizedRules: [String]     // 已规范化的规则
    let fontFailures: [String]        // 字体注册失败
}

12.2 资源诊断

struct RDEPUBTextResourceReferenceDiagnostic {
    let href: String                  // 引用路径
    let exists: Bool                  // 文件是否存在
    let type: String                  // 资源类型image/font/stylesheet
}

12.3 语义摘要

RDEPUBReaderController.nativeTextSemanticSummary() 返回当前页面的语义摘要,包含:

  • 页码
  • 分页原因breakReason
  • 块类型blockKinds
  • 语义提示semanticHints
  • 附件位置attachmentPlacements