583 lines
14 KiB
Markdown
583 lines
14 KiB
Markdown
# EPUB 渲染管线详解
|
||
|
||
读书 (WeRead) 的 EPUB 渲染管线将原始 XHTML 文件转换为可分页、可交互的阅读视图。本文档完整描述从 EPUB 文件到屏幕像素的每一步。
|
||
|
||
---
|
||
|
||
## 1. 管线总览
|
||
|
||
```
|
||
EPUB 文件 (.epub)
|
||
|
|
||
v
|
||
[WREpubParser] 解析 EPUB 结构
|
||
|
|
||
v
|
||
[WREpubTypesetter] XHTML → NSAttributedString
|
||
| (CSS 级联 + HTML 解析 + 后处理)
|
||
v
|
||
[WRCoreTextLayouter] NSAttributedString → 分页布局
|
||
| (CTTypesetter + 分页算法)
|
||
v
|
||
[WRCoreTextLayoutFrame] 单页布局帧
|
||
| (CTFrame + 行提取 + 避免断页)
|
||
v
|
||
[WRPageView] 渲染到屏幕
|
||
| (CGContext 绘制 + 图片 + 装饰)
|
||
v
|
||
屏幕像素
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 阶段一: EPUB 解析 (WREpubParser)
|
||
|
||
### 2.1 输入
|
||
|
||
EPUB 文件路径 + WRBook 模型对象
|
||
|
||
### 2.2 解析步骤
|
||
|
||
```
|
||
1. parseContainerXML
|
||
- 读取 META-INF/container.xml
|
||
- 提取 OPF 文件路径 (rootfile full-path)
|
||
|
||
2. parseOPFAtRelativePath
|
||
- 解析 content.opf
|
||
- 提取 manifest (id → href 映射)
|
||
- 提取 spine (阅读顺序 idref 列表)
|
||
- 提取 metadata (书名、标识符等)
|
||
|
||
3. parseNCX
|
||
- 解析 toc.ncx (目录)
|
||
- 提取 navPoint 树 (id, label, src, playOrder)
|
||
|
||
4. _buildChapterList
|
||
- 将 spine idref 映射到 manifest href
|
||
- 生成 chapters 数组 [{id, href, mediaType, fullPath}]
|
||
|
||
5. _buildResourceMap
|
||
- 构建资源路径映射 (href → 绝对路径)
|
||
- 同时索引文件名用于快速查找
|
||
```
|
||
|
||
### 2.3 输出
|
||
|
||
- chapters 数组(有序章节列表)
|
||
- resourceMap 字典(资源路径映射)
|
||
- WRBook 元数据更新
|
||
|
||
---
|
||
|
||
## 3. 阶段二: CSS 级联 (WREpubTypesetter)
|
||
|
||
### 3.1 CSS 加载顺序
|
||
|
||
```
|
||
Layer 1: default.css (App Bundle)
|
||
↓ 覆盖
|
||
Layer 2: replace.css (App Bundle)
|
||
↓ 覆盖
|
||
Layer 3: dark.css (App Bundle, 仅暗色模式)
|
||
↓ 覆盖
|
||
Layer 4: EPUB 嵌入 CSS (书籍自带)
|
||
↓ 覆盖
|
||
Layer 5: 用户设置 CSS (运行时生成)
|
||
```
|
||
|
||
### 3.2 各层内容
|
||
|
||
**Layer 1 - default.css**:
|
||
```css
|
||
body { font-family: "PingFang SC", sans-serif; margin: 0; padding: 10px 15px; }
|
||
p { margin-top: 0.5em; margin-bottom: 0.5em; }
|
||
h1 { font-size: 1.8em; font-weight: bold; }
|
||
h2 { font-size: 1.5em; font-weight: bold; }
|
||
h3 { font-size: 1.3em; font-weight: bold; }
|
||
ul, ol { padding-left: 1.5em; }
|
||
blockquote { margin-left: 1em; font-style: italic; }
|
||
```
|
||
|
||
**Layer 2 - replace.css**:
|
||
```css
|
||
h1, h2, h3 { font-family: "Source Han Serif CN", serif; }
|
||
pre, code { font-family: "Menlo", monospace; }
|
||
img.bodyPic { wr-vertical-center-style: 2; max-width: 100%; }
|
||
.conQuot { /* 引用块样式 */ }
|
||
```
|
||
|
||
**Layer 3 - dark.css**:
|
||
```css
|
||
body { background-color: #1a1a1a; color: #cccccc; }
|
||
a { color: #6eaad7; }
|
||
img { filter: brightness(0.85); }
|
||
```
|
||
|
||
**Layer 5 - 用户设置 CSS** (动态生成):
|
||
```css
|
||
body { font-size: 18px; line-height: 1.8; font-family: "PingFang SC", sans-serif; }
|
||
body { background-color: #f5f0e8; } /* 护眼模式 */
|
||
p { text-indent: 2em; } /* 首行缩进 */
|
||
```
|
||
|
||
### 3.3 级联合并
|
||
|
||
使用 DTCSSStylesheet.mergeStylesheet: 方法按顺序合并,后加载的覆盖先前的同名规则。
|
||
|
||
---
|
||
|
||
## 4. 阶段三: HTML 解析 (DTHTMLAttributedStringBuilder)
|
||
|
||
### 4.1 SAX 解析流程
|
||
|
||
```
|
||
XHTML 数据
|
||
|
|
||
v
|
||
DTHTMLParser (SAX)
|
||
|
|
||
├── didStartElement: → 创建 DTHTMLElement 节点
|
||
| 应用 CSS 样式
|
||
| 处理自定义属性
|
||
|
|
||
├── foundCharacters: → 累积文本到当前元素
|
||
|
|
||
├── foundCDATA: → 处理 CDATA 内容
|
||
|
|
||
└── didEndElement: → 弹出元素栈
|
||
调用 interpretAttributes
|
||
插入分页标记
|
||
```
|
||
|
||
### 4.2 元素处理
|
||
|
||
每个 HTML 标签被转换为 DTHTMLElement 节点:
|
||
|
||
```
|
||
DTHTMLElement
|
||
├── tagName: "p" / "div" / "img" / ...
|
||
├── classNames: ["bodyPic", "conQuot"]
|
||
├── fontDescriptor: 字体描述符
|
||
├── paragraphStyle: 段落样式
|
||
├── textColor / backgroundColor
|
||
├── children: [DTHTMLElement]
|
||
├── textAttachment (图片)
|
||
├── linkURL (链接)
|
||
├── [WeRead 扩展]
|
||
│ ├── verticalCenterStyle
|
||
│ ├── pageRelate
|
||
│ ├── shouldAvoidPageBreakInside
|
||
│ ├── pageBreakAfter / pageBreakBefore
|
||
│ ├── pageBackgroundColor
|
||
│ └── pageBackgroundImage
|
||
```
|
||
|
||
### 4.3 样式应用顺序
|
||
|
||
```
|
||
1. CSS 样式表规则 (class, id, tag 选择器)
|
||
2. 内联 style="" 属性
|
||
3. WeRead 自定义 CSS 属性
|
||
4. 元素默认样式 (基于标签名)
|
||
```
|
||
|
||
### 4.4 后处理 (_WRPostProcessElementTree)
|
||
|
||
在元素树转为 NSAttributedString 之前,执行 WeRead 特有的后处理:
|
||
|
||
**图片处理**:
|
||
- 设置最大显示尺寸 (1080x1920)
|
||
- 超限图片按比例缩放
|
||
- 添加 .bodyPic CSS 类
|
||
- 设置 wr-vertical-center-style: 2
|
||
|
||
**链接处理**:
|
||
- 添加下划线样式
|
||
- 存储链接 URL 到自定义属性
|
||
|
||
**自定义属性处理**:
|
||
- wr-vertical-center-style → DTHTMLVerticalCenterAttribute
|
||
- weread-page-relate → DTPageRelateAttribute
|
||
|
||
---
|
||
|
||
## 5. 阶段四: NSAttributedString 生成
|
||
|
||
### 5.1 递归转换
|
||
|
||
```
|
||
DTHTMLElement.attributedString
|
||
|
|
||
├── 处理 void 元素 (br → "\n", img → attachment, hr → "\n")
|
||
|
|
||
├── 插入 pageBreakBefore 标记 (如果需要)
|
||
|
|
||
├── 转换文本内容
|
||
| ├── 应用 CTFont (从 fontDescriptor)
|
||
| ├── 应用前景色 (kCTForegroundColorAttributeName)
|
||
| ├── 应用背景色 (DTBackgroundColor)
|
||
| └── 应用链接 (DTLink)
|
||
|
|
||
├── 递归处理子元素
|
||
|
|
||
├── 应用段落样式 (kCTParagraphStyleAttributeName)
|
||
|
|
||
├── 插入 pageBreakAfter 标记 (如果需要)
|
||
|
|
||
└── 应用 WeRead 页面属性
|
||
├── DTHTMLVerticalCenterAttribute
|
||
├── DTPageRelateAttribute
|
||
├── DTPageBreakInsideAvoidAttribute
|
||
├── DTPageBackgroundColorAttribute
|
||
└── DTPageBackgroundImageAttribute
|
||
```
|
||
|
||
### 5.2 输出
|
||
|
||
一个 NSAttributedString,包含:
|
||
- 标准 CoreText 属性 (字体、颜色、段落样式)
|
||
- DTCoreText 标准属性 (链接、附件、列表)
|
||
- WeRead 自定义属性 (分页、居中、背景)
|
||
|
||
---
|
||
|
||
## 6. 阶段五: 分页布局 (WRCoreTextLayouter)
|
||
|
||
### 6.1 Typesetter 创建
|
||
|
||
```
|
||
NSAttributedString
|
||
|
|
||
v
|
||
CTTypesetterCreateWithAttributedString
|
||
|
|
||
v
|
||
CTTypesetter (内部缓存字形分析结果)
|
||
```
|
||
|
||
### 6.2 分页算法
|
||
|
||
```
|
||
输入: NSAttributedString + pageSize
|
||
|
||
1. 计算可用宽度 = pageSize.width - edgeInsets.left - edgeInsets.right
|
||
|
||
2. 初始化 currentIndex = 0
|
||
|
||
3. 循环:
|
||
a. lineBreakIndex = CTTypesetterSuggestLineBreak(typesetter, currentIndex, usableWidth)
|
||
b. 如果 lineBreakIndex <= 0, 退出循环
|
||
c. 创建 WRCoreTextLayoutFrame(range: currentIndex..<currentIndex+lineBreakIndex)
|
||
d. currentIndex += lineBreakIndex
|
||
|
||
4. 返回 [WRCoreTextLayoutFrame] 数组 (每帧 = 一页)
|
||
```
|
||
|
||
### 6.3 页面范围查询
|
||
|
||
`rangeForPageAtIndex:pageSize:` 通过模拟分页快速定位指定页的文本范围,无需创建布局帧对象。
|
||
|
||
### 6.4 行高建议
|
||
|
||
`suggestedLineFragHeights` 逐行创建临时 CTLine 测量高度,用于精确分页计算和避免孤行/寡行。
|
||
|
||
---
|
||
|
||
## 7. 阶段六: 单页布局 (WRCoreTextLayoutFrame)
|
||
|
||
### 7.1 CTFrame 创建
|
||
|
||
```
|
||
CTTypesetter + range + CGPath(rect)
|
||
|
|
||
v
|
||
CTTypesetterCreateFrame
|
||
|
|
||
v
|
||
CTFrame (包含 CTLines 和 CTRuns)
|
||
```
|
||
|
||
### 7.2 行提取
|
||
|
||
```
|
||
CTFrameGetLines → CTLine 数组
|
||
CTFrameGetLineOrigins → 行原点数组
|
||
|
||
对每个 CTLine:
|
||
CTLineGetStringRange → 字符范围
|
||
CTLineGetTypographicBounds → ascent/descent/leading
|
||
创建 WRCoreTextLayoutLine 对象
|
||
|
||
判断每行是否为段落末尾行 (检查下一个字符是否为 '\n')
|
||
```
|
||
|
||
### 7.3 avoidPageBreakInside 实现
|
||
|
||
```
|
||
1. 从最后一行向前遍历
|
||
2. 检查行的 attributedString 属性:
|
||
- WRAvoidPageBreakInside == YES?
|
||
- WRBlockType ∈ {table, code, list, blockquote}?
|
||
3. 如果是受保护元素的一部分,标记需要移除
|
||
4. 最多移除 3 行 (kMaxLinesToRemove)
|
||
5. 调用 rebuildFrameWithoutLastLines: 更新帧
|
||
6. 返回 YES 表示有行被移除
|
||
```
|
||
|
||
### 7.4 渲染高度计算
|
||
|
||
```
|
||
getRenderHeight:
|
||
lastLine = lines.lastObject
|
||
lastLineBottom = lastLine.origin.y - lastLine.descent
|
||
renderedHeight = frameHeight - lastLineBottom + insets.top + insets.bottom
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 阶段七: 绘制到屏幕 (WRCoreTextLayoutFrame)
|
||
|
||
### 8.1 绘制流程
|
||
|
||
```
|
||
drawInContext:image:size:inRect:position:
|
||
|
||
1. CGContextSaveGState
|
||
|
||
2. 坐标系翻转 (CoreText 底左原点 → UIKit 顶左原点)
|
||
CGContextTranslateCTM(0, height)
|
||
CGContextScaleCTM(1, -1)
|
||
|
||
3. 应用位置偏移 (多列/多页布局)
|
||
|
||
4. 应用内容内边距
|
||
|
||
5. 绘制封面图片 (如果有)
|
||
|
||
6. CTFrameDraw(ctFrame, context) — 绘制文本
|
||
|
||
7. drawAttachmentsInContext: — 绘制图片附件
|
||
|
||
8. drawDecorativeElementsInContext: — 绘制装饰元素
|
||
├── 删除线
|
||
├── 下划线
|
||
└── 高亮背景
|
||
|
||
9. CGContextRestoreGState
|
||
```
|
||
|
||
### 8.2 图片绘制
|
||
|
||
```
|
||
遍历 attachments 数组:
|
||
对每个 attachment:
|
||
获取 image, position, size
|
||
CGContextDrawImage(context, imageRect, image.CGImage)
|
||
```
|
||
|
||
### 8.3 装饰元素绘制
|
||
|
||
**删除线**:
|
||
```
|
||
遍历 strikethroughRanges:
|
||
对每个 range:
|
||
找到相交的行
|
||
计算 startX, endX (CTLineGetOffsetForStringIndex)
|
||
y = line.origin.y + ascent * 0.3
|
||
CGContextStrokePath
|
||
```
|
||
|
||
**下划线**:
|
||
```
|
||
遍历 underlineRanges:
|
||
对每个 range:
|
||
找到相交的行
|
||
y = line.origin.y - descent
|
||
CGContextStrokePath (蓝色)
|
||
```
|
||
|
||
**高亮**:
|
||
```
|
||
遍历 highlightRanges:
|
||
对每个 range:
|
||
找到相交的行
|
||
计算高亮矩形 (descent 到 ascent)
|
||
CGContextFillRect (半透明黄色)
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 交互层
|
||
|
||
### 9.1 文本选择
|
||
|
||
```
|
||
characterIndexAtPoint:
|
||
遍历所有行
|
||
检查点是否在行的垂直范围内
|
||
使用 CTLineGetStringIndexForPosition 定位字符
|
||
|
||
selectTextInRange:
|
||
遍历所有行
|
||
找到与 range 相交的行
|
||
添加到 selectedLineIndices
|
||
通过 RACSubject 发送选择通知
|
||
```
|
||
|
||
### 9.2 搜索高亮
|
||
|
||
```
|
||
highlightSearchResults:
|
||
使用 NSString rangeOfString: 搜索
|
||
收集所有匹配范围
|
||
创建 highlightRanges (黄色半透明)
|
||
返回匹配数量
|
||
```
|
||
|
||
### 9.3 Hit Testing
|
||
|
||
```
|
||
stringIndexAtPoint:
|
||
遍历行 → 找到目标行
|
||
CTLineGetStringIndexForPosition → 字符索引
|
||
|
||
rectForCharacterAtIndex:
|
||
遍历行 → 找到包含索引的行
|
||
CTLineGetOffsetForStringIndex → x 坐标
|
||
返回 CGRect
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 附加功能
|
||
|
||
### 10.1 繁简转换
|
||
|
||
```
|
||
如果 book.language ∈ {zh-Hant, zh-TW, zh-HK}:
|
||
CFStringTransform(Hans → Latin → Hant)
|
||
```
|
||
|
||
### 10.2 免费试读截断
|
||
|
||
```
|
||
如果 isFreeTrial && result.length > trialCharacterLimit:
|
||
从限制位置向前搜索段落边界 (\n 或 U+2029)
|
||
截取子串
|
||
追加 "\n\n...\n\n" 指示器
|
||
```
|
||
|
||
### 10.3 附件注入
|
||
|
||
```
|
||
如果 insertArticleToolAttachment:
|
||
追加 NSTextAttachment {type: "articleTool"}
|
||
|
||
如果 insertBookChapterToolAttachment:
|
||
追加 NSTextAttachment {type: "bookChapterTool"}
|
||
|
||
如果 insertRecommendView:
|
||
追加 NSTextAttachment {type: "recommendView"}
|
||
```
|
||
|
||
### 10.4 页面背景生成
|
||
|
||
```
|
||
pageBackgroundImageAtRange:themeBgColor:
|
||
检查是否为章节开头页
|
||
创建 UIGraphicsImageContext
|
||
填充主题背景色
|
||
如果是章节开头: 绘制装饰边框
|
||
否则: 绘制边距参考线
|
||
缓存结果
|
||
```
|
||
|
||
### 10.5 图片缩放
|
||
|
||
```
|
||
resizedImageForImagePath:rect:position:sizePattern:darkMode:themeBgColor:
|
||
根据 sizePattern 计算目标尺寸:
|
||
"full" → 全宽
|
||
"half" → 半宽
|
||
"third" → 1/3 宽
|
||
"quarter" → 1/4 宽
|
||
如果 darkMode: 混合背景色
|
||
高质量插值缩放
|
||
缓存结果
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 性能优化
|
||
|
||
### 11.1 缓存策略
|
||
|
||
- **布局帧缓存**: NSCache, 最多 20 个 (DTCoreTextLayouter)
|
||
- **图片缓存**: NSCache, 最多 50 个 (WRCoreTextLayouter)
|
||
- **页面背景缓存**: NSCache, 按 range + 颜色键 (WRCoreTextLayouter)
|
||
- **内容高度缓存**: 布局帧级别 (DTCoreTextLayoutFrame)
|
||
|
||
### 11.2 懒加载
|
||
|
||
- CTTypesetter 按需创建 (createTypesetter)
|
||
- CTFramesetter 按需创建 (createFramesetter)
|
||
- 行数据按需提取 (extractLines)
|
||
- 字形数据按需提取 (extractGlyphs)
|
||
|
||
### 11.3 线程安全
|
||
|
||
所有核心类使用 NSLock 保护:
|
||
- DTCoreTextLayouter._lock
|
||
- DTCoreTextLayoutFrame._lock
|
||
- DTCoreTextLayoutLine._lock
|
||
- DTCoreTextGlyphRun._lock
|
||
- WRCoreTextLayouter._layoutLock
|
||
- WRCoreTextLayoutFrame._frameLock
|
||
|
||
### 11.4 增量更新
|
||
|
||
- attributedString 变化时标记 typesetterDirty
|
||
- 仅在下次访问时重建 typesetter
|
||
- 布局帧缓存自动失效
|
||
|
||
---
|
||
|
||
## 12. 数据流总结
|
||
|
||
```
|
||
EPUB 文件
|
||
│
|
||
├─ container.xml → OPF 路径
|
||
├─ content.opf → manifest + spine + metadata
|
||
├─ toc.ncx → 目录树
|
||
└─ *.xhtml → 章节内容
|
||
│
|
||
▼
|
||
XHTML 字符串
|
||
│ (繁简转换)
|
||
▼
|
||
CSS 级联合并
|
||
│ (5 层合并)
|
||
▼
|
||
DTHTMLAttributedStringBuilder
|
||
│ (SAX 解析 + DOM 构建 + 样式应用)
|
||
▼
|
||
DTHTMLElement 树
|
||
│ (后处理: 图片/链接/自定义属性)
|
||
▼
|
||
NSAttributedString
|
||
│ (包含标准 + 自定义属性)
|
||
▼
|
||
CTTypesetter
|
||
│ (字形分析 + 行断点计算)
|
||
▼
|
||
WRCoreTextLayoutFrame[]
|
||
│ (每帧 = 一页, 含 avoidPageBreakInside)
|
||
▼
|
||
CGContext 绘制
|
||
│ (文本 + 图片 + 装饰)
|
||
▼
|
||
屏幕像素
|
||
```
|