# 排版管线详解
> 最后更新: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)