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