docs: add WXRead reflowable renderer design

This commit is contained in:
shen 2026-05-21 20:44:30 +08:00
parent 4daa3a6ed4
commit c95d80e257

View File

@ -0,0 +1,251 @@
# Reflowable EPUB 使用 WXRead 风格原生渲染:详细设计
> 文档目的:讨论并固化“将 ReadViewSDK 的 reflowable EPUB 渲染/排版/分页改为参考 Doc/WXRead 的微信读书WXRead原生渲染方式”的可落地设计供后续开发与回归使用。
> 版本v0设计草案
> 日期2026-05-21
## 0. 背景与结论(先说人话)
ReadViewSDK 当前对 EPUB 有三类渲染路径:
- `RDEPUBReadingProfile.webFixedLayout`Fixed Layout EPUB → `WKWebView`(保持不变)
- `RDEPUBReadingProfile.webInteractive`:交互式 EPUBJS/音视频/表单/iframe/外链/bridge`WKWebView`(保持不变)
- `RDEPUBReadingProfile.textReflowable`:普通 reflowable EPUB → **当前走 DTCoreText → NSAttributedString → CoreText 分页 → 原生文本视图**(这是我们要“升级成 WXRead 风格”的主战场)
本次改造的最小可落地方向是:**保留三分流策略不变**,只增强 `.textReflowable` 分支使其在“CSS 分层、样式一致性、资源解析、分页稳定性”等方面更接近 `Doc/WXRead/analysis/EPUB渲染管线详解.md` 所描述的 WXRead 管线,而不是引入新的 WebView 渲染。
## 1. 目标 / 非目标
### 1.1 目标In Scope
- G1reflowable EPUB 的正文渲染/排版/分页改为“WXRead 风格原生渲染管线”:
- XHTML/HTML →CSS 分层 + 解析 + 后处理)→ `NSAttributedString`
- `NSAttributedString`CoreText 分页)→ 单页内容
- 单页内容 → 原生绘制/展示
- G2保持 Fixed Layout 与交互式 EPUB 的 `WKWebView` 路径不回归。
- G3不破坏 `RDURLReaderController` 打开 `.epub` / `.txt` 的主流程。
- G4Demo 可用于验证:至少 2-3 本典型 reflowable EPUB 在段落/标题/图片/链接等常见内容下可稳定阅读。
### 1.2 非目标Out of Scope
- N1Fixed Layout EPUB 切换为原生渲染(明确不做)。
- N2交互式 EPUB 切换为原生渲染(明确不做)。
- N3一次性复刻 WXRead 对 DTCoreText 的所有深度魔改(例如自定义 CSS 属性体系、复杂后处理、分页避断规则等)——本次先按“问题驱动”逐步对齐。
## 2. 关键事实核验(当前代码真实路径)
### 2.1 渲染路径分流(已存在)
- 判定在 `Sources/RDReaderView/EPUBCore/RDEPUBParser+ReadingProfile.swift`
- `metadata.layout == .fixed``.webFixedLayout`
- `hasInteractiveContent() == true``.webInteractive`
- 否则 → `.textReflowable`
### 2.2 `.textReflowable` 当前实现(已存在,且是正确切入点)
`Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
- `publication.readingProfile == .textReflowable` 时:
- 使用 `RDEPUBTextBookBuilder(renderer: resolvedTextRenderer())`
- 默认 renderer 是 `RDEPUBDTCoreTextRenderer``#if canImport(DTCoreText)`
- 分页使用 `NSAttributedString.ss_pageRanges(size:)``CTFramesetterCreateFrame` + `CTFrameGetVisibleStringRange`
- UI 展示使用 `RDEPUBTextContentView`
结论:我们不需要“新起一个阅读器”,只要把 `.textReflowable` 的 **渲染typesetter层**与部分 **分页策略**升级即可。
## 3. WXRead 参考模型(我们要对齐的最小子集)
来自 `Doc/WXRead/analysis/EPUB渲染管线详解.md` 的管线:
1) `WREpubParser`:解析 EPUB 结构spine、manifest、resourceMap
2) `WREpubTypesetter`XHTML → `NSAttributedString`CSS 级联 + HTML 解析 + 后处理)
3) `WRCoreTextLayouter``NSAttributedString` → 分页布局(`CTTypesetter` + 分页算法)
4) `WRCoreTextLayoutFrame`:单页 layout frame
5) `WRPageView`:绘制到屏幕
本次设计对齐重点(最小集合):
- A**CSS 分层与合成**default/replace/dark/epub/user并注入到渲染输入
- B**资源解析**(图片/CSS 的相对路径 baseURL与稳定性保障
- C在现有分页基础上逐步迭代先可用后对齐“避免断页”等高级策略
## 4. 是否能直接使用 Doc/WXRead 中的 JS/CSS
结论:**不建议、也不应该直接把“来自微信读书 App bundle 的私有 JS/CSS”拷贝进 SDK 作为产品代码**;但可以按以下原则“选择性使用”:
### 4.1 可以使用的情况(需满足其一)
- 文件本身带有明确开源许可证声明,且我们按许可证要求引入(保留 license、署名、NOTICE 等),并建议从官方 upstream 获取:
- 例如 `Doc/WXRead/resources/js/rangy-core.js` 明确标注 MIT
- 例如 `Doc/WXRead/resources/js/Readability.js` 明确标注 Apache-2.0
> 建议:即便文件里有 license 头,也优先从其原始开源仓库拉取对应版本,而不是从逆向提取的副本直接入库,以降低合规风险。
### 4.2 不建议/不能直接使用的情况
- 无明确许可证头、看起来是微信读书私有逻辑/样式(例如 `weread-highlighter.js`、`MediaPlatform.js`、`replace.css` 等):默认视为私有作品,不应直接拷贝使用。
- 即便是“Safari 默认样式”类文件(例如 `default.css` 的注释提到 Safari也不建议直接照搬我们可以根据需求写一份“SDK 自己的 default.css / replace.css”只实现必要规则。
### 4.3 对本次需求的实际影响
本次 reflowable EPUB 走原生渲染,不依赖 WebView因此 **JS 不是本次必需**
CSS 方面我们需要的是“分层策略”和一小部分通用排版规则,可在 SDK 内重写为“WXRead 风格的默认样式集合”。
## 5. 详细设计(核心)
### 5.1 总体架构:在现有 `.textReflowable` 上增量替换 renderer
新增一个 renderer实现 `RDEPUBTextRenderer`
- `RDEPUBWXReadTextRenderer`(新)
- 输入:`html: String`, `baseURL: URL?`, `style: RDEPUBTextRenderStyle`
- 输出:`RDEPUBRenderedChapterContent``NSAttributedString` + `fragmentOffsets`
- 内部职责:
1) 读取/生成 CSS 各层default/replace/dark/user
2) 与 EPUB 自带 CSS 共同作用(通过 HTML 注入 + baseURL
3) 调用 DTCoreText builder 生成 attributedString
4) 做最小后处理(段落间距/字体/颜色标准化、fragment marker 提取等)
切换点:
- 在 `RDEPUBReaderController.resolvedTextRenderer()`(或其配置位置)增加策略:当开关启用时选择 `RDEPUBWXReadTextRenderer()`,否则沿用 `RDEPUBDTCoreTextRenderer()`
- 建议默认先提供“实验开关”(仅 Demo / debug 可见),降低回归风险。
### 5.2 CSS 分层策略WXRead 风格)
我们在 SDK 内实现与 `Doc/WXRead/analysis/EPUB渲染管线详解.md` 一致的分层概念,但不直接照搬其私有样式文件:
- Layer 1`default.css`SDK 自己维护的基础排版规则)
- Layer 2`replace.css`SDK 自己维护的替换/增强规则:标题、代码块、图片最大宽度等)
- Layer 3`dark.css`(暗色主题覆盖,仅在暗色主题启用)
- Layer 4EPUB 嵌入 CSS书籍自带DTCoreText 解析 HTML 时自然生效;相对路径靠 baseURL
- Layer 5用户设置 CSS`RDEPUBTextRenderStyle` 动态生成:字体、字号、行高、背景色、文字色等)
实现方式(建议):
1) 新增 `RDEPUBWXReadStyleSheetBuilder`
- `func makeDefaultCSS() -> String`
- `func makeReplaceCSS() -> String`
- `func makeDarkCSS(theme: RDEPUBTheme) -> String?`
- `func makeUserCSS(style: RDEPUBTextRenderStyle, theme: RDEPUBTheme) -> String`
- `func composeCSS(...) -> String`(按层拼接,后层覆盖前层)
2) 在 renderer 中将合成后的 CSS 注入到 HTML
- 若存在 `<head>`:插入 `<style id="rd-wxread-layered-style">...`
- 若不存在:在 `<html>` 后插入 `<head>...`
- 保持原 HTML 内容尽量不改动(外部脚本/交互内容已被 readingProfile 判定剔除到 web 分支)
### 5.3 baseURL 与资源解析
目前 `RDEPUBTextBookBuilder` 传入:
- `baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent()`
原则:
- baseURL 必须是“章节文件所在目录”,以确保:
- `<img src="...">` 相对路径可解析
- `<link href="...">` CSS 相对路径可解析(如果 DTCoreText 支持)
待核验点(实现时做小实验):
- DTCoreText 对 `<link rel="stylesheet">` 的解析策略是否完整;若不完整:
- 兜底策略:在渲染前解析 HTML 中的 `<link rel="stylesheet">`,读取 CSS 内容并内联到 `<style>`(仅限 `file://` 且位于 EPUB 解压目录内)。
### 5.4 分页策略(阶段性)
现状:
- `NSAttributedString.ss_pageRanges(size:)` 使用 `CTFramesetterCreateFrame` + `CTFrameGetVisibleStringRange`,属于“最小可用分页”。
WXRead 的更高阶策略(参考 `Doc/WXRead/analysis/DTCoreText自定义修改分析.md`)可能包含:
- 避免孤行/断页
- 图片/附件的分页边界处理
- 特定块元素的分页规则
本次建议:
- Phase 2先保持现有分页算法只要渲染输入CSS 分层 + 后处理)到位,就能显著改善一致性。
- Phase 3针对真实书籍出现的问题逐条补齐分页规则问题驱动避免一开始就引入复杂分页器导致风险扩大。
### 5.5 与现有高亮/搜索/位置映射的兼容
当前 `.textReflowable` 路径:
- 高亮/搜索依赖 `RDEPUBTextBook``fragmentOffsets``location/progression` 映射(见 `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift``RDEPUBTextBook``pageNumber(for:)` / `location(forPageNumber:)`)。
兼容策略:
- 继续使用现有的 fragment marker 注入与提取:
- `RDEPUBTextRendererSupport.injectFragmentMarkers(...)`
- `RDEPUBTextRendererSupport.extractFragmentOffsets(...)`
- renderer 只改变“CSS 注入与 DTCoreText options/后处理”,不改变 marker 体系与 `RDEPUBTextBook` 数据结构,以降低 UI 层回归。
## 6. 开发落点(文件 / 类型 / 目录)
### 6.1 新增文件(建议位置)
放在 `Sources/RDReaderView/EPUBTextRendering/`(因为它是 textReflowable 的渲染与分页域):
- `Sources/RDReaderView/EPUBTextRendering/RDEPUBWXReadTextRenderer.swift`(新)
- `Sources/RDReaderView/EPUBTextRendering/RDEPUBWXReadStyleSheetBuilder.swift`(新)
- (可选)`Sources/RDReaderView/EPUBTextRendering/RDEPUBWXReadHTMLPreprocessor.swift`(新:仅当需要内联 `<link>` CSS 时)
资源文件(建议):
- `Sources/RDReaderView/Resources/WXRead/default.css`SDK 自己写)
- `Sources/RDReaderView/Resources/WXRead/replace.css`SDK 自己写)
- `Sources/RDReaderView/Resources/WXRead/dark.css`SDK 自己写)
> 注意:这些资源需被 `RDReaderView.podspec` 的 resource bundle 覆盖到(当前资源 bundle 为 `RDReaderViewAssets`,来源 `Sources/RDReaderView/Resources/**`)。
### 6.2 改动文件(建议最小改动)
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
- 在 `resolvedTextRenderer()` 或相邻配置处增加选择逻辑(开关 / 版本策略)
- `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift`
- 如需内联 link CSS在获取 `rawHTML` 后做预处理(保持接口不变)
## 7. 验收标准与验证方式(对应 REQUIREMENTS
### 对应 REND-01 / REND-03
- 在 Demo 中打开 reflowable EPUB
- 正文渲染不依赖 `WKWebView`(可通过日志/断点确认不走 `RDEPUBWebContentView`
- CSS 分层生效:默认样式可控、主题/字号/行高变化可控
- 图片/链接至少可正确显示/响应(链接行为按现有 text 内容策略)
### 对应 REND-02
- Fixed Layout EPUB仍走 `.webFixedLayout``WKWebView` 路径
- 交互式 EPUB仍走 `.webInteractive``WKWebView` 路径(桥接与外链不回归)
### 对应 STAB-01 / STAB-02
- `RDURLReaderController` 打开 `.epub` / `.txt` 主流程不回归
- 2-3 本典型 reflowable EPUB不崩溃、不白屏、不无限加载分页/翻页可用
## 8. 风险清单与降级策略
### 8.1 主要风险
- R1DTCoreText 对 EPUB 内嵌 CSS / `<link>` CSS 支持不足,导致样式缺失
- R2分页质量不足断页不美观、图片分页异常
- R3`hasInteractiveContent()` 判定过宽,导致大量书被误判为 `.webInteractive`,覆盖率不足
### 8.2 降级/灰度(建议)
- D1增加一个“渲染引擎开关”仅 debug 或 demo 可配置),可在出现严重问题时快速回退到现有 `RDEPUBDTCoreTextRenderer`
- D2`hasInteractiveContent()` 的判定提供可配置白名单/黑名单(例如按 manifest properties、按 tag 命中级别)
## 9. 下一步(交接到开发)
建议按 `.planning/ROADMAP.md` 从 Phase 1 开始推进:
- 先基于 `Doc/WXRead/analysis/*` 提炼“我们要实现的 CSS 分层最小集合”
- 再在 `.textReflowable` renderer 里实现“分层 CSS 注入 + baseURL/资源解析兜底”
- 最后用 Demo 书籍做回归,按问题驱动补齐分页/样式细节
---
*Last updated: 2026-05-21 after discuss-feature-solution*