ReadViewSDK/Doc/FeatureSolution/ReflowableEPUB_WXReadRenderer_Design.md

15 KiB
Raw Blame History

Reflowable EPUB 使用 WXRead 风格原生渲染:详细设计

文档目的:讨论并固化“将 ReadViewSDK 的 reflowable EPUB 渲染/排版/分页改为参考 Doc/WXRead 的读书WXRead原生渲染方式”的可落地设计供后续开发与回归使用。
版本v0设计草案
日期2026-05-21

0. 背景与结论(先说人话)

ReadViewSDK 当前对 EPUB 有三类渲染路径:

  • RDEPUBReadingProfile.webFixedLayoutFixed Layout EPUB → WKWebView(保持不变)
  • RDEPUBReadingProfile.webInteractive:交互式 EPUBJS/音视频/表单/iframe/外链/bridgeWKWebView(保持不变)
  • 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
    • NSAttributedStringCoreText 分页)→ 单页内容
    • 单页内容 → 原生绘制/展示
  • 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. 关键事实核验(当前代码真实路径)

纠偏说明项目初始化时曾把”reflowable EPUB 当前路径”概括为偏 WKWebView 的历史性表述。经本次代码核验,当前真实主路径是 .textReflowableRDEPUBTextBookBuilderRDEPUBDTCoreTextRenderer → CoreText 分页 → RDEPUBTextContentView。本设计以代码事实为准,并默认后续实现都按此理解推进。

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. WREpubTypesetterXHTML → NSAttributedStringCSS 级联 + HTML 解析 + 后处理)
  3. WRCoreTextLayouterNSAttributedString → 分页布局(CTTypesetter + 分页算法)
  4. WRCoreTextLayoutFrame:单页 layout frame
  5. WRPageView:绘制到屏幕

本次设计对齐重点(最小集合):

  • ACSS 分层与合成default/replace/dark/epub/user并注入到渲染输入
  • B资源解析(图片/CSS 的相对路径 baseURL与稳定性保障
  • C在现有分页基础上逐步迭代先可用后对齐“避免断页”等高级策略

3.1 第一阶段实施假设(必须遵守)

  • H1第一期只对齐“管线形态”和“CSS 分层策略”,即把现有 .textReflowable renderer 增强为更接近 WXRead 的 typesetter 输入与样式组织方式。
  • H2第一期不实现 WXRead 对 DTCoreText 的深度魔改,包括但不限于自定义 CSS 属性体系、复杂附件布局规则、完整的 WRCoreTextLayouter / WRCoreTextLayoutFrame 等价分页器。
  • H3当开发过程中遇到图片断页、复杂样式缺失、附件布局异常等问题时默认先作为“第二阶段问题清单”记录只有在它阻塞 REND-01 / STAB-02 的最小验收时,才允许做局部补丁,而不是扩展为全面重写分页引擎。

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.jsMediaPlatform.jsreplace.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
    • 输出:RDEPUBRenderedChapterContentNSAttributedString + 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 1default.cssSDK 自己维护的基础排版规则)
  • Layer 2replace.cssSDK 自己维护的替换/增强规则:标题、代码块、图片最大宽度等)
  • Layer 3dark.css(暗色主题覆盖,仅在暗色主题启用)
  • Layer 4EPUB 嵌入 CSS书籍自带DTCoreText 解析 HTML 时自然生效;相对路径靠 baseURL
  • Layer 5用户设置 CSSRDEPUBTextRenderStyle 动态生成:字体、字号、行高、背景色、文字色等)

实现方式(建议):

  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针对真实书籍出现的问题逐条补齐分页规则问题驱动避免一开始就引入复杂分页器导致风险扩大。

范围约束:如果某个分页问题需要引入“新的复杂分页器”或大规模模拟 WRCoreTextLayouter / WRCoreTextLayoutFrame,应先暂停并回到方案讨论,不默认并入第一期实现。

5.5 与现有高亮/搜索/位置映射的兼容

当前 .textReflowable 路径:

  • 高亮/搜索依赖 RDEPUBTextBookfragmentOffsetslocation/progression 映射(见 Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swiftRDEPUBTextBookpageNumber(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.cssSDK 自己写)
  • Sources/RDReaderView/Resources/WXRead/replace.cssSDK 自己写)
  • Sources/RDReaderView/Resources/WXRead/dark.cssSDK 自己写)

注意:这些资源需被 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仍走 .webFixedLayoutWKWebView 路径
  • 交互式 EPUB仍走 .webInteractiveWKWebView 路径(桥接与外链不回归)

对应 STAB-01 / STAB-02

  • RDURLReaderController 打开 .epub / .txt 主流程不回归
  • 至少使用以下 3 类 reflowable EPUB 样本进行回归:
    • 样本 A纯文本/小说类章节为主,验证基础段落、标题、分页与阅读位置恢复
    • 样本 B包含内嵌图片与多段样式的章节验证图片显示、图片前后分页、基础 CSS 生效
    • 样本 C包含外链与多个 CSS 文件引用的章节,验证 baseURL、样式解析与链接呈现稳定性
  • 对以上样本的共同要求:不崩溃、不白屏、不无限加载;分页/翻页可用

8. 风险清单与降级策略

8.1 主要风险

  • R1DTCoreText 对 EPUB 内嵌 CSS / <link> CSS 支持不足,导致样式缺失
  • R2分页质量不足断页不美观、图片分页异常
  • R3hasInteractiveContent() 判定过宽,导致大量书被误判为 .webInteractive,覆盖率不足

8.2 降级/灰度(建议)

  • D1增加一个“渲染引擎开关”仅 debug 或 demo 可配置),可在出现严重问题时快速回退到现有 RDEPUBDTCoreTextRenderer
  • D2hasInteractiveContent() 的判定提供可配置白名单/黑名单(例如按 manifest properties、按 tag 命中级别)

9. 下一步(交接到开发)

建议按 Doc/ARCHITECTURE-CONTEXT.md 中的架构决策逐步推进:

  • 先基于 Doc/WXRead/analysis/* 提炼“我们要实现的 CSS 分层最小集合”
  • 再在 .textReflowable renderer 里实现“分层 CSS 注入 + baseURL/资源解析兜底”
  • 最后用 Demo 书籍做回归,按问题驱动补齐分页/样式细节

Last updated: 2026-05-21 after discuss-feature-solution