370 lines
12 KiB
Markdown
370 lines
12 KiB
Markdown
# DTCoreText 自定义修改分析
|
||
|
||
微信读书 (WeRead) 基于开源 DTCoreText 库进行了深度定制,将其从一个通用的 HTML-to-NSAttributedString 转换库改造为一套完整的电子书分页渲染引擎。本文档详细记录所有自定义修改。
|
||
|
||
---
|
||
|
||
## 1. 架构概览
|
||
|
||
开源 DTCoreText 提供三个核心能力:
|
||
- HTML/CSS 解析 (DTHTMLAttributedStringBuilder)
|
||
- DOM 元素模型 (DTHTMLElement)
|
||
- CoreText 排版封装 (DTCoreTextLayouter / LayoutFrame / LayoutLine / GlyphRun)
|
||
|
||
WeRead 在此基础上增加了:
|
||
- 一套完整的分页渲染引擎 (WRCoreTextLayouter / WRCoreTextLayoutFrame)
|
||
- 自定义 CSS 属性体系(控制分页、背景、垂直居中等)
|
||
- 自定义 NSAttributedString 属性键(传递页面布局元数据)
|
||
- 翻译/双语支持
|
||
- 免费试读截断机制
|
||
- 主题/字体动态切换
|
||
|
||
---
|
||
|
||
## 2. 自定义 CSS 属性
|
||
|
||
### 2.1 wr-vertical-center-style
|
||
|
||
**用途**: 控制内联元素(主要是图片)在行内的垂直居中方式。
|
||
|
||
**取值**:
|
||
- `1` — 基线对齐(默认)
|
||
- `2` — 垂直居中(用于 .bodyPic 类图片)
|
||
|
||
**处理流程**:
|
||
1. DTHTMLAttributedStringBuilder 解析 HTML 时读取该属性
|
||
2. 存储到 DTHTMLElement.verticalCenterStyle
|
||
3. 转换为 NSAttributedString 属性 `DTHTMLVerticalCenterAttribute`
|
||
4. WRCoreTextLayoutLine 排版时根据该属性调整 baseline offset
|
||
|
||
**关联 CSS 规则** (replace.css):
|
||
```css
|
||
img.bodyPic {
|
||
wr-vertical-center-style: 2;
|
||
max-width: 100%;
|
||
}
|
||
```
|
||
|
||
### 2.2 weread-page-relate
|
||
|
||
**用途**: 标记元素与页面级布局相关。
|
||
|
||
**取值**: `true` / `false`
|
||
|
||
**处理流程**:
|
||
1. 解析时存储到 DTHTMLElement.pageRelate
|
||
2. 转换为 `DTPageRelateAttribute`
|
||
3. 分页引擎使用该属性决定元素是否参与页面级定位
|
||
|
||
### 2.3 avoidPageBreakInside
|
||
|
||
**用途**: 防止元素内部被分页截断(类似 CSS `break-inside: avoid`)。
|
||
|
||
**适用场景**: 代码块、表格、列表、引用块。
|
||
|
||
**处理流程**:
|
||
1. 解析时设置 DTHTMLElement.shouldAvoidPageBreakInside = YES
|
||
2. 转换为 `DTPageBreakInsideAvoidAttribute`
|
||
3. WRCoreTextLayoutFrame.avoidPageBreakInsideByRemovingLastLinesIfNeeded 检查该属性
|
||
4. 如果元素会被截断,移除末尾几行(最多 3 行)以避免断页
|
||
|
||
**关联类**: `WRBlockType` 属性标识块类型(table / code / list / blockquote)
|
||
|
||
### 2.4 DTPageBreakAfter / DTPageBreakBefore
|
||
|
||
**用途**: 强制在元素前/后插入分页符。
|
||
|
||
**处理流程**:
|
||
1. 解析时设置 DTHTMLElement.pageBreakAfter / pageBreakBefore
|
||
2. 转换为 `DTPageBreakAfterAttribute` / `DTPageBreakBeforeAttribute`
|
||
3. attributedString 生成时插入 LINE SEPARATOR (U+2028) 作为分页标记
|
||
|
||
---
|
||
|
||
## 3. 自定义 NSAttributedString 属性键
|
||
|
||
WeRead 在 DTCoreText 标准属性之外定义了 12 个自定义属性键:
|
||
|
||
| 属性键 | 类型 | 用途 |
|
||
|--------|------|------|
|
||
| `DTPageBackgroundColorAttribute` | UIColor | 页面背景色 |
|
||
| `DTPageBackgroundImageAttribute` | NSString | 页面背景图片路径 |
|
||
| `DTPageBackgroundImagePathAttribute` | NSString | 背景图片绝对路径 |
|
||
| `DTPageBreakAfterAttribute` | NSNumber(BOOL) | 元素后强制分页 |
|
||
| `DTPageBreakBeforeAttribute` | NSNumber(BOOL) | 元素前强制分页 |
|
||
| `DTPageBreakInsideAvoidAttribute` | NSNumber(BOOL) | 避免元素内分页 |
|
||
| `DTPageRelateAttribute` | NSString | 页面关联标记 |
|
||
| `DTPageSize` | NSValue(CGSize) | 页面尺寸 |
|
||
| `DTPageFlippingStyle` | NSNumber(NSInteger) | 翻页动画样式 |
|
||
| `DTHTMLVerticalCenterAttribute` | NSString | 垂直居中样式 |
|
||
| `DTHTMLTranslateTagAttribute` | NSString | 翻译标签标记 |
|
||
| `DTHTMLTranslateNoStyleAttribute` | NSString | 无翻译样式标记 |
|
||
|
||
这些属性在 DTHTMLElement.attributedString 生成阶段写入,在 WRCoreTextLayoutFrame 排版和渲染阶段被读取。
|
||
|
||
---
|
||
|
||
## 4. DTHTMLElement 修改
|
||
|
||
### 4.1 新增属性
|
||
|
||
原版 DTHTMLElement 的属性主要关注字体、颜色、段落样式。WeRead 新增:
|
||
|
||
```objc
|
||
// 分页控制
|
||
@property BOOL shouldAvoidPageBreakInside;
|
||
@property BOOL pageBreakAfter;
|
||
@property BOOL pageBreakBefore;
|
||
|
||
// 页面布局
|
||
@property NSString *verticalCenterStyle; // wr-vertical-center-style
|
||
@property NSString *pageRelate; // weread-page-relate
|
||
@property UIColor *pageBackgroundColor; // DTPageBackgroundColor
|
||
@property NSString *pageBackgroundImage; // DTPageBackgroundImage
|
||
```
|
||
|
||
### 4.2 applyStyleDictionary 扩展
|
||
|
||
原版处理标准 CSS 属性(font-family, color, margin 等)。WeRead 新增 `_applyWeReadProperties:` 方法处理自定义 CSS 属性。
|
||
|
||
### 4.3 attributedString 生成扩展
|
||
|
||
原版递归生成 NSAttributedString。WeRead 在生成流程中增加了:
|
||
- 分页标记插入(pageBreakBefore / pageBreakAfter)
|
||
- WeRead 页面属性应用(_applyWeReadPageAttributesToString:)
|
||
|
||
---
|
||
|
||
## 5. DTHTMLAttributedStringBuilder 修改
|
||
|
||
### 5.1 新增 Book/Chapter 上下文
|
||
|
||
```objc
|
||
@property WRBook *book;
|
||
@property WRChapter *chapter;
|
||
```
|
||
|
||
原版 builder 无业务上下文。WeRead 注入书籍和章节信息,用于:
|
||
- 判断是否拉丁语书籍(影响字体回退)
|
||
- 获取 EPUB 嵌入 CSS
|
||
- 解析相对资源路径
|
||
|
||
### 5.2 自定义属性处理
|
||
|
||
新增 `_applyWeReadCustomAttributes:fromAttributes:` 方法,在 SAX 解析阶段识别并存储自定义 CSS 属性。
|
||
|
||
### 5.3 DTHTMLParserDelegate 扩展
|
||
|
||
内部 delegate 新增:
|
||
- `_book` / `_chapter` 上下文引用
|
||
- `_currentImageView` / `_imageContainerView` 等图片视图引用
|
||
- `_rootView` / `_parentView` 视图层级追踪
|
||
|
||
---
|
||
|
||
## 6. DTCoreTextLayouter 修改
|
||
|
||
### 6.1 布局帧缓存
|
||
|
||
新增 `NSCache *layoutFrameCache`(最多 20 个帧),避免重复排版。
|
||
|
||
### 6.2 分页辅助方法
|
||
|
||
新增:
|
||
- `stringIndexFittingLengthForWidth:startIndex:` — 模拟分页计算适配长度
|
||
- `suggestLineBreakStartIndex:width:` — 行断点建议
|
||
|
||
### 6.3 线程安全
|
||
|
||
新增 `NSLock *_lock` 保护 typesetter 和 layout frames 的并发访问。
|
||
|
||
---
|
||
|
||
## 7. DTCoreTextLayoutFrame 修改
|
||
|
||
### 7.1 完整的绘图管线
|
||
|
||
原版提供基本的 drawInContext:。WeRead 扩展为:
|
||
- `drawInContext:options:` — 支持 drawImages / drawLinks / drawSelection 选项
|
||
- `drawImagesInContext:options:` — 绘制图片附件
|
||
- `drawLinksInContext:options:` — 绘制链接下划线
|
||
- `drawSelectionInContext:options:` — 绘制选区高亮
|
||
|
||
### 7.2 文本几何查询
|
||
|
||
新增丰富的几何查询方法:
|
||
- `rectsForRange:` — 字符串范围对应的矩形数组
|
||
- `frameForStringRange:` — 范围的包围矩形
|
||
- `stringRangeForRect:` — 矩形内的字符串范围
|
||
- `baselineOriginForStringIndex:` — 基线原点
|
||
- `cursorRectForIndex:` — 光标位置
|
||
|
||
### 7.3 截断检测
|
||
|
||
新增 `isTruncated` / `truncatedStringRange` 用于检测内容是否超出可见区域。
|
||
|
||
---
|
||
|
||
## 8. DTCoreTextLayoutLine / GlyphRun 修改
|
||
|
||
### 8.1 LayoutLine
|
||
|
||
- 新增 `lineIndex` 属性标识行序号
|
||
- 新增 `isLastLineInParagraph` / `isLineBreak` 标记
|
||
- 新增 `paragraphStyle` / `metrics` 扩展属性
|
||
- 完整的 hit testing 支持
|
||
|
||
### 8.2 GlyphRun
|
||
|
||
- 新增 `glyphImages` 数组支持自定义字形渲染
|
||
- 新增 `attachments` 数组支持附件
|
||
- 新增 `isAttachment` / `isWhitespace` / `isNewline` 标记
|
||
- 新增 `writingDirection` / `isRTL` 支持从右到左文字
|
||
- 完整的 CGPath 生成(newPathWithGlyphs / newPathForGlyphAtIndex:)
|
||
- 字形级别的 hit testing
|
||
|
||
---
|
||
|
||
## 9. WRCoreTextLayouter(WeRead 新增类)
|
||
|
||
这是 WeRead 自己的排版器,包装 DTCoreTextLayouter 并增加业务逻辑。
|
||
|
||
### 9.1 核心功能
|
||
|
||
- **分页**: `layoutFramesForPageSize:` 将整章内容分割为页面
|
||
- **页数计算**: `numberOfPagesForPageSize:` 快速计算总页数
|
||
- **范围查询**: `rangeForPageAtIndex:pageSize:` 获取指定页的文本范围
|
||
- **页面背景**: `pageBackgroundImageAtRange:themeBgColor:` 生成页面背景图
|
||
- **图片缩放**: `resizedImageForImagePath:` 支持 full/half/third/quarter 尺寸模式
|
||
- **暗色模式**: `darkModeAdjusted` 图片混合处理
|
||
|
||
### 9.2 布局配置
|
||
|
||
```objc
|
||
@interface WRCoreTextLayoutConfig : NSObject
|
||
@property CGFloat frameWidth;
|
||
@property CGFloat frameHeight;
|
||
@property UIEdgeInsets edgeInsets;
|
||
@property NSUInteger numberOfColumns;
|
||
@property CGFloat columnGap;
|
||
@property BOOL avoidOrphans; // 避免孤行
|
||
@property BOOL avoidWidows; // 避免寡行
|
||
@property BOOL hyphenation; // 连字符断字
|
||
@end
|
||
```
|
||
|
||
### 9.3 与 DTCoreTextLayouter 的关系
|
||
|
||
WRCoreTextLayouter 内部持有 DTCoreTextLayouter 的 CTTypesetter,但使用自己的 WRCoreTextLayoutFrame 进行页面级布局。
|
||
|
||
---
|
||
|
||
## 10. WRCoreTextLayoutFrame(WeRead 新增类)
|
||
|
||
这是 WeRead 自己的布局帧,包装 CTFrame 并增加页面级功能。
|
||
|
||
### 10.1 核心功能
|
||
|
||
- **avoidPageBreakInside**: 实现 CSS break-inside:avoid 语义
|
||
- **渲染高度计算**: `getRenderHeight` 返回实际内容高度
|
||
- **附件绘制**: `drawAttachmentsInContext:` 绘制图片
|
||
- **装饰元素**: 删除线、下划线、高亮背景
|
||
- **文本选择**: 完整的选择/搜索/高亮支持
|
||
- **RAC 响应式**: 通过 RACSubject 发送布局变化通知
|
||
|
||
### 10.2 avoidPageBreakInside 实现算法
|
||
|
||
```
|
||
1. 从末尾向前遍历所有行
|
||
2. 检查每行是否属于 avoidPageBreakInside 元素
|
||
3. 如果是,标记需要移除
|
||
4. 最多移除 3 行 (kMaxLinesToRemove)
|
||
5. 调用 rebuildFrameWithoutLastLines: 重建帧
|
||
```
|
||
|
||
---
|
||
|
||
## 11. WREpubTypesetter(EPUB 排版入口)
|
||
|
||
这是 EPUB 渲染的最高层入口,协调整个排版流程。
|
||
|
||
### 11.1 CSS 级联顺序(优先级从低到高)
|
||
|
||
1. **default.css** — 基础 HTML 标签样式
|
||
2. **replace.css** — WeRead 默认替换(思源宋体标题、Menlo 代码、bodyPic 图片)
|
||
3. **dark.css** — 暗色主题覆盖(仅暗色模式加载)
|
||
4. **EPUB 嵌入 CSS** — 书籍自带样式
|
||
5. **用户设置 CSS** — 运行时用户偏好(字号、行高、主题色)
|
||
|
||
### 11.2 繁简转换
|
||
|
||
支持将简体中文内容转换为繁体(zh-Hant / zh-TW / zh-HK),使用 CFStringTransform 实现。
|
||
|
||
### 11.3 免费试读截断
|
||
|
||
非 VIP 用户每章有字符数限制,截断点为限制前最后一个段落边界,追加 "..." 指示器。
|
||
|
||
### 11.4 附件注入
|
||
|
||
章节末尾可选注入:
|
||
- articleTool — 文章工具栏(高亮、笔记、分享)
|
||
- bookChapterTool — 章节工具
|
||
- recommendView — 推荐视图
|
||
|
||
---
|
||
|
||
## 12. 字体系统
|
||
|
||
### 12.1 默认字体
|
||
|
||
- 正文: PingFang SC(苹方简体)
|
||
- 标题: Source Han Serif CN(思源宋体)
|
||
- 代码: Menlo(等宽)
|
||
|
||
### 12.2 动态字体加载
|
||
|
||
支持从 CDN 动态下载字体,通过 DTCoreTextFontDescriptor 管理字体描述符和匹配。
|
||
|
||
### 12.3 字体回退
|
||
|
||
拉丁语书籍使用不同的字体回退链,通过 `isLatinLanguageBook` 标记区分。
|
||
|
||
---
|
||
|
||
## 13. 主题系统
|
||
|
||
### 13.1 主题类型
|
||
|
||
- 默认(白底黑字)
|
||
- 护眼(米色 #f5f0e8)
|
||
- 暗色(黑底灰字 #1a1a1a / #cccccc)
|
||
- 自定义背景色
|
||
|
||
### 13.2 用户可配置项
|
||
|
||
通过 options 字典传入:
|
||
- `fontFamily` — 字体族
|
||
- `fontSize` — 字号 (默认 18px)
|
||
- `lineHeight` — 行高倍数
|
||
- `letterSpacing` — 字间距
|
||
- `backgroundColor` / `textColor` — 主题色
|
||
- `paragraphSpacing` — 段间距
|
||
- `firstLineIndent` — 首行缩进
|
||
|
||
---
|
||
|
||
## 14. 与原版 DTCoreText 的差异总结
|
||
|
||
| 维度 | 原版 DTCoreText | WeRead 定制版 |
|
||
|------|----------------|---------------|
|
||
| 定位 | 通用 HTML 渲染库 | 电子书分页渲染引擎 |
|
||
| CSS 支持 | 标准 CSS 2.1 | 标准 + 6 个自定义属性 |
|
||
| 分页 | 无 | 完整分页 + avoidPageBreakInside |
|
||
| 主题 | 无 | 暗色/护眼/自定义主题 |
|
||
| 翻译 | 无 | 繁简转换 + 双语支持 |
|
||
| 图片 | 基本附件 | 垂直居中 + 暗色适配 + CDN 加载 |
|
||
| 选择 | 无 | 完整文本选择 + 搜索高亮 |
|
||
| 响应式 | 无 | RACSubject 通知 |
|
||
| 线程安全 | 部分 | 全面 NSLock 保护 |
|
||
| 缓存 | 无 | 布局帧缓存 + 图片缓存 |
|