ReadViewSDK/Doc/WXRead/analysis/DTCoreText自定义修改分析.md
2026-05-21 19:40:51 +08:00

12 KiB
Raw Blame History

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):

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 新增:

// 分页控制
@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. WRCoreTextLayouterWeRead 新增类)

这是 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. WRCoreTextLayoutFrameWeRead 新增类)

这是 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. WREpubTypesetterEPUB 排版入口)

这是 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 保护
缓存 布局帧缓存 + 图片缓存