12 KiB
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 类图片)
处理流程:
- DTHTMLAttributedStringBuilder 解析 HTML 时读取该属性
- 存储到 DTHTMLElement.verticalCenterStyle
- 转换为 NSAttributedString 属性
DTHTMLVerticalCenterAttribute - WRCoreTextLayoutLine 排版时根据该属性调整 baseline offset
关联 CSS 规则 (replace.css):
img.bodyPic {
wr-vertical-center-style: 2;
max-width: 100%;
}
2.2 weread-page-relate
用途: 标记元素与页面级布局相关。
取值: true / false
处理流程:
- 解析时存储到 DTHTMLElement.pageRelate
- 转换为
DTPageRelateAttribute - 分页引擎使用该属性决定元素是否参与页面级定位
2.3 avoidPageBreakInside
用途: 防止元素内部被分页截断(类似 CSS break-inside: avoid)。
适用场景: 代码块、表格、列表、引用块。
处理流程:
- 解析时设置 DTHTMLElement.shouldAvoidPageBreakInside = YES
- 转换为
DTPageBreakInsideAvoidAttribute - WRCoreTextLayoutFrame.avoidPageBreakInsideByRemovingLastLinesIfNeeded 检查该属性
- 如果元素会被截断,移除末尾几行(最多 3 行)以避免断页
关联类: WRBlockType 属性标识块类型(table / code / list / blockquote)
2.4 DTPageBreakAfter / DTPageBreakBefore
用途: 强制在元素前/后插入分页符。
处理流程:
- 解析时设置 DTHTMLElement.pageBreakAfter / pageBreakBefore
- 转换为
DTPageBreakAfterAttribute/DTPageBreakBeforeAttribute - 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 新增:
// 分页控制
@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 上下文
@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 布局配置
@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 级联顺序(优先级从低到高)
- default.css — 基础 HTML 标签样式
- replace.css — WeRead 默认替换(思源宋体标题、Menlo 代码、bodyPic 图片)
- dark.css — 暗色主题覆盖(仅暗色模式加载)
- EPUB 嵌入 CSS — 书籍自带样式
- 用户设置 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 保护 |
| 缓存 | 无 | 布局帧缓存 + 图片缓存 |