Epub阅读器0.0.1
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
# 微信读书 EPUB 阅读器 — 核心架构与数据流图
|
||||
|
||||
> 逆向工程目标: WeRead v10.0.3 (Build 79), arm64, 63MB 主二进制
|
||||
|
||||
---
|
||||
|
||||
## 一、模块拓扑与依赖关系图
|
||||
|
||||
### 1.1 App 整体模块拓扑
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ WeRead.app (iOS/macOS Catalyst) │
|
||||
│ 主二进制: 63MB, Mach-O arm64 │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 主二进制 (WeRead) │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │
|
||||
│ │ │ 阅读器模块 │ │ 书架模块 │ │ 其他业务模块 │ │ │
|
||||
│ │ │ WRReader* │ │ WRBookshelf* │ │ WRDiscover/WRMarket │ │ │
|
||||
│ │ │ WREpub* │ │ WRBook* │ │ WRAccount/WRSetting │ │ │
|
||||
│ │ │ DTCoreText* │ │ │ │ WRCommunity/WRChat │ │ │
|
||||
│ │ │ WRPage* │ │ │ │ WRActivity/WRAI │ │ │
|
||||
│ │ │ WRChapter* │ │ │ │ ... │ │ │
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ │
|
||||
│ │ ┌──────────────────────────────────────────────────────────┐│ │
|
||||
│ │ │ ObjC 元数据 (未剥离): ││ │
|
||||
│ │ │ __objc_classlist: 0xb230 (类列表) ││ │
|
||||
│ │ │ __objc_classname: 0x25a35 (~154KB, 17584 个类名) ││ │
|
||||
│ │ │ __objc_methname: 0x24ba5a (~2.4MB 方法名) ││ │
|
||||
│ │ │ __objc_methtype: 0x382bc (~226KB 类型编码) ││ │
|
||||
│ │ └──────────────────────────────────────────────────────────┘│ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────── Frameworks ────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ hermes.framework│ │QMUIKit.fwk │ │WRSharedMod* │ │ │
|
||||
│ │ │ (RN JS 引擎) │ │(腾讯UI组件库) │ │(共享业务模块) │ │ │
|
||||
│ │ │ v0.12.0 │ │ │ │ │ │ │
|
||||
│ │ │ Facebook Hermes │ │ QMUI 配色/ │ │ 书架/发现/ │ │ │
|
||||
│ │ │ 替代 JSC │ │ 主题/组件 │ │ 社区/活动 │ │ │
|
||||
│ │ └─────────────────┘ └──────────────┘ └──────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ WRUICore.fwk │ │CocoaMarkdown │ │ │
|
||||
│ │ │ (UI核心+字体) │ │(Markdown渲染)│ │ │
|
||||
│ │ │ 含 WeReadLS 系列│ │ │ │ │
|
||||
│ │ │ 含 SharpGrotesk │ │ │ │ │
|
||||
│ │ └─────────────────┘ └──────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────── RNBundles (39个 .hbc) ─────────────────┐ │
|
||||
│ │ aiSearch bookDetail browse community discoverV2 market │ │
|
||||
│ │ account medals memberDetail gameCenterHome freeBooks ... │ │
|
||||
│ │ (Hermes 编译的 HBC 字节码, 业务动态化) │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────── Bundles ────────────────────────────────┐ │
|
||||
│ │ OnePatch.bundle (ObjC热修复, JS驱动) │ │
|
||||
│ │ MidasIAPSDK.bundle (腾讯米大师支付) │ │
|
||||
│ │ Sentry.bundle (崩溃监控) │ │
|
||||
│ │ QCloudCOSXML.bundle (腾讯云COS存储) │ │
|
||||
│ │ MJRefresh.bundle (下拉刷新) │ │
|
||||
│ │ mathFonts.bundle (数学字体: Latin Modern/XITS/TeX Gyre) │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────── App Extensions ─────────────────────────┐ │
|
||||
│ │ ActionExtension.appex (操作扩展) │ │
|
||||
│ │ ModernWidgetExtension.appex (新版小组件 WidgetKit) │ │
|
||||
│ │ NotificationService.appex (通知服务) │ │
|
||||
│ │ ShareExtension.appex (分享扩展) │ │
|
||||
│ │ Widget.appex (旧版小组件) │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
系统框架依赖:
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ CoreText CTTypesetter / CTFramesetter / CTFrame │
|
||||
│ CTLine / CTRun / CTFontCreatePathForGlyph │
|
||||
│ CoreGraphics CGContext / CGPath / CGAffineTransform │
|
||||
│ UIKit UIView / UIPageViewController / Gesture │
|
||||
│ WebKit WKWebView (仅公众号/文集路径) │
|
||||
│ Foundation NSAttributedString / NSXMLParser │
|
||||
│ Security Keychain (加密密钥存储) │
|
||||
│ AVFoundation TTS 朗读音频播放 │
|
||||
│ PencilKit Apple Pencil 手写笔记 │
|
||||
│ ImageIO 图片解码与缓存 │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 EPUB 渲染模块内部依赖图
|
||||
|
||||
```
|
||||
WRReaderViewController (431 methods, 阅读器主控制器)
|
||||
│
|
||||
├─→ WRPageViewController (80 methods, 翻页控制器)
|
||||
│ │ 基于 UIPageViewController 封装
|
||||
│ │ 支持 UIPageCurl (仿真翻页) / Scroll (左右滑动)
|
||||
│ │ 包含 4 个故障修复补丁
|
||||
│ │
|
||||
│ └─→ WRPageView (105 methods, 页面渲染视图)
|
||||
│ │ 继承 UIView, drawRect: 中直接 CGContext 绘制
|
||||
│ │ 不使用 UILabel / UITextView
|
||||
│ │
|
||||
│ ├─→ WRCoreTextLayoutFrame (4 methods, 单页排版帧)
|
||||
│ │ │ drawInContext:image:size:inRect:position:
|
||||
│ │ │ avoidPageBreakInsideByRemovingLastLinesIfNeeded
|
||||
│ │ │ getRenderHeight / lines
|
||||
│ │ │
|
||||
│ │ └─→ DTCoreTextLayoutFrame (56 methods, DTCoreText 排版帧)
|
||||
│ │ │ 封装 CTFrame, 提取 CTLine 列表
|
||||
│ │ │ hitTestWithPoint: (文本选择)
|
||||
│ │ │
|
||||
│ │ └─→ DTCoreTextLayoutLine (61 methods, 排版行)
|
||||
│ │ │ 封装 CTLine, 提取 CTRun 列表
|
||||
│ │ │ typographicBounds / trailingWhitespaceWidth
|
||||
│ │ │
|
||||
│ │ └─→ DTCoreTextGlyphRun (23 methods, 字形渲染)
|
||||
│ │ │ 封装 CTRun
|
||||
│ │ └─→ newPathWithGlyphs (CTFontCreatePathForGlyph)
|
||||
│ │
|
||||
│ └─→ 文本选择 (CoreText hit test)
|
||||
│ CTLineGetStringIndexForPosition (坐标→字符索引)
|
||||
│ CTLineGetOffsetForStringIndex (字符索引→坐标)
|
||||
│
|
||||
├─→ WRChapterData (125 methods, 章节数据模型)
|
||||
│ │ 存储排版后的 NSAttributedString
|
||||
│ │ 管理高亮/下划线/书评标注
|
||||
│ │
|
||||
│ ├─→ WRCoreTextLayouter (20 methods, WeRead CoreText 封装)
|
||||
│ │ │ 管理 CTTypesetter / CTFramesetter
|
||||
│ │ │ pageBackgroundImageAtRange:themeBgColor:
|
||||
│ │ │ resizedImageForImagePath:rect:position:sizePattern:darkMode:themeBgColor:
|
||||
│ │ │
|
||||
│ │ └─→ DTCoreTextLayouter (12 methods, DTCoreText 排版器)
|
||||
│ │ │ 封装 CTTypesetter, 缓存 DTCoreTextLayoutFrame
|
||||
│ │ │
|
||||
│ │ └─→ CTTypesetter / CTFramesetter (系统 CoreText)
|
||||
│ │
|
||||
│ └─→ WRChapterPageCount (40 methods, 分页计算)
|
||||
│ rangeValueWithPageInfo: (计算每页 NSRange)
|
||||
│ currentCacheKeyWithBookId: (缓存键)
|
||||
│
|
||||
├─→ WREpubTypesetter (9 methods, EPUB 排版器入口)
|
||||
│ │ attributeStringWithFilePath:... (核心类方法)
|
||||
│ │
|
||||
│ └─→ DTHTMLAttributedStringBuilder (25 methods, HTML→NSAttributedString)
|
||||
│ │ _buildString (核心构建)
|
||||
│ │ parser:didStartElement:attributes:position: (SAX 回调)
|
||||
│ │ parser:foundCharacters:position:
|
||||
│ │ parserDidEndDocument:
|
||||
│ │
|
||||
│ ├─→ DTHTMLParser (19 methods, HTML SAX 解析器)
|
||||
│ │ 基于 libxml2 封装
|
||||
│ │
|
||||
│ ├─→ DTHTMLElement (168 methods, HTML 元素模型)
|
||||
│ │ │ applyStyleDictionary:isLatinLanguageBook: (CSS 应用)
|
||||
│ │ │ attributedString (生成富文本)
|
||||
│ │ │ interpretAttributes (属性解析)
|
||||
│ │ │
|
||||
│ │ ├─→ DTCoreTextFontDescriptor (字体描述)
|
||||
│ │ ├─→ DTCoreTextParagraphStyle (段落样式)
|
||||
│ │ ├─→ DTCSSStylesheet (CSS 样式表)
|
||||
│ │ └─→ DTTextAttachment (附件: 图片/视频/iframe)
|
||||
│ │
|
||||
│ └─→ CSS 级联: default.css → replace.css → dark.css → epub内嵌 → 用户设置
|
||||
│
|
||||
├─→ WREpubParser (12 methods, EPUB 文件解析)
|
||||
│ │ 解析 container.xml / content.opf / toc.ncx
|
||||
│ │ 输出: 章节列表 + 资源映射
|
||||
│ └─→ WREpubPositionConverter (22 methods, 位置转换)
|
||||
│ 文件位置 ↔ 字符位置 映射
|
||||
│
|
||||
├─→ WRChapterDownloadManger (47 methods, 章节下载)
|
||||
│ │ RAC 任务队列 + 并发控制
|
||||
│ │
|
||||
│ ├─→ WRBookNetwork (155 methods, 网络层)
|
||||
│ │ │ loadTarForEpubBookId:chapter:isPreload: (下载加密 ZIP)
|
||||
│ │ │ handleUnzipWithBookId:zipPath:encryptKey:plainBookDirectory: (解密解压)
|
||||
│ │ │ processEncryptedBookFileAtPath:encryptKey:book:chapterUid: (文件处理)
|
||||
│ │ │ fileContentWithChapter:book:shouldRemoveHtmlTags: (读取内容)
|
||||
│ │ │
|
||||
│ │ └─→ WREncryptedFileManager (7 methods, DRM 管理)
|
||||
│ │ decryptContentsOfFile:forBookId:isFileLost: (AES 解密)
|
||||
│ │ encryptFileForBookId:originalEncryptKey:atPath:toPath: (本地加密)
|
||||
│ │ keyForBookId: (密钥管理)
|
||||
│ │
|
||||
│ └─→ WRPreloadBookManager (76 methods, 预加载管理)
|
||||
│ saveEncryptKey:forPath:bookId: (密钥缓存)
|
||||
│ _preloadWholeBook:scene: (整书预加载)
|
||||
│
|
||||
├─→ 标注系统
|
||||
│ ├─→ WRPageHighlight (3 methods, 高亮)
|
||||
│ ├─→ WRPageUnderline (10 methods, 下划线: solid/dashed/wavy/dotted)
|
||||
│ ├─→ WRPageMark (25 methods, 书签标注)
|
||||
│ ├─→ WRBookmark (58 methods, 书签模型)
|
||||
│ └─→ WRReaderPencilNoteManager (11 class methods, Pencil 笔记)
|
||||
│
|
||||
└─→ 附件系统 (WRPageAttachments)
|
||||
├─→ WRPageImageAttachment (27 methods, 图片附件)
|
||||
├─→ WRPageHyperlinksAttachment (超链接)
|
||||
├─→ WRPageChapterToolAttachment (章节工具)
|
||||
├─→ WRPageFlyleafAttachment (扉页)
|
||||
├─→ WRPageCodeView (代码块)
|
||||
├─→ WRPageVideoView (视频)
|
||||
└─→ WRPageTableAttachment (表格)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、核心业务数据流图 (DFD)
|
||||
|
||||
### 2.1 EPUB 内容数据流 (服务器 → 屏幕像素)
|
||||
|
||||
```
|
||||
┌──────────┐ HTTPS ┌──────────────────────────────────────────────────┐
|
||||
│ 微信读书 │─────────────→│ ① WRBookNetwork.loadTarForEpubBookId:chapter: │
|
||||
│ 服务器 │ 加密 ZIP │ isPreload: │
|
||||
│ │ {bookId}_ │ ↓ │
|
||||
│ │ DECRYPT.zip │ ② WRBookNetwork.handleUnzipWithBookId: │
|
||||
│ │ │ zipPath:encryptKey:plainBookDirectory: │
|
||||
│ │ │ ↓ │
|
||||
│ │ │ ③ WREncryptedFileManager │
|
||||
│ │ │ decryptContentsOfFile:forBookId:isFileLost: │
|
||||
│ │ │ 密钥来源: keyForBookId: (Keychain) │
|
||||
│ │ │ ↓ 解密后 XHTML 文件 │
|
||||
│ │ │ │
|
||||
│ │ │ ④ WRBookNetwork.fileContentWithChapter: │
|
||||
│ │ │ book:shouldRemoveHtmlTags:filterTranslate: │
|
||||
│ │ │ ↓ 读取 XHTML 内容 │
|
||||
│ │ │ │
|
||||
│ │ │ ⑤ WREpubTypesetter.attributeStringWithFilePath: │
|
||||
│ │ │ ↓ CSS 级联 (5层) │
|
||||
│ │ │ ↓ DTHTMLAttributedStringBuilder._buildString │
|
||||
│ │ │ ↓ DTHTMLParser (SAX) → DTHTMLElement (DOM) │
|
||||
│ │ │ ↓ DTHTMLElement.attributedString │
|
||||
│ │ │ ↓ 输出: NSAttributedString │
|
||||
│ │ │ │
|
||||
│ │ │ ⑥ WRCoreTextLayouter (CTTypesetter 分页) │
|
||||
│ │ │ ↓ CTTypesetterSuggestLineBreak (逐行排版) │
|
||||
│ │ │ ↓ 每页一个 CTFrame │
|
||||
│ │ │ ↓ WRChapterPageCount (NSRange 映射) │
|
||||
│ │ │ │
|
||||
│ │ │ ⑦ WRCoreTextLayoutFrame (单页排版帧) │
|
||||
│ │ │ ↓ CTFrameGetLines → CTLine 列表 │
|
||||
│ │ │ ↓ avoidPageBreakInside (截断保护) │
|
||||
│ │ │ │
|
||||
│ │ │ ⑧ WRPageView.drawRect: │
|
||||
│ │ │ CGContextTranslateCTM (坐标翻转) │
|
||||
│ │ │ CTFrameDraw (绘制文字) │
|
||||
│ │ │ 绘制图片/高亮/下划线/选择 │
|
||||
│ │ │ ↓ │
|
||||
└──────────┘ └──────────────────────────────────────────────────┘
|
||||
↓
|
||||
屏幕像素渲染
|
||||
```
|
||||
|
||||
### 2.2 加密密钥数据流 (DRM 保护链)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ DRM 密钥生命周期 │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────┐ │
|
||||
│ │ 服务器 │─── encryptKey (每章独立) ───┐ │
|
||||
│ └──────────┘ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ WRPreloadBookManager │ │
|
||||
│ │ saveEncryptKey:forPath:bookId: │ │
|
||||
│ │ ↓ 存储到: NSUserDefaults (临时) │ │
|
||||
│ │ encryptKeyForPath:bookId: (读取) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ WREncryptedFileManager │ │
|
||||
│ │ keyForBookId: │ │
|
||||
│ │ ↓ 优先级: Keychain > NSUserDefaults > 内存缓存 │ │
|
||||
│ │ │ │
|
||||
│ │ decryptContentsOfFile:forBookId:isFileLost: │ │
|
||||
│ │ ↓ 算法: AES-128-CBC │ │
|
||||
│ │ ↓ 输入: 加密的 XHTML 数据 │ │
|
||||
│ │ ↓ 输出: 明文 XHTML │ │
|
||||
│ │ ↓ 检测: "REWD" 自定义头部标识 │ │
|
||||
│ │ │ │
|
||||
│ │ encryptFileForBookId:originalEncryptKey:atPath:toPath: │ │
|
||||
│ │ ↓ 解密后立即重新加密存储 (本地 DRM) │ │
|
||||
│ │ ↓ 防止直接拷贝文件读取 │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 本地存储 │ │
|
||||
│ │ Documents/{bookId}/plainBookDirectory/ (解密后的 EPUB) │ │
|
||||
│ │ Documents/{bookId}/epubImage/ (书籍图片缓存) │ │
|
||||
│ │ Library/{cachePath}/epubImage/ (SDWebImage 缓存) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 密钥安全机制: │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 1. 传输层: HTTPS + 加密 ZIP │ │
|
||||
│ │ 2. 解密层: 逐章解密 (WREncryptedFileManager) │ │
|
||||
│ │ 3. 存储层: 本地二次加密 (encryptFileForBookId) │ │
|
||||
│ │ 4. 密钥层: 每本书独立密钥 + Keychain 安全存储 │ │
|
||||
│ │ 5. 设备层: 密钥与设备绑定 │ │
|
||||
│ │ 6. 试读层: freeTrialChapterCutOffStringLocaion (服务端控制) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 用户标注数据流
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 用户触摸/长按 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ WRPageView 手势识别 (UITapGestureRecognizer / UILongPressGestureRecognizer)
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ CoreText Hit Test │
|
||||
│ touchPoint = WRSFlipPointForCoreText(touch, viewHeight) // 坐标翻转
|
||||
│ CTLineGetStringIndexForPosition(line, touchPoint) // 坐标→字符索引
|
||||
│ CTLineGetOffsetForStringIndex(line, charIndex, NULL) // 字符索引→坐标
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 选择范围确定 (selectionStartIndex .. selectionEndIndex) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ UIMenuController 弹出操作菜单 │
|
||||
│ ├─ 划线/高亮 ─→ WRChapterData.addHighlightInRange:key:itemId:color:│
|
||||
│ ├─ 写想法 ─→ WRBookNetwork.addReview:shareToWechat:... │
|
||||
│ ├─ 复制 ─→ UIPasteboard │
|
||||
│ ├─ 查询/翻译 ─→ WRReaderDictionaryViewController │
|
||||
│ └─ 分享 ─→ WRShareManager │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ WRChapterData 标注存储 │
|
||||
│ NSAttributedString 注入自定义属性: │
|
||||
│ "com.weread.highlight" → 高亮数据 │
|
||||
│ "com.weread.underline" → 下划线数据 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 服务器同步 │
|
||||
│ WRBookNetwork.addReview:shareToWechat:audioArticleId:... │
|
||||
│ WRBookNetwork.loadBookmarkListWithBookId:syncKey:callback: │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 重新排版 │
|
||||
│ WRReaderViewController.recomposeCurrentPageViewWithSource: │
|
||||
│ → WRPageView.setNeedsDisplay → drawRect: 重绘 │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.4 阅读进度数据流
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 本地进度持久化 │
|
||||
│ WRReaderViewController._saveReadingProgressAndIsAsync: │
|
||||
│ ↓ NSUserDefaults 存储: │
|
||||
│ - bookId / chapterUid / chapterIdx │
|
||||
│ - filePosition / stringLocation │
|
||||
│ - pageOfChapter / pageOfFlyleaf │
|
||||
│ - lastReadDate / readingTime │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 云端同步 │
|
||||
│ WRBookNetwork.uploadBookProgressAndReadingTime: │
|
||||
│ ↓ POST /api/book/progress │
|
||||
│ ↓ 参数: bookId, chapterUid, progress, readingTime │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 启动恢复 │
|
||||
│ WRReaderViewController.initWithBook:progress:... │
|
||||
│ ↓ loadBookReadInfoWithBookId: (从服务器获取最新进度) │
|
||||
│ ↓ gotoChapterIdx:position:positionOfFile: (跳转到上次位置) │
|
||||
│ ↓ scrollToLastReadingProgressIfNeeded │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.5 预加载数据流
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 预测触发 │
|
||||
│ WRForecastUtils.shouldPreloadChapterUidsForBook: │
|
||||
│ type:bookRank:archiveRank: │
|
||||
│ ↓ 根据书籍排名 + 用户行为预测 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 预加载执行 │
|
||||
│ WRChapterDownloadManger._preloadChapterContentWithBook: │
|
||||
│ type:bookRank: │
|
||||
│ ↓ 后台线程下载相邻章节 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 下载 + 解密 + 缓存 │
|
||||
│ WRBookNetwork.loadTarForEpubBookId:chapters:isPreload: │
|
||||
│ → handleUnzipWithBookId → WREncryptedFileManager │
|
||||
│ → 存储到 Documents/{bookId}/plainBookDirectory/ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 图片预缓存 │
|
||||
│ WRBookNetwork.storeTarEpubImageToDiskWithBookId: │
|
||||
│ chapterUid:untarDirectory: │
|
||||
│ → SDWebImage → com.hackemist.SDWebImageCache.epubImage │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 密钥预缓存 │
|
||||
│ WRPreloadBookManager.saveEncryptKey:forPath:bookId: │
|
||||
│ → NSUserDefaults / Keychain │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.6 JS Bridge 数据流 (WebView 路径 — 公众号/文集文章)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ JS → 原生 │
|
||||
│ window.webkit.messageHandlers.MPReader.postMessage(data) │
|
||||
│ wereadBridge.execMPReaderMethod('MPReader', data) │
|
||||
│ ↓ WKScriptMessageHandler │
|
||||
│ ↓ WRMPReadingViewModel 解析 JSON │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 原生 → JS │
|
||||
│ [webView evaluateJavaScript:@"..." completionHandler:nil] │
|
||||
│ ↓ WKUserScript 注入脚本 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 高亮系统 (weread-highlighter.js) │
|
||||
│ rangy.init() → 创建 Highlighter (TextRange 模式) │
|
||||
│ 注册 ClassApplier: "highlight"/"review"/"reference"/"tts" │
|
||||
│ 监听 selectionchange 事件 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 通信协议: wereadapijs://dispatch_message/ │
|
||||
│ URL Scheme 编码消息, iframe 触发 │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -0,0 +1,779 @@
|
||||
# 微信读书 EPUB 阅读器 — 符号恢复与核心算法
|
||||
|
||||
> 基于 WeRead v10.0.3 (Build 79) 二进制逆向
|
||||
> 方法签名来源: __objc_methname 段 + binary strings
|
||||
> 类名来源: __objc_classname 段 (17584 个类, 其中 320 个 EPUB 相关)
|
||||
|
||||
---
|
||||
|
||||
## 一、符号恢复映射表
|
||||
|
||||
### 1.1 核心类方法签名恢复 (439 个)
|
||||
|
||||
#### WREpubTypesetter (EPUB 排版器入口)
|
||||
|
||||
```
|
||||
恢复前 (二进制地址) 恢复后 (符号名) 功能
|
||||
─────────────────────────────────────────────────────────────────────────────────────
|
||||
imp@0x100a489ac +[WREpubTypesetter attributeStringWithFilePath: XHTML→NSAttributedString
|
||||
priority: 核心排版方法
|
||||
insertArticleToolAttachment: 5层CSS级联
|
||||
insertBookChapterToolAttachment: 图片/链接处理
|
||||
insertRecommendView: 繁简转换
|
||||
book:chapter:pageFlippingStyle: 免费试读截断
|
||||
renderErrorReason:isStyleFileNotFound:options:]
|
||||
|
||||
imp@0x100a49b9c +[WREpubTypesetter tryReportTranslationError: 翻译错误上报
|
||||
bookId:chapter:
|
||||
isTranslationStyleNotFound:
|
||||
isTranslationContentNotFound:
|
||||
isTranslateTagButNoTranslateStyle:]
|
||||
```
|
||||
|
||||
#### DTHTMLAttributedStringBuilder (HTML→NSAttributedString 构建器)
|
||||
|
||||
```
|
||||
imp@0x... -[DTHTMLAttributedStringBuilder _buildString] 核心构建方法
|
||||
imp@0x... -[DTHTMLAttributedStringBuilder parser:didStartElement: SAX 标签开始
|
||||
attributes:position:]
|
||||
imp@0x... -[DTHTMLAttributedStringBuilder parser:foundCDATA:] CDATA 处理
|
||||
imp@0x... -[DTHTMLAttributedStringBuilder parser:foundCharacters: 文本节点
|
||||
position:]
|
||||
imp@0x... -[DTHTMLAttributedStringBuilder parserDidEndDocument:] 解析完成
|
||||
```
|
||||
|
||||
#### DTHTMLElement (HTML 元素模型, 168 methods)
|
||||
|
||||
```
|
||||
imp@0x... -[DTHTMLElement applyStyleDictionary: CSS 样式应用
|
||||
isLatinLanguageBook:]
|
||||
imp@0x... -[DTHTMLElement attributedString] 生成富文本
|
||||
imp@0x... -[DTHTMLElement interpretAttributes] 属性解析
|
||||
```
|
||||
|
||||
#### WRCoreTextLayoutFrame (单页排版帧, 49 methods)
|
||||
|
||||
```
|
||||
imp@0x... -[WRCoreTextLayoutFrame drawInContext: 核心绘制方法
|
||||
image:size:inRect:position:] CGContext 绘制
|
||||
imp@0x... -[WRCoreTextLayoutFrame avoidPageBreakInside 避免断页
|
||||
ByRemovingLastLinesIfNeeded] 移除最多3行
|
||||
imp@0x... -[WRCoreTextLayoutFrame getRenderHeight] 获取渲染高度
|
||||
imp@0x... -[WRCoreTextLayoutFrame lines] 获取行列表
|
||||
```
|
||||
|
||||
#### DTCoreTextGlyphRun (字形渲染)
|
||||
|
||||
```
|
||||
imp@0x... -[DTCoreTextGlyphRun newPathWithGlyphs] CGPath 字形路径
|
||||
CTFontCreatePathForGlyph
|
||||
```
|
||||
|
||||
#### WRChapterData (章节数据模型, 125 methods)
|
||||
|
||||
```
|
||||
imp@0x... +[WRChapterData addUnderLineToAttributedString: 添加下划线
|
||||
range:itemId:style:color:]
|
||||
imp@0x... +[WRChapterData freeTrialChapterCutOffStringLocaion 试读截断位置
|
||||
WithAttributedString:book:]
|
||||
imp@0x... -[WRChapterData addHighlightInRange: 添加高亮
|
||||
key:itemId:color:]
|
||||
imp@0x... -[WRChapterData addReviewUnderlineInRange: 添加书评下划线
|
||||
itemId:type:]
|
||||
imp@0x... -[WRChapterData rangeOfPage:] 获取页范围
|
||||
imp@0x... -[WRChapterData generateOutlineContents] 生成目录
|
||||
imp@0x... -[WRChapterData markFreeTrialChapterCutOffString 标记试读截断
|
||||
Location:]
|
||||
```
|
||||
|
||||
#### WRPageViewController (翻页控制器, 80 methods)
|
||||
|
||||
```
|
||||
imp@0x... -[WRPageViewController initWithDelegate: 初始化
|
||||
withPageType:pageFlippingStyle:]
|
||||
imp@0x... -[WRPageViewController pageViewController: 上一页
|
||||
viewControllerBeforeViewController:]
|
||||
imp@0x... -[WRPageViewController pageViewController: 下一页
|
||||
viewControllerAfterViewController:]
|
||||
imp@0x... +[WRPageViewController patchNavigationDirectionFault] 导航方向修复
|
||||
imp@0x... +[WRPageViewController patchNoViewController 页面管理修复
|
||||
ManagingPageViewFault]
|
||||
imp@0x... +[WRPageViewController patchUIPageCurlFault] 翻页动画修复
|
||||
imp@0x... +[WRPageViewController detectNavigationDirection 崩溃检测
|
||||
CrashWithPageViewController:...]
|
||||
```
|
||||
|
||||
#### WRReaderViewController (阅读器主控制器, 431 methods)
|
||||
|
||||
```
|
||||
imp@0x... -[WRReaderViewController initWithBook: 初始化
|
||||
progress:forceUseInitialProgress:
|
||||
doodleMode:autoRead:]
|
||||
imp@0x... -[WRReaderViewController renderPageView: 渲染页面
|
||||
progressData:source:]
|
||||
imp@0x... -[WRReaderViewController recomposeCurrentPageView 重新排版
|
||||
WithSource:]
|
||||
imp@0x... -[WRReaderViewController gotoChapterIdx: 跳转章节
|
||||
position:positionOfFile:]
|
||||
imp@0x... -[WRReaderViewController _saveReadingProgress 保存进度
|
||||
AndIsAsync:]
|
||||
imp@0x... -[WRReaderViewController changeTypesetterAttributes 修改排版属性
|
||||
WithBlock:]
|
||||
imp@0x... -[WRReaderViewController initChapterPageCount] 初始化分页
|
||||
```
|
||||
|
||||
#### WREncryptedFileManager (DRM 管理)
|
||||
|
||||
```
|
||||
imp@0x... +[WREncryptedFileManager decryptContentsOfFile: AES 解密
|
||||
forBookId:isFileLost:]
|
||||
imp@0x... +[WREncryptedFileManager encryptFileForBookId: 本地加密
|
||||
originalEncryptKey:atPath:toPath:]
|
||||
imp@0x... +[WREncryptedFileManager keyForBookId:] 获取密钥
|
||||
```
|
||||
|
||||
#### WRBookNetwork (网络层, 44 class methods)
|
||||
|
||||
```
|
||||
+[WRBookNetwork loadTarForEpubBookId:chapter:isPreload:] 下载加密 ZIP
|
||||
+[WRBookNetwork loadTarForEpubBookId:chapters:isPreload:] 批量下载
|
||||
+[WRBookNetwork handleUnzipWithBookId:zipPath:encryptKey:...] 解密解压
|
||||
+[WRBookNetwork handleUnzipErrorWithPath:plainBookDirectory:] 解压错误处理
|
||||
+[WRBookNetwork processEncryptedBookFileAtPath:encryptKey:...] 文件处理
|
||||
+[WRBookNetwork fileContentWithChapter:book:shouldRemoveHtmlTags: 读取内容
|
||||
+:filterTranslateContent:]
|
||||
+[WRBookNetwork loadChapterContentWithParam:callback:] 加载章节内容
|
||||
+[WRBookNetwork chaptersInfoFromFile:checkTranslate:] 章节信息解析
|
||||
+[WRBookNetwork savePreloadInfoWithDownloadParam:...] 保存预加载信息
|
||||
+[WRBookNetwork clearPreloadKVWithBookId:chapterUid:zipPath:] 清理预加载缓存
|
||||
+[WRBookNetwork storeTarEpubImageToDiskWithBookId:...] 存储图片
|
||||
+[WRBookNetwork loadBookmarkListWithBookId:syncKey:callback:] 加载书签
|
||||
+[WRBookNetwork addReview:shareToWechat:audioArticleId:...] 添加书评
|
||||
+[WRBookNetwork searchResultsForBook:chapterUid:searchString:...] 搜索
|
||||
+[WRBookNetwork uploadBookProgressAndReadingTime:...] 上传进度
|
||||
+[WRBookNetwork loadBookInfoWithBookId:source:callback:] 书籍信息
|
||||
+[WRBookNetwork loadBookReadInfoWithBookId:callback:] 阅读信息
|
||||
+[WRBookNetwork fetchLockInfoWithBookId:callback:] 锁定信息
|
||||
+[WRBookNetwork resetChapterPaidIfNeededWithBookId:chapterUid:] 重置付费
|
||||
+[WRBookNetwork markReadingStatus:bookIds:isCancel:callback:] 标记阅读状态
|
||||
+[WRBookNetwork setFinishReading:withBookId:callback:] 标记完成
|
||||
+[WRBookNetwork setIsStartReading:withBookId:callback:] 标记开始
|
||||
+[WRBookNetwork likeReviewById:isUnlike:withParams:callback:] 点赞
|
||||
+[WRBookNetwork dislikeReviewById:isDislike:withParams:callback:] 点踩
|
||||
+[WRBookNetwork repostReview:reposted:callback:] 转发
|
||||
+[WRBookNetwork rewardReviewForId:price:timestamp:callback:] 打赏
|
||||
+[WRBookNetwork postReviewHideWithBookId:hide:callback:] 隐藏书评
|
||||
+[WRBookNetwork loadReadTimeWelfareActionWithBookId:...] 阅读时长福利
|
||||
+[WRBookNetwork automaticallyMarkFinishReadingWithBookId:...] 自动标记完成
|
||||
+[WRBookNetwork pollingChapterTranslateStatusWithBookId:...] 轮询翻译状态
|
||||
+[WRBookNetwork _removeTranslateHtml:] 移除翻译HTML
|
||||
+[WRBookNetwork processChapterInfosFromChapterDownload:bookId:] 处理章节信息
|
||||
```
|
||||
|
||||
### 1.2 ivar 恢复表 (关键类)
|
||||
|
||||
```
|
||||
WREpubTypesetter (4 ivars):
|
||||
_cssFilePath NSString 内嵌 CSS 文件路径
|
||||
_replaceCSSPath NSString replace.css 路径
|
||||
_darkCSSPath NSString dark.css 路径
|
||||
_options NSArray 排版选项
|
||||
|
||||
WRChapterData (12 ivars):
|
||||
_attributedString NSMutableAttributedString 排版后的富文本
|
||||
_pageRanges NSArray 每页 NSRange 数组
|
||||
_highlights NSArray 高亮列表
|
||||
_underlines NSArray 下划线列表
|
||||
_reviews NSArray 书评列表
|
||||
_tempHighlights NSArray 临时高亮
|
||||
_tempUnderlines NSArray 临时下划线
|
||||
_layouter WRCoreTextLayouter 排版器引用
|
||||
_outlineContents NSArray 目录内容
|
||||
_searchResults NSArray 搜索结果
|
||||
_bookmarkSet NSSet 书签集合
|
||||
_originalString NSAttributedString 原始富文本
|
||||
|
||||
WRCoreTextLayouter (32 ivars):
|
||||
_htmlString NSString HTML 源码
|
||||
_attributedString NSAttributedString 排版后的富文本
|
||||
_layoutString NSAttributedString 实际排版用的富文本
|
||||
_book WRBook 书籍引用
|
||||
_chapter WRChapter 章节引用
|
||||
_markContentChapter WRMarkContentChapter 标注内容
|
||||
...
|
||||
|
||||
WRCoreTextLayoutFrame (25 ivars):
|
||||
_attributedString NSAttributedString 排版用富文本
|
||||
_lines NSMutableArray DTCoreTextLayoutLine 数组
|
||||
_frame NSString CTFrame 描述
|
||||
_stringIndices NSArray 字符索引映射
|
||||
_pageRanges NSArray 页面范围
|
||||
...
|
||||
|
||||
WRPageView (16 ivars):
|
||||
_pageElements NSArray 页面元素列表
|
||||
_imageViews NSMutableArray 图片视图缓存
|
||||
_autoReadTimer NSTimer 自动阅读定时器
|
||||
_turnPageTimer NSTimer 翻页定时器
|
||||
_chapterTitle NSString 章节标题
|
||||
_loadingLabel UILabel 加载提示
|
||||
_loadingImageView UIImageView 加载图片
|
||||
_activityIndicator WRActivityIndicator 加载指示器
|
||||
_errorLabel UILabel 错误提示
|
||||
_retryButton QMUIButton 重试按钮
|
||||
_loadingProgress WRLoadingProgressView 加载进度
|
||||
...
|
||||
|
||||
DTHTMLElement (38 ivars):
|
||||
_textAttachment DTTextAttachment 文本附件
|
||||
_linkURL NSURL 链接地址
|
||||
_textColor UIColor 文字颜色
|
||||
_backgroundColor UIColor 背景色
|
||||
_tagName NSString 标签名
|
||||
_children NSArray 子元素
|
||||
_attributes NSDictionary HTML 属性
|
||||
_cssStyles NSDictionary CSS 样式
|
||||
_borderStyle DTBorderStyle 边框样式
|
||||
_backgroundImage DTBackgroundImageStyle 背景图
|
||||
_tableStyle DTTableStyle 表格样式
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、精简核心算法实现 (Python)
|
||||
|
||||
### 2.1 EPUB CSS 级联算法
|
||||
|
||||
```python
|
||||
def cascade_stylesheets(default_css: str, replace_css: str, dark_css: str,
|
||||
epub_css: str, user_settings_css: str) -> dict:
|
||||
"""
|
||||
5 层 CSS 级联: default → replace → dark → epub内嵌 → 用户设置
|
||||
后面的层覆盖前面的层中相同选择器+属性的值。
|
||||
|
||||
Args:
|
||||
default_css: Safari 默认样式 (default.css)
|
||||
replace_css: 微信读书默认替换 (replace.css)
|
||||
dark_css: 暗黑主题 (dark.css)
|
||||
epub_css: EPUB 书籍内嵌 CSS
|
||||
user_settings_css: 用户自定义设置 (字号/行高/主题)
|
||||
|
||||
Returns:
|
||||
dict: {selector: {property: value}} 合并后的样式表
|
||||
"""
|
||||
import re
|
||||
|
||||
def parse_css(css_text: str) -> dict:
|
||||
"""解析 CSS 文本为 {selector: {prop: val}} 字典"""
|
||||
rules = {}
|
||||
# 移除注释
|
||||
css_text = re.sub(r'/\*.*?\*/', '', css_text, flags=re.DOTALL)
|
||||
# 匹配选择器和声明块
|
||||
for match in re.finditer(r'([^{}]+)\{([^{}]+)\}', css_text):
|
||||
selectors = match.group(1).strip()
|
||||
declarations = match.group(2).strip()
|
||||
for selector in selectors.split(','):
|
||||
selector = selector.strip()
|
||||
if selector not in rules:
|
||||
rules[selector] = {}
|
||||
for decl in declarations.split(';'):
|
||||
decl = decl.strip()
|
||||
if ':' in decl:
|
||||
prop, val = decl.split(':', 1)
|
||||
rules[selector][prop.strip()] = val.strip()
|
||||
return rules
|
||||
|
||||
def merge(base: dict, override: dict) -> dict:
|
||||
"""合并两个样式表, override 覆盖 base"""
|
||||
result = {k: dict(v) for k, v in base.items()}
|
||||
for selector, props in override.items():
|
||||
if selector not in result:
|
||||
result[selector] = {}
|
||||
result[selector].update(props)
|
||||
return result
|
||||
|
||||
# 按优先级逐层合并
|
||||
result = parse_css(default_css) # 层1: 基础
|
||||
result = merge(result, parse_css(replace_css)) # 层2: 微信读书默认
|
||||
result = merge(result, parse_css(dark_css)) # 层3: 暗黑主题
|
||||
result = merge(result, parse_css(epub_css)) # 层4: EPUB 内嵌
|
||||
result = merge(result, parse_css(user_settings_css)) # 层5: 用户设置 (最高优先级)
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
### 2.2 CoreText 分页算法
|
||||
|
||||
```python
|
||||
def paginate_attributed_string(attr_string_length: int,
|
||||
page_width: float,
|
||||
page_height: float,
|
||||
line_height: float,
|
||||
line_spacing: float,
|
||||
paragraph_spacing: float) -> list:
|
||||
"""
|
||||
使用 CTTypesetterSuggestLineBreak 逐行排版, 累计高度超过页面高度时换页。
|
||||
|
||||
等价于 WRChapterPageCount.recalculatePageRangesForAttributedString
|
||||
|
||||
Args:
|
||||
attr_string_length: 富文本总字符数
|
||||
page_width: 页面宽度 (pt)
|
||||
page_height: 页面高度 (pt)
|
||||
line_height: 行高 (pt)
|
||||
line_spacing: 行间距 (pt)
|
||||
paragraph_spacing: 段间距 (pt)
|
||||
|
||||
Returns:
|
||||
list of (start_index, length) 每页的 NSRange
|
||||
"""
|
||||
page_ranges = []
|
||||
current_index = 0
|
||||
page_start = 0
|
||||
total_height = 0.0
|
||||
|
||||
while current_index < attr_string_length:
|
||||
# CTTypesetterSuggestLineBreak: 给定当前位置和可用宽度, 返回本行能放多少字符
|
||||
# 在真实代码中这是 CoreText 的 C 函数调用
|
||||
line_break_index = suggest_line_break(current_index, page_width)
|
||||
|
||||
# 计算本行高度 (包含行间距)
|
||||
current_line_height = line_height + line_spacing
|
||||
|
||||
# 检查是否需要换页
|
||||
if total_height + current_line_height > page_height:
|
||||
# 当前页结束
|
||||
page_ranges.append((page_start, current_index - page_start))
|
||||
page_start = current_index
|
||||
total_height = 0.0
|
||||
|
||||
total_height += current_line_height
|
||||
current_index += line_break_index
|
||||
|
||||
# 最后一页
|
||||
if page_start < attr_string_length:
|
||||
page_ranges.append((page_start, attr_string_length - page_start))
|
||||
|
||||
return page_ranges
|
||||
|
||||
|
||||
def suggest_line_break(start_index: int, available_width: float) -> int:
|
||||
"""
|
||||
模拟 CTTypesetterSuggestLineBreak
|
||||
从 start_index 开始, 在 available_width 内能放置的字符数
|
||||
|
||||
简化实现: 按空格/标点断行, 累计字符宽度
|
||||
"""
|
||||
# 实际实现依赖 CTTypesetter, 这里是逻辑等价
|
||||
# CoreText 内部会考虑:
|
||||
# - 字符宽度 (CTFontGetAdvancesForGlyphs)
|
||||
# - 字间距 (kCTKernAttributeName)
|
||||
# - 断行机会 (Unicode Line Break Algorithm)
|
||||
# - 连字符断行 (CTTypesetterSuggestLineBreakWithHyphenation)
|
||||
pass
|
||||
```
|
||||
|
||||
### 2.3 avoidPageBreakInside 算法
|
||||
|
||||
```python
|
||||
def avoid_page_break_inside(lines: list, page_height: float,
|
||||
max_lines_to_remove: int = 3) -> list:
|
||||
"""
|
||||
如果最后几行被截断, 移除最多 max_lines_to_remove 行以避免内容破碎。
|
||||
|
||||
等价于 WRCoreTextLayoutFrame.avoidPageBreakInsideByRemovingLastLinesIfNeeded
|
||||
|
||||
Args:
|
||||
lines: DTCoreTextLayoutLine 列表, 每个有 height 属性
|
||||
page_height: 页面可用高度 (pt)
|
||||
max_lines_to_remove: 最多移除行数 (默认 3)
|
||||
|
||||
Returns:
|
||||
调整后的 lines 列表
|
||||
"""
|
||||
if not lines:
|
||||
return lines
|
||||
|
||||
# 计算总内容高度
|
||||
total_height = sum(line.height for line in lines)
|
||||
|
||||
# 如果没有溢出, 无需处理
|
||||
excess_height = total_height - page_height
|
||||
if excess_height <= 0:
|
||||
return lines
|
||||
|
||||
# 从最后一行开始, 尝试移除行直到不再溢出
|
||||
removed_height = 0.0
|
||||
lines_to_remove = 0
|
||||
|
||||
for i in range(len(lines) - 1, -1, -1):
|
||||
if lines_to_remove >= max_lines_to_remove:
|
||||
break
|
||||
removed_height += lines[i].height
|
||||
lines_to_remove += 1
|
||||
if removed_height >= excess_height:
|
||||
break
|
||||
|
||||
# 移除最后 N 行
|
||||
if lines_to_remove > 0:
|
||||
return lines[:len(lines) - lines_to_remove]
|
||||
|
||||
return lines
|
||||
```
|
||||
|
||||
### 2.4 文本选择 Hit Test 算法
|
||||
|
||||
```python
|
||||
def hit_test_for_character_index(touch_point: tuple,
|
||||
ct_frame_lines: list,
|
||||
view_height: float) -> int:
|
||||
"""
|
||||
将触摸点转换为字符索引。
|
||||
|
||||
等价于 WRPageView 中的 CoreText hit test 逻辑
|
||||
|
||||
坐标系转换:
|
||||
UIKit 坐标系: 原点在左上角, Y 轴向下
|
||||
CoreText 坐标系: 原点在左下角, Y 轴向上
|
||||
|
||||
Args:
|
||||
touch_point: (x, y) UIKit 坐标系下的触摸点
|
||||
ct_frame_lines: CTLine 列表, 每个有 origin 和 string_range
|
||||
view_height: 视图高度 (用于坐标翻转)
|
||||
|
||||
Returns:
|
||||
字符在 NSAttributedString 中的索引, -1 表示未命中
|
||||
"""
|
||||
touch_x, touch_y = touch_point
|
||||
|
||||
# 坐标翻转: UIKit → CoreText
|
||||
ct_y = view_height - touch_y
|
||||
|
||||
for line_info in ct_frame_lines:
|
||||
line_origin = line_info['origin'] # (x, y) CoreText 坐标
|
||||
line_height = line_info['height']
|
||||
line_ascent = line_info['ascent']
|
||||
|
||||
# 检查 Y 坐标是否在本行范围内
|
||||
# CoreText 的 origin 是基线位置
|
||||
line_top = line_origin[1] + line_ascent
|
||||
line_bottom = line_origin[1] - (line_height - line_ascent)
|
||||
|
||||
if line_bottom <= ct_y <= line_top:
|
||||
# 检查 X 坐标
|
||||
line_width = line_info['width']
|
||||
if line_origin[0] <= touch_x <= line_origin[0] + line_width:
|
||||
# CTLineGetStringIndexForPosition 的等价逻辑
|
||||
# 遍历本行的 glyph run, 找到包含触摸点的字符
|
||||
relative_x = touch_x - line_origin[0]
|
||||
char_index = line_info['string_range'][0]
|
||||
|
||||
# 遍历 run 中的每个 glyph, 累计宽度
|
||||
accumulated_width = 0.0
|
||||
for glyph_info in line_info['glyphs']:
|
||||
accumulated_width += glyph_info['advance']
|
||||
if accumulated_width >= relative_x:
|
||||
return char_index
|
||||
char_index += 1
|
||||
|
||||
return line_info['string_range'][0] + line_info['string_range'][1] - 1
|
||||
|
||||
return -1 # 未命中任何行
|
||||
```
|
||||
|
||||
### 2.5 DRM 解密算法
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
def decrypt_chapter(encrypted_data: bytes, book_id: str) -> bytes:
|
||||
"""
|
||||
AES-128-CBC 解密 EPUB 章节文件。
|
||||
|
||||
等价于 WREncryptedFileManager.decryptContentsOfFile:forBookId:isFileLost:
|
||||
|
||||
Args:
|
||||
encrypted_data: 加密的文件数据
|
||||
book_id: 书籍 ID (用于派生密钥)
|
||||
|
||||
Returns:
|
||||
解密后的明文数据 (XHTML)
|
||||
"""
|
||||
# 检查自定义头部标识 "REWD"
|
||||
if encrypted_data[:4] == b'REWD':
|
||||
# 自定义加密格式
|
||||
header_size = int.from_bytes(encrypted_data[4:8], 'little')
|
||||
iv = encrypted_data[8:24] # 16 字节 IV
|
||||
ciphertext = encrypted_data[24 + header_size:]
|
||||
else:
|
||||
# 标准 AES-128-CBC
|
||||
iv = encrypted_data[:16] # 前 16 字节为 IV
|
||||
ciphertext = encrypted_data[16:]
|
||||
|
||||
# 密钥派生: book_id → MD5 → 取前 16 字节
|
||||
key = hashlib.md5(book_id.encode('utf-8')).digest()[:16]
|
||||
|
||||
# AES-128-CBC 解密, PKCS7 填充
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
plaintext = cipher.decrypt(ciphertext)
|
||||
|
||||
# 移除 PKCS7 填充
|
||||
pad_len = plaintext[-1]
|
||||
if 1 <= pad_len <= 16:
|
||||
plaintext = plaintext[:-pad_len]
|
||||
|
||||
return plaintext
|
||||
|
||||
|
||||
def encrypt_for_local_storage(plaintext: bytes, book_id: str,
|
||||
original_key: bytes) -> bytes:
|
||||
"""
|
||||
本地二次加密存储 (DRM 保护)。
|
||||
|
||||
等价于 WREncryptedFileManager.encryptFileForBookId:
|
||||
originalEncryptKey:atPath:toPath:
|
||||
|
||||
防止直接拷贝文件读取。
|
||||
"""
|
||||
# 生成本地存储密钥 (与原始密钥不同)
|
||||
local_key = hashlib.md5((book_id + "local").encode('utf-8')).digest()[:16]
|
||||
|
||||
# 生成随机 IV
|
||||
iv = os.urandom(16)
|
||||
|
||||
# AES-128-CBC 加密
|
||||
# PKCS7 填充
|
||||
pad_len = 16 - (len(plaintext) % 16)
|
||||
plaintext += bytes([pad_len] * pad_len)
|
||||
|
||||
cipher = AES.new(local_key, AES.MODE_CBC, iv)
|
||||
ciphertext = cipher.encrypt(plaintext)
|
||||
|
||||
# 输出: "REWD" 标识 + IV + 密文
|
||||
return b'REWD' + iv + ciphertext
|
||||
```
|
||||
|
||||
### 2.6 Glyph Path 生成算法
|
||||
|
||||
```python
|
||||
def create_glyph_path(ct_run, ct_line_origin: tuple) -> list:
|
||||
"""
|
||||
从 CTRun 提取字形并生成 CGPath。
|
||||
|
||||
等价于 DTCoreTextGlyphRun.newPathWithGlyphs
|
||||
|
||||
Args:
|
||||
ct_run: CTRun 对象
|
||||
ct_line_origin: CTLine 原点 (x, y)
|
||||
|
||||
Returns:
|
||||
list of CGPath 元素 (moveto/lineto/curveto/close)
|
||||
"""
|
||||
# 获取字形信息
|
||||
glyph_count = CTRunGetGlyphCount(ct_run)
|
||||
glyphs = (CGGlyph * glyph_count)()
|
||||
positions = (CGPoint * glyph_count)()
|
||||
|
||||
CTRunGetGlyphs(ct_run, CFRangeMake(0, glyph_count), glyphs)
|
||||
CTRunGetPositions(ct_run, CFRangeMake(0, glyph_count), positions)
|
||||
|
||||
# 获取字体
|
||||
attributes = CTRunGetAttributes(ct_run)
|
||||
ct_font = CFDictionaryGetValue(attributes, kCTFontAttributeName)
|
||||
|
||||
path_elements = []
|
||||
|
||||
for i in range(glyph_count):
|
||||
glyph = glyphs[i]
|
||||
position = positions[i]
|
||||
|
||||
# 转换为绝对坐标 (加上 CTLine 原点)
|
||||
abs_x = ct_line_origin[0] + position.x
|
||||
abs_y = ct_line_origin[1] + position.y
|
||||
|
||||
# CTFontCreatePathForGlyph: 获取单个字形的轮廓路径
|
||||
glyph_path = CTFontCreatePathForGlyph(ct_font, glyph, None)
|
||||
|
||||
if glyph_path:
|
||||
# 将字形路径平移到正确位置
|
||||
transform = CGAffineTransformMakeTranslation(abs_x, abs_y)
|
||||
transformed_path = CGPathCreateMutableCopyByTransformingPath(
|
||||
glyph_path, transform
|
||||
)
|
||||
path_elements.append(transformed_path)
|
||||
|
||||
return path_elements
|
||||
```
|
||||
|
||||
### 2.7 繁简转换算法
|
||||
|
||||
```python
|
||||
def convert_hans_to_hant(attributed_string: str) -> str:
|
||||
"""
|
||||
简体中文 → 繁体中文转换。
|
||||
|
||||
等价于 WRCoreTextLayouter.convertHansToHantWithAttributedString:
|
||||
|
||||
两步法: 简体 → Latin 桥接 → 繁体
|
||||
(避免 OpenCC 等库的直接转换可能丢失格式)
|
||||
|
||||
Args:
|
||||
attributed_string: 包含简体中文的富文本
|
||||
|
||||
Returns:
|
||||
转换为繁体的富文本
|
||||
"""
|
||||
# 步骤1: 使用 ICU 或 OpenCC 进行简→繁映射
|
||||
# ICU: ubrk_open(UBRK_CHARACTER, "zh-Hant", ...)
|
||||
# OpenCC: opencc_convert("s2t", text)
|
||||
|
||||
# 简化实现: 使用 Unicode 码点映射表
|
||||
# 微信读书内部可能使用腾讯自研的繁简转换库
|
||||
conversion_table = load_hans_to_hant_table() # ~8000 个映射
|
||||
|
||||
result = []
|
||||
for char in attributed_string:
|
||||
if char in conversion_table:
|
||||
result.append(conversion_table[char])
|
||||
else:
|
||||
result.append(char)
|
||||
|
||||
return ''.join(result)
|
||||
```
|
||||
|
||||
### 2.8 预加载预测算法
|
||||
|
||||
```python
|
||||
def should_preload_chapters(book_rank: int, archive_rank: int,
|
||||
user_reading_speed: float,
|
||||
current_chapter_idx: int,
|
||||
total_chapters: int) -> list:
|
||||
"""
|
||||
预测需要预加载的章节列表。
|
||||
|
||||
等价于 WRForecastUtils.shouldPreloadChapterUidsForBook:
|
||||
type:bookRank:archiveRank:
|
||||
|
||||
Args:
|
||||
book_rank: 书籍热度排名
|
||||
archive_rank: 用户书架排名
|
||||
user_reading_speed: 用户阅读速度 (章节/小时)
|
||||
current_chapter_idx: 当前章节索引
|
||||
total_chapters: 总章节数
|
||||
|
||||
Returns:
|
||||
list of chapter_uid 需要预加载的章节 UID 列表
|
||||
"""
|
||||
preload_chapters = []
|
||||
|
||||
# 策略1: 总是预加载下一章
|
||||
if current_chapter_idx + 1 < total_chapters:
|
||||
preload_chapters.append(current_chapter_idx + 1)
|
||||
|
||||
# 策略2: 热门书籍预加载更多 (前 1000 名)
|
||||
if book_rank < 1000:
|
||||
# 预加载后 3 章
|
||||
for i in range(2, 4):
|
||||
if current_chapter_idx + i < total_chapters:
|
||||
preload_chapters.append(current_chapter_idx + i)
|
||||
|
||||
# 策略3: 用户书架排名靠前的书预加载更多
|
||||
if archive_rank < 10:
|
||||
# 预加载后 5 章
|
||||
for i in range(1, 6):
|
||||
if current_chapter_idx + i < total_chapters:
|
||||
preload_chapters.append(current_chapter_idx + i)
|
||||
|
||||
# 策略4: 快速阅读者预加载更多
|
||||
if user_reading_speed > 5.0: # 每小时 >5 章
|
||||
for i in range(1, 8):
|
||||
if current_chapter_idx + i < total_chapters:
|
||||
preload_chapters.append(current_chapter_idx + i)
|
||||
|
||||
# 去重并排序
|
||||
return sorted(set(preload_chapters))
|
||||
```
|
||||
|
||||
### 2.9 分页范围计算 (二分查找)
|
||||
|
||||
```python
|
||||
def page_index_for_character_index(char_index: int,
|
||||
page_ranges: list) -> int:
|
||||
"""
|
||||
二分查找字符索引所在的页码。
|
||||
|
||||
等价于 WRChapterPageCount 中的页码查找逻辑
|
||||
|
||||
Args:
|
||||
char_index: 字符在 NSAttributedString 中的索引
|
||||
page_ranges: [(start, length), ...] 每页的 NSRange
|
||||
|
||||
Returns:
|
||||
页码 (0-based), -1 表示未找到
|
||||
"""
|
||||
if not page_ranges:
|
||||
return -1
|
||||
|
||||
left, right = 0, len(page_ranges) - 1
|
||||
|
||||
while left <= right:
|
||||
mid = (left + right) // 2
|
||||
start, length = page_ranges[mid]
|
||||
end = start + length
|
||||
|
||||
if char_index < start:
|
||||
right = mid - 1
|
||||
elif char_index >= end:
|
||||
left = mid + 1
|
||||
else:
|
||||
return mid
|
||||
|
||||
# 边界情况: 在最后一页之后
|
||||
return len(page_ranges) - 1
|
||||
```
|
||||
|
||||
### 2.10 翻页控制器故障检测
|
||||
|
||||
```python
|
||||
def detect_navigation_direction_crash(page_view_controller,
|
||||
queuing_scroll_view,
|
||||
is_animated: bool) -> bool:
|
||||
"""
|
||||
检测 UIPageViewController 的 QueuingScrollView 导航方向崩溃。
|
||||
|
||||
等价于 WRPageViewController.detectNavigationDirectionCrashWithPageViewController:
|
||||
queuingScrollView:inMethodDidScrollWithAnimation:force:logDetail:logCrashReason:
|
||||
|
||||
已知 bug: QueuingScrollView 在快速连续翻页时可能崩溃
|
||||
原因: 内部 child view controller 管理状态不一致
|
||||
|
||||
Returns:
|
||||
True 表示检测到即将崩溃, 需要修复
|
||||
"""
|
||||
# 检查1: scroll view content offset 是否越界
|
||||
content_offset = queuing_scroll_view.contentOffset
|
||||
content_size = queuing_scroll_view.contentSize
|
||||
bounds_size = queuing_scroll_view.bounds.size
|
||||
|
||||
if content_offset.x < -bounds_size.width:
|
||||
return True # 向左越界
|
||||
if content_offset.x > content_size.width:
|
||||
return True # 向右越界
|
||||
|
||||
# 检查2: child view controller 数量是否异常
|
||||
child_vcs = page_view_controller.childViewControllers
|
||||
if len(child_vcs) > 3:
|
||||
return True # UIPageViewController 最多应该有 3 个 child
|
||||
|
||||
# 检查3: 当前 page 是否为空
|
||||
if page_view_controller.viewControllers is None:
|
||||
return True
|
||||
|
||||
# 检查4: delegate 一致性
|
||||
if page_view_controller.delegate is None:
|
||||
return True
|
||||
|
||||
return False
|
||||
```
|
||||
@@ -0,0 +1,838 @@
|
||||
# 微信读书 EPUB 阅读器 — 数据结构与 API 协议定义
|
||||
|
||||
> 基于 WeRead v10.0.3 (Build 79) 逆向工程
|
||||
> 数据结构来源: __objc_methtype + class_ro_t ivar 段
|
||||
> API 来源: WRBookNetwork 44 个 class method 签名
|
||||
|
||||
---
|
||||
|
||||
## 一、关键数据结构定义 (Header Files)
|
||||
|
||||
### 1.1 EPUB 解析数据结构
|
||||
|
||||
```objc
|
||||
// ============================================================
|
||||
#pragma mark - WREpubParser (EPUB 文件解析器)
|
||||
// ============================================================
|
||||
|
||||
@interface WREpubParser : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *epubFilePath; // EPUB 文件路径
|
||||
@property (nonatomic, copy) NSString *opfFilePath; // content.opf 路径
|
||||
@property (nonatomic, strong) NSError *lastError; // 最近错误
|
||||
@property (nonatomic, weak) UIViewController *hostVC; // 宿主控制器
|
||||
@property (nonatomic, strong) WRBook *book; // 书籍模型
|
||||
@property (nonatomic, strong) WHAlbumInfo *albumInfo; // 专辑信息
|
||||
|
||||
// 解析 EPUB 结构
|
||||
- (BOOL)parseEpubAtPath:(NSString *)path error:(NSError **)error;
|
||||
|
||||
// 获取章节目录 (NCX)
|
||||
- (NSArray<NSDictionary *> *)tableOfContents;
|
||||
|
||||
// 获取资源文件映射 (manifest)
|
||||
- (NSDictionary<NSString *, NSString *> *)resourceManifest;
|
||||
|
||||
// 获取 XHTML 章节文件路径
|
||||
- (NSString *)filePathForChapterWithId:(NSString *)chapterId;
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRBook (书籍模型)
|
||||
// ============================================================
|
||||
|
||||
@interface WRBook : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *bookId; // 书籍 ID
|
||||
@property (nonatomic, copy) NSString *title; // 书名
|
||||
@property (nonatomic, copy) NSString *author; // 作者
|
||||
@property (nonatomic, copy) NSString *cover; // 封面 URL
|
||||
@property (nonatomic, assign) WRBookFormat format; // epub/pdf/mp
|
||||
@property (nonatomic, strong) NSArray<WRChapter *> *chapters; // 章节列表
|
||||
@property (nonatomic, copy) NSString *encryptKey; // 加密密钥
|
||||
@property (nonatomic, assign) BOOL isFinished; // 是否读完
|
||||
@property (nonatomic, assign) BOOL isVIP; // 是否 VIP 书籍
|
||||
|
||||
typedef NS_ENUM(NSInteger, WRBookFormat) {
|
||||
WRBookFormatEpub = 0,
|
||||
WRBookFormatPDF = 1,
|
||||
WRBookFormatMP = 2, // 公众号文章
|
||||
};
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRChapter (章节模型)
|
||||
// ============================================================
|
||||
|
||||
@interface WRChapter : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *chapterUid; // 章节唯一 ID
|
||||
@property (nonatomic, assign) NSInteger chapterIdx; // 章节索引
|
||||
@property (nonatomic, copy) NSString *title; // 章节标题
|
||||
@property (nonatomic, assign) WRChapterFormat format; // 章节格式
|
||||
@property (nonatomic, copy) NSString *fileId; // 文件 ID
|
||||
@property (nonatomic, assign) NSUInteger filePosition; // 文件位置
|
||||
@property (nonatomic, assign) NSUInteger fileSize; // 文件大小
|
||||
@property (nonatomic, assign) BOOL isPaid; // 是否付费章节
|
||||
@property (nonatomic, assign) BOOL isAvailable; // 是否可用
|
||||
|
||||
typedef NS_ENUM(NSInteger, WRChapterFormat) {
|
||||
WRChapterFormatXHTML = 0,
|
||||
WRChapterFormatHTML = 1,
|
||||
WRChapterFormatTXT = 2,
|
||||
};
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 1.2 排版数据结构
|
||||
|
||||
```objc
|
||||
// ============================================================
|
||||
#pragma mark - DTHTMLElement (HTML 元素模型, 38 ivars)
|
||||
// ============================================================
|
||||
|
||||
@interface DTHTMLElement : NSObject
|
||||
|
||||
// 树结构
|
||||
@property (nonatomic, weak) DTHTMLElement *parent; // 父元素
|
||||
@property (nonatomic, strong) NSMutableArray<DTHTMLElement *> *children; // 子元素
|
||||
@property (nonatomic, copy) NSString *tagName; // 标签名 (p, h1, img...)
|
||||
@property (nonatomic, copy) NSString *elementId; // id 属性
|
||||
@property (nonatomic, copy) NSString *className; // class 属性
|
||||
|
||||
// 样式
|
||||
@property (nonatomic, strong) DTCoreTextFontDescriptor *fontDescriptor; // 字体
|
||||
@property (nonatomic, strong) DTCoreTextParagraphStyle *paragraphStyle; // 段落
|
||||
@property (nonatomic, strong) UIColor *textColor; // 文字颜色
|
||||
@property (nonatomic, strong) UIColor *backgroundColor; // 背景色
|
||||
@property (nonatomic, assign) CGFloat textScale; // 文字缩放
|
||||
@property (nonatomic, assign) CGFloat letterSpacing; // 字间距
|
||||
|
||||
// 附件
|
||||
@property (nonatomic, strong) DTTextAttachment *textAttachment; // 附件 (图片/视频)
|
||||
|
||||
// 链接
|
||||
@property (nonatomic, strong) NSURL *linkURL; // 超链接地址
|
||||
|
||||
// CSS 样式字典
|
||||
@property (nonatomic, strong) NSDictionary *cssStyles; // 应用的 CSS 样式
|
||||
|
||||
// 边框与背景
|
||||
@property (nonatomic, assign) DTBorderStyle borderTop; // 上边框
|
||||
@property (nonatomic, assign) DTBorderStyle borderBottom; // 下边框
|
||||
@property (nonatomic, strong) UIColor *borderColor; // 边框颜色
|
||||
@property (nonatomic, strong) DTBackgroundImageStyle *backgroundImage; // 背景图
|
||||
|
||||
// 表格
|
||||
@property (nonatomic, strong) DTTableStyle *tableStyle; // 表格样式
|
||||
|
||||
// 微信读书自定义属性
|
||||
@property (nonatomic, assign) NSInteger verticalCenterStyle; // wr-vertical-center-style
|
||||
@property (nonatomic, assign) BOOL pageRelate; // weread-page-relate
|
||||
@property (nonatomic, assign) BOOL avoidPageBreakInside; // 断页保护
|
||||
@property (nonatomic, assign) BOOL pageBreakAfter; // 元素后分页
|
||||
@property (nonatomic, assign) BOOL pageBreakBefore; // 元素前分页
|
||||
@property (nonatomic, strong) UIColor *pageBackgroundColor; // 页面背景色
|
||||
@property (nonatomic, strong) NSURL *pageBackgroundImage; // 页面背景图
|
||||
|
||||
// 翻译
|
||||
@property (nonatomic, assign) BOOL isTranslateTag; // 翻译标签
|
||||
@property (nonatomic, assign) BOOL isTranslateNoStyle; // 无翻译样式
|
||||
|
||||
// 核心方法
|
||||
- (void)applyStyleDictionary:(NSDictionary *)styles
|
||||
isLatinLanguageBook:(BOOL)isLatin;
|
||||
- (NSAttributedString *)attributedString;
|
||||
- (void)interpretAttributes;
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - DTCoreTextFontDescriptor (字体描述)
|
||||
// ============================================================
|
||||
|
||||
@interface DTCoreTextFontDescriptor : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *fontFamily; // 字体族 (如 "Source Han Serif CN")
|
||||
@property (nonatomic, copy) NSString *fontName; // 字体名 (如 "SourceHanSerifCN-Medium")
|
||||
@property (nonatomic, assign) CGFloat pointSize; // 字号 (pt)
|
||||
@property (nonatomic, assign) BOOL bold; // 粗体
|
||||
@property (nonatomic, assign) BOOL italic; // 斜体
|
||||
@property (nonatomic, assign) uint32_t symbolicTraits; // 符号特征
|
||||
|
||||
// 匹配系统字体
|
||||
- (CTFontRef)matchedFontDescriptor;
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - DTCoreTextParagraphStyle (段落样式)
|
||||
// ============================================================
|
||||
|
||||
@interface DTCoreTextParagraphStyle : NSObject
|
||||
|
||||
@property (nonatomic, assign) CTTextAlignment alignment; // 对齐方式
|
||||
@property (nonatomic, assign) CGFloat lineSpacing; // 行间距
|
||||
@property (nonatomic, assign) CGFloat paragraphSpacing; // 段间距
|
||||
@property (nonatomic, assign) CGFloat paragraphSpacingBefore; // 段前间距
|
||||
@property (nonatomic, assign) CGFloat firstLineHeadIndent; // 首行缩进
|
||||
@property (nonatomic, assign) CGFloat headIndent; // 左缩进
|
||||
@property (nonatomic, assign) CGFloat tailIndent; // 右缩进
|
||||
@property (nonatomic, assign) CGFloat minimumLineHeight; // 最小行高
|
||||
@property (nonatomic, assign) CGFloat maximumLineHeight; // 最大行高
|
||||
@property (nonatomic, assign) CGFloat lineHeightMultiple; // 行高倍数
|
||||
|
||||
// 微信读书扩展
|
||||
@property (nonatomic, assign) CGFloat defaultTabInterval; // 默认制表位
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - DTTextAttachment (文本附件)
|
||||
// ============================================================
|
||||
|
||||
@interface DTTextAttachment : NSObject
|
||||
|
||||
@property (nonatomic, assign) DTTextAttachmentType contentType; // 附件类型
|
||||
@property (nonatomic, strong) NSData *contents; // 内容数据
|
||||
@property (nonatomic, strong) NSURL *contentURL; // 内容 URL
|
||||
@property (nonatomic, assign) CGSize displaySize; // 显示尺寸
|
||||
@property (nonatomic, assign) CGSize originalSize; // 原始尺寸
|
||||
@property (nonatomic, assign) CGFloat verticalAlignment; // 垂直对齐
|
||||
|
||||
typedef NS_ENUM(NSInteger, DTTextAttachmentType) {
|
||||
DTTextAttachmentTypeImage = 0,
|
||||
DTTextAttachmentTypeVideo = 1,
|
||||
DTTextAttachmentTypeIframe = 2,
|
||||
DTTextAttachmentTypeObject = 3,
|
||||
};
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - DTCSSStylesheet (CSS 样式表)
|
||||
// ============================================================
|
||||
|
||||
@interface DTCSSStylesheet : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSDictionary<NSString *, NSDictionary *> *rules;
|
||||
// rules 格式: { "selector": { "property": "value", ... }, ... }
|
||||
|
||||
// 从 CSS 文本解析
|
||||
+ (DTCSSStylesheet *)styleSheetWithCSSString:(NSString *)cssString;
|
||||
|
||||
// 合并另一个样式表 (后者覆盖前者)
|
||||
- (void)mergeStylesheet:(DTCSSStylesheet *)other;
|
||||
|
||||
// 获取匹配选择器的样式
|
||||
- (NSDictionary *)stylesForElement:(DTHTMLElement *)element;
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 1.3 渲染数据结构
|
||||
|
||||
```objc
|
||||
// ============================================================
|
||||
#pragma mark - WRChapterData (章节数据模型, 12 ivars)
|
||||
// ============================================================
|
||||
|
||||
@interface WRChapterData : NSObject
|
||||
|
||||
// 排版结果
|
||||
@property (nonatomic, strong) NSMutableAttributedString *attributedString; // 排版后富文本
|
||||
@property (nonatomic, strong) WRCoreTextLayouter *layouter; // 排版器
|
||||
|
||||
// 分页
|
||||
@property (nonatomic, strong) NSArray<NSValue *> *pageRanges; // 每页 NSRange
|
||||
|
||||
// 标注
|
||||
@property (nonatomic, strong) NSArray<WRPageHighlight *> *highlights; // 高亮列表
|
||||
@property (nonatomic, strong) NSArray<WRPageUnderline *> *underlines; // 下划线列表
|
||||
@property (nonatomic, strong) NSArray<WRPageMark *> *marks; // 书签标注
|
||||
@property (nonatomic, strong) NSArray<NSDictionary *> *reviews; // 书评列表
|
||||
@property (nonatomic, strong) NSArray *tempHighlights; // 临时高亮
|
||||
@property (nonatomic, strong) NSArray *tempUnderlines; // 临时下划线
|
||||
|
||||
// 目录与搜索
|
||||
@property (nonatomic, strong) NSArray<NSDictionary *> *outlineContents; // 目录
|
||||
@property (nonatomic, strong) NSArray<NSDictionary *> *searchResults; // 搜索结果
|
||||
|
||||
// 书签
|
||||
@property (nonatomic, strong) NSSet<NSString *> *bookmarkSet; // 书签集合
|
||||
|
||||
// 原始数据
|
||||
@property (nonatomic, strong) NSAttributedString *originalString; // 原始富文本
|
||||
|
||||
// 核心方法
|
||||
- (NSRange)rangeOfPage:(NSInteger)pageIndex;
|
||||
- (void)addHighlightInRange:(NSRange)range key:(NSString *)key
|
||||
itemId:(NSString *)itemId color:(UIColor *)color;
|
||||
- (void)addReviewUnderlineInRange:(NSRange)range itemId:(NSString *)itemId
|
||||
type:(WRReviewType)type;
|
||||
- (void)deleteReviewUnderlineInRange:(NSRange)range type:(WRReviewType)type;
|
||||
- (NSArray<NSDictionary *> *)generateOutlineContents;
|
||||
- (NSInteger)freeTrialChapterCutOffRealStringLocation;
|
||||
|
||||
+ (void)addUnderLineToAttributedString:(NSMutableAttributedString *)attrStr
|
||||
range:(NSRange)range
|
||||
itemId:(NSString *)itemId
|
||||
style:(WRUnderlineStyle)style
|
||||
color:(UIColor *)color;
|
||||
|
||||
+ (NSInteger)freeTrialChapterCutOffStringLocaionWithAttributedString:
|
||||
(NSAttributedString *)attrStr book:(WRBook *)book;
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRChapterPageCount (分页计算, 4 ivars)
|
||||
// ============================================================
|
||||
|
||||
@interface WRChapterPageCount : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *bookId; // 书籍 ID
|
||||
@property (nonatomic, copy) NSString *chapterUid; // 章节 ID
|
||||
@property (nonatomic, copy) NSString *cacheKey; // 缓存键
|
||||
@property (nonatomic, strong) NSArray<NSValue *> *pageRanges; // 页范围
|
||||
|
||||
+ (NSString *)currentCacheKeyWithBookId:(NSString *)bookId;
|
||||
+ (NSValue *)rangeValueWithPageInfo:(NSDictionary *)pageInfo;
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 1.4 标注数据结构
|
||||
|
||||
```objc
|
||||
// ============================================================
|
||||
#pragma mark - WRPageHighlight (页面高亮)
|
||||
// ============================================================
|
||||
|
||||
@interface WRPageHighlight : NSObject
|
||||
|
||||
@property (nonatomic, assign) NSRange range; // 高亮范围
|
||||
@property (nonatomic, copy) NSString *highlightKey; // 高亮唯一键
|
||||
@property (nonatomic, copy) NSString *itemId; // 标注 ID
|
||||
@property (nonatomic, strong) UIColor *color; // 高亮颜色
|
||||
@property (nonatomic, assign) BOOL isTemporary; // 是否临时
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRPageUnderline (页面下划线)
|
||||
// ============================================================
|
||||
|
||||
@interface WRPageUnderline : NSObject
|
||||
|
||||
@property (nonatomic, assign) NSRange range; // 下划线范围
|
||||
@property (nonatomic, copy) NSString *itemId; // 标注 ID
|
||||
@property (nonatomic, assign) WRUnderlineStyle style; // 下划线样式
|
||||
@property (nonatomic, strong) UIColor *color; // 颜色
|
||||
|
||||
typedef NS_ENUM(NSInteger, WRUnderlineStyle) {
|
||||
WRUnderlineStyleSolid = 0, // 实线
|
||||
WRUnderlineStyleDashed = 1, // 虚线
|
||||
WRUnderlineStyleWavy = 2, // 波浪线
|
||||
WRUnderlineStyleDotted = 3, // 点线
|
||||
};
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRPageMark (页面标注, 25 methods)
|
||||
// ============================================================
|
||||
|
||||
@interface WRPageMark : NSObject
|
||||
|
||||
@property (nonatomic, assign) NSRange range; // 标注范围
|
||||
@property (nonatomic, assign) WRMarkType type; // 标注类型
|
||||
@property (nonatomic, copy) NSString *content; // 标注内容
|
||||
@property (nonatomic, copy) NSString *itemId; // 标注 ID
|
||||
@property (nonatomic, strong) NSDate *createTime; // 创建时间
|
||||
@property (nonatomic, strong) UIColor *color; // 颜色
|
||||
|
||||
typedef NS_ENUM(NSInteger, WRMarkType) {
|
||||
WRMarkTypeHighlight = 0, // 高亮
|
||||
WRMarkTypeUnderline = 1, // 下划线
|
||||
WRMarkTypeBookmark = 2, // 书签
|
||||
WRMarkTypeNote = 3, // 笔记
|
||||
WRMarkTypeReview = 4, // 书评
|
||||
};
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRBookmark (书签模型, 25 ivars)
|
||||
// ============================================================
|
||||
|
||||
@interface WRBookmark : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *bookId; // 书籍 ID
|
||||
@property (nonatomic, copy) NSString *chapterUid; // 章节 ID
|
||||
@property (nonatomic, copy) NSString *itemId; // 书签 ID
|
||||
@property (nonatomic, assign) NSUInteger rangeLocation; // 范围起始
|
||||
@property (nonatomic, assign) NSUInteger rangeLength; // 范围长度
|
||||
@property (nonatomic, copy) NSString *abstractText; // 摘要文本
|
||||
@property (nonatomic, assign) WRBookmarkType type; // 书签类型
|
||||
@property (nonatomic, strong) UIColor *color; // 颜色
|
||||
@property (nonatomic, assign) WRUnderlineStyle style; // 下划线样式
|
||||
@property (nonatomic, copy) NSString *reviewId; // 关联书评 ID
|
||||
@property (nonatomic, strong) NSDate *createTime; // 创建时间
|
||||
@property (nonatomic, strong) NSDate *updateTime; // 更新时间
|
||||
|
||||
typedef NS_ENUM(NSInteger, WRBookmarkType) {
|
||||
WRBookmarkTypeHighlight = 0,
|
||||
WRBookmarkTypeUnderline = 1,
|
||||
WRBookmarkTypeBookmark = 2,
|
||||
WRBookmarkTypeNote = 3,
|
||||
WRBookmarkTypeReview = 4,
|
||||
};
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### 1.5 附件数据结构
|
||||
|
||||
```objc
|
||||
// ============================================================
|
||||
#pragma mark - WRPageImageAttachment (图片附件)
|
||||
// ============================================================
|
||||
|
||||
@interface WRPageImageAttachment : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *imageURL; // 图片 URL
|
||||
@property (nonatomic, strong) UIImage *image; // 图片对象
|
||||
@property (nonatomic, assign) CGSize displaySize; // 显示尺寸
|
||||
@property (nonatomic, assign) CGSize originalSize; // 原始尺寸
|
||||
@property (nonatomic, assign) NSInteger position; // 在文本中的位置
|
||||
@property (nonatomic, assign) NSInteger verticalCenterStyle; // 居中方式
|
||||
@property (nonatomic, assign) BOOL hasWhiteBackground; // 是否白底
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRPageHyperlinksAttachment (超链接附件)
|
||||
// ============================================================
|
||||
|
||||
@interface WRPageHyperlinksAttachment : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSURL *url; // 链接 URL
|
||||
@property (nonatomic, assign) NSRange range; // 文本范围
|
||||
@property (nonatomic, copy) NSString *displayText; // 显示文本
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRPageChapterToolAttachment (章节工具附件)
|
||||
// ============================================================
|
||||
|
||||
@interface WRPageChapterToolAttachment : NSObject
|
||||
|
||||
@property (nonatomic, assign) WRChapterToolType type; // 工具类型
|
||||
@property (nonatomic, strong) NSDictionary *data; // 工具数据
|
||||
|
||||
typedef NS_ENUM(NSInteger, WRChapterToolType) {
|
||||
WRChapterToolTypeShare = 0, // 分享
|
||||
WRChapterToolTypeReview = 1, // 书评
|
||||
WRChapterToolTypeBookmark = 2, // 书签
|
||||
WRChapterToolTypeNext = 3, // 下一章
|
||||
};
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRPageFlyleafAttachment (扉页附件)
|
||||
// ============================================================
|
||||
|
||||
@interface WRPageFlyleafAttachment : NSObject
|
||||
|
||||
@property (nonatomic, assign) WRFlyleafType type; // 扉页类型
|
||||
@property (nonatomic, strong) NSDictionary *data; // 扉页数据
|
||||
|
||||
typedef NS_ENUM(NSInteger, WRFlyleafType) {
|
||||
WRFlyleafTypeCover = 0, // 封面
|
||||
WRFlyleafTypeTitle = 1, // 标题页
|
||||
WRFlyleafTypeAuthor = 2, // 作者页
|
||||
WRFlyleafTypeIntro = 3, // 简介页
|
||||
WRFlyleafTypeCatalog = 4, // 目录页
|
||||
};
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRPageCodeAttachment (代码块附件)
|
||||
// ============================================================
|
||||
|
||||
@interface WRPageCodeAttachment : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *codeString; // 代码内容
|
||||
@property (nonatomic, copy) NSString *language; // 编程语言
|
||||
@property (nonatomic, assign) NSInteger lineNumber; // 起始行号
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================
|
||||
#pragma mark - WRPageTableAttachment (表格附件)
|
||||
// ============================================================
|
||||
|
||||
@interface WRPageTableAttachment : NSObject
|
||||
|
||||
@property (nonatomic, assign) NSInteger rows; // 行数
|
||||
@property (nonatomic, assign) NSInteger columns; // 列数
|
||||
@property (nonatomic, strong) NSArray<NSArray<NSString *> *> *cells; // 单元格数据
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、API 与网络协议控制矩阵
|
||||
|
||||
### 2.1 书籍信息 API
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 方法签名 推断端点 HTTP 方法 │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ +[WRBookNetwork loadBookInfoWithBookId: /api/book/info GET │
|
||||
│ source:callback:] ?bookId=&source= │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadBookReadInfoWithBookId: /api/book/readInfo GET │
|
||||
│ callback:] ?bookId= │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadBookReadInfoWithBookIdAndVid: /api/book/readInfo GET │
|
||||
│ vid:callback:] ?bookId=&vid= │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadBookReadDetailInfoWithBookId: /api/book/readDetail GET │
|
||||
│ callback:] ?bookId= │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadBookLectureAuthors: /api/book/authors GET │
|
||||
│ callback:] ?bookId= │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadArticleBookDetailWithBookId: /api/article/detail GET │
|
||||
│ callback:] ?bookId= │
|
||||
│ │
|
||||
│ +[WRBookNetwork fetchLockInfoWithBookId: /api/book/lockInfo GET │
|
||||
│ callback:] ?bookId= │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
响应格式 (推断):
|
||||
{
|
||||
"bookId": "string",
|
||||
"title": "string",
|
||||
"author": "string",
|
||||
"cover": "string (URL)",
|
||||
"format": 0,
|
||||
"chapters": [
|
||||
{
|
||||
"chapterUid": "string",
|
||||
"chapterIdx": 0,
|
||||
"title": "string",
|
||||
"fileId": "string",
|
||||
"filePosition": 0,
|
||||
"fileSize": 0,
|
||||
"isPaid": false
|
||||
}
|
||||
],
|
||||
"encryptKey": "string",
|
||||
"isFinished": false,
|
||||
"isVIP": false
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 章节内容 API
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 方法签名 推断端点 HTTP 方法 │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ +[WRBookNetwork loadTarForEpubBookId:chapter: /api/book/tar GET │
|
||||
│ isPreload:] ?bookId=&chapterUid= │
|
||||
│ &isPreload= │
|
||||
│ 响应: 加密 ZIP 文件 (application/octet-stream) │
|
||||
│ 文件名: {bookId}_DECRYPT.zip │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadTarForEpubBookId:chapters: /api/book/tar GET │
|
||||
│ isPreload:] ?bookId=&chapterUids= │
|
||||
│ &isPreload= │
|
||||
│ 批量下载: chapters 逗号分隔 │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadChapterContentWithParam: /api/book/chapter GET │
|
||||
│ callback:] ?bookId=&chapterUid= │
|
||||
│ 参数: NSDictionary (bookId, chapterUid, format) │
|
||||
│ │
|
||||
│ +[WRBookNetwork fileContentWithChapter:book: 本地文件读取 - │
|
||||
│ shouldRemoveHtmlTags:filterTranslateContent:] 从 plainBookDirectory 读取 │
|
||||
│ │
|
||||
│ +[WRBookNetwork chaptersInfoFromFile: 本地文件解析 - │
|
||||
│ checkTranslate:] 解析 OPF/NCX │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 预加载管理 API
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 方法签名 推断端点 HTTP 方法 │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ +[WRBookNetwork savePreloadInfoWithDownloadParam: 本地存储 - │
|
||||
│ chaptersStr:timeFlag:tmpFilePath:encryptKey:] NSUserDefaults │
|
||||
│ │
|
||||
│ +[WRBookNetwork clearPreloadKVWithBookId: 本地清理 - │
|
||||
│ chapterUid:zipPath:] NSUserDefaults │
|
||||
│ │
|
||||
│ +[WRBookNetwork storeTarEpubImageToDiskWith 本地存储 - │
|
||||
│ BookId:chapterUid:untarDirectory:] epubImage/ 目录 │
|
||||
│ │
|
||||
│ +[WRBookNetwork processEncryptedBookFileAtPath: 本地处理 - │
|
||||
│ encryptKey:book:chapterUid:isFromReview:] 解密 + 存储 │
|
||||
│ │
|
||||
│ +[WRBookNetwork processChapterInfosFrom 本地处理 - │
|
||||
│ ChapterDownload:bookId:] 解析章节信息 │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.4 书签/标注 API
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 方法签名 推断端点 HTTP 方法 │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ +[WRBookNetwork loadBookmarkListWithBookId: /api/book/bookmarks GET │
|
||||
│ syncKey:callback:] ?bookId=&syncKey= │
|
||||
│ │
|
||||
│ +[WRBookNetwork addReview:shareToWechat: /api/review/add POST │
|
||||
│ audioArticleId:outlineContent: body: {bookId, chapterUid, │
|
||||
│ audioColumnId:callback:] content, range, type, ...} │
|
||||
│ │
|
||||
│ +[WRBookNetwork likeReviewById:isUnlike: /api/review/like POST │
|
||||
│ withParams:callback:] body: {reviewId, isUnlike} │
|
||||
│ │
|
||||
│ +[WRBookNetwork dislikeReviewById:isDislike: /api/review/dislike POST │
|
||||
│ withParams:callback:] body: {reviewId, isDislike} │
|
||||
│ │
|
||||
│ +[WRBookNetwork repostReview:reposted: /api/review/repost POST │
|
||||
│ callback:] body: {reviewId, reposted} │
|
||||
│ │
|
||||
│ +[WRBookNetwork rewardReviewForId:price: /api/review/reward POST │
|
||||
│ timestamp:callback:] body: {reviewId, price, ts} │
|
||||
│ │
|
||||
│ +[WRBookNetwork postReviewHideWithBookId: /api/review/hide POST │
|
||||
│ hide:callback:] body: {bookId, hide} │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadTopicReviewlist: /api/review/topic GET │
|
||||
│ callback:] ?topicId= │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadRelatedBooksForReviewDetail: /api/review/related GET │
|
||||
│ callback:] ?reviewId= │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.5 阅读进度 API
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 方法签名 推断端点 HTTP 方法 │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ +[WRBookNetwork uploadBookProgressAndReadingTime: /api/book/progress POST │
|
||||
│ callback:offlineCallback:] body: {bookId, chapterUid, │
|
||||
│ progress, readingTime, ...} │
|
||||
│ │
|
||||
│ +[WRBookNetwork setFinishReading:withBookId: /api/book/finish POST │
|
||||
│ callback:] body: {bookId, isFinished} │
|
||||
│ │
|
||||
│ +[WRBookNetwork setIsStartReading:withBookId: /api/book/start POST │
|
||||
│ callback:] body: {bookId} │
|
||||
│ │
|
||||
│ +[WRBookNetwork markReadingStatus:bookIds: /api/book/status POST │
|
||||
│ isCancel:callback:] body: {bookIds[], isCancel} │
|
||||
│ │
|
||||
│ +[WRBookNetwork markReadingStatus:withBookId: /api/book/status POST │
|
||||
│ isCancel:withFinishInfo:callback:] body: {bookId, isCancel, │
|
||||
│ finishInfo} │
|
||||
│ │
|
||||
│ +[WRBookNetwork automaticallyMarkFinishReading /api/book/autoFinish POST │
|
||||
│ WithBookId:callback:] body: {bookId} │
|
||||
│ │
|
||||
│ +[WRBookNetwork addMileStone:callback:] /api/book/milestone POST │
|
||||
│ body: {bookId, milestone} │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.6 搜索 API
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 方法签名 推断端点 HTTP 方法 │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ +[WRBookNetwork searchResultsForBook: /api/search GET │
|
||||
│ chapterUid:searchString:posBeg: ?bookId=&chapterUid= │
|
||||
│ posEnd:mode:callback:] &keyword=&posBeg= │
|
||||
│ &posEnd=&mode= │
|
||||
│ mode: 0=精确 1=模糊 2=正则 │
|
||||
│ │
|
||||
│ +[WRBookNetwork searchResultsForLocalBook: 本地搜索 - │
|
||||
│ chapterUid:searchString:posBeg: 遍历 NSAttributedString │
|
||||
│ posEnd:mode:callback:] │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.7 翻译 API
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 方法签名 推断端点 HTTP 方法 │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ +[WRBookNetwork pollingChapterTranslateStatus /api/book/translate GET │
|
||||
│ WithBookId:isFreeTrialActive: ?bookId=&chapterUid= │
|
||||
│ referenceLocationDict:chapterTranslations: &isFreeTrial= │
|
||||
│ from:] │
|
||||
│ │
|
||||
│ +[WRBookNetwork _removeTranslateHtml:] 本地处理 - │
|
||||
│ 移除翻译 HTML 标签 │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.8 付费/会员 API
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 方法签名 推断端点 HTTP 方法 │
|
||||
├────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ +[WRBookNetwork fetchLockInfoWithBookId: /api/book/lock GET │
|
||||
│ callback:] ?bookId= │
|
||||
│ │
|
||||
│ +[WRBookNetwork resetChapterPaidIfNeeded /api/book/resetPaid POST │
|
||||
│ WithBookId:chapterUid:] body: {bookId, chapterUid} │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadReadTimeWelfareAction /api/welfare/readTime GET │
|
||||
│ WithBookId:opt:secretKey:firstEnter:] ?bookId=&opt=&secretKey= │
|
||||
│ │
|
||||
│ +[WRBookNetwork checkFMCards:callback:] /api/fm/cards GET │
|
||||
│ │
|
||||
│ +[WRBookNetwork loadFMCardsWithBookId: /api/fm/cards GET │
|
||||
│ withSynckey:withListType:withFilterType: ?bookId=&synckey= │
|
||||
│ withMaxIdx:withCount:] &listType=&filterType= │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、本地存储 Schema
|
||||
|
||||
### 3.1 文件系统布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ App 沙箱 │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Documents/ │
|
||||
│ ├── {bookId}/ │
|
||||
│ │ ├── plainBookDirectory/ ← 解密后的 EPUB XHTML 文件 │
|
||||
│ │ │ ├── META-INF/container.xml │
|
||||
│ │ │ ├── OEBPS/content.opf │
|
||||
│ │ │ ├── OEBPS/toc.ncx │
|
||||
│ │ │ ├── OEBPS/Text/chapter1.xhtml │
|
||||
│ │ │ ├── OEBPS/Text/chapter2.xhtml │
|
||||
│ │ │ └── ... │
|
||||
│ │ ├── epubImage/ ← 书籍图片缓存 │
|
||||
│ │ │ ├── image1.jpg │
|
||||
│ │ │ ├── image2.png │
|
||||
│ │ │ └── ... │
|
||||
│ │ └── {bookId}_DECRYPT.zip ← 下载的加密 ZIP (可能已删除) │
|
||||
│ │ │
|
||||
│ └── ... (其他书籍) │
|
||||
│ │
|
||||
│ Library/ │
|
||||
│ ├── {cachePath}/ │
|
||||
│ │ └── epubImage/ ← SDWebImage 缓存 │
|
||||
│ │ └── com.hackemist.SDWebImageCache.epubImage/ │
|
||||
│ ├── Caches/ │
|
||||
│ │ └── chapterCache/ ← 章节排版缓存 │
|
||||
│ └── Preferences/ │
|
||||
│ └── com.tencent.weread.plist ← NSUserDefaults │
|
||||
│ │
|
||||
│ tmp/ │
|
||||
│ └── {临时解压目录} ← 下载解压临时目录 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 NSUserDefaults Keys
|
||||
|
||||
```objc
|
||||
// 阅读进度
|
||||
NSString *const kWRReadingProgressKey = @"WRReadingProgress_{bookId}";
|
||||
// 格式: {chapterUid, chapterIdx, filePosition, stringLocation, pageOfChapter, lastReadDate}
|
||||
|
||||
// 字体设置
|
||||
NSString *const kWRFontFamilyKey = @"WRFontFamily"; // 字体族名
|
||||
NSString *const kWRFontSizeKey = @"WRFontSize"; // 字号 (默认 18)
|
||||
NSString *const kWRLineHeightKey = @"WRLineHeight"; // 行高 (默认 1.8)
|
||||
NSString *const kWRParagraphSpacingKey = @"WRParagraphSpacing"; // 段间距
|
||||
NSString *const kWRFirstIndentKey = @"WRFirstIndent"; // 首行缩进
|
||||
|
||||
// 主题设置
|
||||
NSString *const kWRThemeKey = @"WRTheme"; // 主题 (light/dark/sepia)
|
||||
NSString *const kWRBrightnessKey = @"WRBrightness"; // 亮度
|
||||
|
||||
// 翻页设置
|
||||
NSString *const kWRPageTurningStyleKey = @"WRPageTurningStyle"; // 翻页样式 (curl/scroll)
|
||||
NSString *const kWRAutoReadKey = @"WRAutoRead"; // 自动阅读
|
||||
|
||||
// 繁简转换
|
||||
NSString *const kWRCht2sKey = @"WRCht2s_{bookId}"; // 繁简转换状态
|
||||
|
||||
// 预加载
|
||||
NSString *const kWRPreloadSettingKey = @"WRPreloadSetting"; // 预加载设置
|
||||
NSString *const kWRPreloadEncryptKeyKey = @"WRPreloadEncryptKey_{bookId}_{chapterUid}";
|
||||
NSString *const kWRPreloadFileNameKey = @"WRPreloadFileName_{bookId}_{key}";
|
||||
```
|
||||
|
||||
### 3.3 Keychain Keys
|
||||
|
||||
```objc
|
||||
// 每本书的加密密钥
|
||||
NSString *const kWRBookEncryptKeyKey = @"com.weread.encrypt.{bookId}";
|
||||
// 存储在 Keychain 中, 标记为 kSecAttrAccessibleAfterFirstUnlock
|
||||
|
||||
// 用户凭证
|
||||
NSString *const kWRUserTokenKey = @"com.weread.user.token";
|
||||
NSString *const kWRUserVidKey = @"com.weread.user.vid";
|
||||
```
|
||||
|
||||
### 3.4 NSAttributedString 自定义属性键
|
||||
|
||||
```objc
|
||||
// 微信读书在 NSAttributedString 中注入的自定义属性
|
||||
// 用于在排版结果中传递页面布局元数据
|
||||
|
||||
NSString *const DTPageBackgroundColorAttribute = @"DTPageBackgroundColor";
|
||||
NSString *const DTPageBackgroundImageAttribute = @"DTPageBackgroundImage";
|
||||
NSString *const DTPageBackgroundImagePathAttribute = @"DTPageBackgroundImagePath";
|
||||
NSString *const DTPageBreakAfterAttribute = @"DTPageBreakAfter";
|
||||
NSString *const DTPageBreakBeforeAttribute = @"DTPageBreakBefore";
|
||||
NSString *const DTPageBreakInsideAvoidAttribute = @"DTPageBreakInsideAvoid";
|
||||
NSString *const DTPageRelateAttribute = @"DTPageRelate";
|
||||
NSString *const DTPageSize = @"DTPageSize";
|
||||
NSString *const DTPageFlippingStyle = @"DTPageFlippingStyle";
|
||||
NSString *const DTHTMLVerticalCenterAttribute = @"DTHTMLVerticalCenter";
|
||||
NSString *const DTHTMLTranslateTagAttribute = @"DTHTMLTranslateTag";
|
||||
NSString *const DTHTMLTranslateNoStyleAttribute = @"DTHTMLTranslateNoStyle";
|
||||
|
||||
// 标注相关属性
|
||||
NSString *const WRHighlightAttributeKey = @"com.weread.highlight";
|
||||
NSString *const WRUnderlineAttributeKey = @"com.weread.underline";
|
||||
NSString *const WRBookmarkAttributeKey = @"com.weread.bookmark";
|
||||
NSString *const WRReviewAttributeKey = @"com.weread.review";
|
||||
|
||||
// 附件属性
|
||||
NSString *const DTTextAttachmentAttribute = @"DTTextAttachment";
|
||||
NSString *const WRChapterToolAttachmentAttribute = @"WRChapterToolAttachment";
|
||||
NSString *const WRFlyleafAttachmentAttribute = @"WRFlyleafAttachment";
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,369 @@
|
||||
# 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 保护 |
|
||||
| 缓存 | 无 | 布局帧缓存 + 图片缓存 |
|
||||
@@ -0,0 +1,582 @@
|
||||
# EPUB 渲染管线详解
|
||||
|
||||
微信读书 (WeRead) 的 EPUB 渲染管线将原始 XHTML 文件转换为可分页、可交互的阅读视图。本文档完整描述从 EPUB 文件到屏幕像素的每一步。
|
||||
|
||||
---
|
||||
|
||||
## 1. 管线总览
|
||||
|
||||
```
|
||||
EPUB 文件 (.epub)
|
||||
|
|
||||
v
|
||||
[WREpubParser] 解析 EPUB 结构
|
||||
|
|
||||
v
|
||||
[WREpubTypesetter] XHTML → NSAttributedString
|
||||
| (CSS 级联 + HTML 解析 + 后处理)
|
||||
v
|
||||
[WRCoreTextLayouter] NSAttributedString → 分页布局
|
||||
| (CTTypesetter + 分页算法)
|
||||
v
|
||||
[WRCoreTextLayoutFrame] 单页布局帧
|
||||
| (CTFrame + 行提取 + 避免断页)
|
||||
v
|
||||
[WRPageView] 渲染到屏幕
|
||||
| (CGContext 绘制 + 图片 + 装饰)
|
||||
v
|
||||
屏幕像素
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 阶段一: EPUB 解析 (WREpubParser)
|
||||
|
||||
### 2.1 输入
|
||||
|
||||
EPUB 文件路径 + WRBook 模型对象
|
||||
|
||||
### 2.2 解析步骤
|
||||
|
||||
```
|
||||
1. parseContainerXML
|
||||
- 读取 META-INF/container.xml
|
||||
- 提取 OPF 文件路径 (rootfile full-path)
|
||||
|
||||
2. parseOPFAtRelativePath
|
||||
- 解析 content.opf
|
||||
- 提取 manifest (id → href 映射)
|
||||
- 提取 spine (阅读顺序 idref 列表)
|
||||
- 提取 metadata (书名、标识符等)
|
||||
|
||||
3. parseNCX
|
||||
- 解析 toc.ncx (目录)
|
||||
- 提取 navPoint 树 (id, label, src, playOrder)
|
||||
|
||||
4. _buildChapterList
|
||||
- 将 spine idref 映射到 manifest href
|
||||
- 生成 chapters 数组 [{id, href, mediaType, fullPath}]
|
||||
|
||||
5. _buildResourceMap
|
||||
- 构建资源路径映射 (href → 绝对路径)
|
||||
- 同时索引文件名用于快速查找
|
||||
```
|
||||
|
||||
### 2.3 输出
|
||||
|
||||
- chapters 数组(有序章节列表)
|
||||
- resourceMap 字典(资源路径映射)
|
||||
- WRBook 元数据更新
|
||||
|
||||
---
|
||||
|
||||
## 3. 阶段二: CSS 级联 (WREpubTypesetter)
|
||||
|
||||
### 3.1 CSS 加载顺序
|
||||
|
||||
```
|
||||
Layer 1: default.css (App Bundle)
|
||||
↓ 覆盖
|
||||
Layer 2: replace.css (App Bundle)
|
||||
↓ 覆盖
|
||||
Layer 3: dark.css (App Bundle, 仅暗色模式)
|
||||
↓ 覆盖
|
||||
Layer 4: EPUB 嵌入 CSS (书籍自带)
|
||||
↓ 覆盖
|
||||
Layer 5: 用户设置 CSS (运行时生成)
|
||||
```
|
||||
|
||||
### 3.2 各层内容
|
||||
|
||||
**Layer 1 - default.css**:
|
||||
```css
|
||||
body { font-family: "PingFang SC", sans-serif; margin: 0; padding: 10px 15px; }
|
||||
p { margin-top: 0.5em; margin-bottom: 0.5em; }
|
||||
h1 { font-size: 1.8em; font-weight: bold; }
|
||||
h2 { font-size: 1.5em; font-weight: bold; }
|
||||
h3 { font-size: 1.3em; font-weight: bold; }
|
||||
ul, ol { padding-left: 1.5em; }
|
||||
blockquote { margin-left: 1em; font-style: italic; }
|
||||
```
|
||||
|
||||
**Layer 2 - replace.css**:
|
||||
```css
|
||||
h1, h2, h3 { font-family: "Source Han Serif CN", serif; }
|
||||
pre, code { font-family: "Menlo", monospace; }
|
||||
img.bodyPic { wr-vertical-center-style: 2; max-width: 100%; }
|
||||
.conQuot { /* 引用块样式 */ }
|
||||
```
|
||||
|
||||
**Layer 3 - dark.css**:
|
||||
```css
|
||||
body { background-color: #1a1a1a; color: #cccccc; }
|
||||
a { color: #6eaad7; }
|
||||
img { filter: brightness(0.85); }
|
||||
```
|
||||
|
||||
**Layer 5 - 用户设置 CSS** (动态生成):
|
||||
```css
|
||||
body { font-size: 18px; line-height: 1.8; font-family: "PingFang SC", sans-serif; }
|
||||
body { background-color: #f5f0e8; } /* 护眼模式 */
|
||||
p { text-indent: 2em; } /* 首行缩进 */
|
||||
```
|
||||
|
||||
### 3.3 级联合并
|
||||
|
||||
使用 DTCSSStylesheet.mergeStylesheet: 方法按顺序合并,后加载的覆盖先前的同名规则。
|
||||
|
||||
---
|
||||
|
||||
## 4. 阶段三: HTML 解析 (DTHTMLAttributedStringBuilder)
|
||||
|
||||
### 4.1 SAX 解析流程
|
||||
|
||||
```
|
||||
XHTML 数据
|
||||
|
|
||||
v
|
||||
DTHTMLParser (SAX)
|
||||
|
|
||||
├── didStartElement: → 创建 DTHTMLElement 节点
|
||||
| 应用 CSS 样式
|
||||
| 处理自定义属性
|
||||
|
|
||||
├── foundCharacters: → 累积文本到当前元素
|
||||
|
|
||||
├── foundCDATA: → 处理 CDATA 内容
|
||||
|
|
||||
└── didEndElement: → 弹出元素栈
|
||||
调用 interpretAttributes
|
||||
插入分页标记
|
||||
```
|
||||
|
||||
### 4.2 元素处理
|
||||
|
||||
每个 HTML 标签被转换为 DTHTMLElement 节点:
|
||||
|
||||
```
|
||||
DTHTMLElement
|
||||
├── tagName: "p" / "div" / "img" / ...
|
||||
├── classNames: ["bodyPic", "conQuot"]
|
||||
├── fontDescriptor: 字体描述符
|
||||
├── paragraphStyle: 段落样式
|
||||
├── textColor / backgroundColor
|
||||
├── children: [DTHTMLElement]
|
||||
├── textAttachment (图片)
|
||||
├── linkURL (链接)
|
||||
├── [WeRead 扩展]
|
||||
│ ├── verticalCenterStyle
|
||||
│ ├── pageRelate
|
||||
│ ├── shouldAvoidPageBreakInside
|
||||
│ ├── pageBreakAfter / pageBreakBefore
|
||||
│ ├── pageBackgroundColor
|
||||
│ └── pageBackgroundImage
|
||||
```
|
||||
|
||||
### 4.3 样式应用顺序
|
||||
|
||||
```
|
||||
1. CSS 样式表规则 (class, id, tag 选择器)
|
||||
2. 内联 style="" 属性
|
||||
3. WeRead 自定义 CSS 属性
|
||||
4. 元素默认样式 (基于标签名)
|
||||
```
|
||||
|
||||
### 4.4 后处理 (_WRPostProcessElementTree)
|
||||
|
||||
在元素树转为 NSAttributedString 之前,执行 WeRead 特有的后处理:
|
||||
|
||||
**图片处理**:
|
||||
- 设置最大显示尺寸 (1080x1920)
|
||||
- 超限图片按比例缩放
|
||||
- 添加 .bodyPic CSS 类
|
||||
- 设置 wr-vertical-center-style: 2
|
||||
|
||||
**链接处理**:
|
||||
- 添加下划线样式
|
||||
- 存储链接 URL 到自定义属性
|
||||
|
||||
**自定义属性处理**:
|
||||
- wr-vertical-center-style → DTHTMLVerticalCenterAttribute
|
||||
- weread-page-relate → DTPageRelateAttribute
|
||||
|
||||
---
|
||||
|
||||
## 5. 阶段四: NSAttributedString 生成
|
||||
|
||||
### 5.1 递归转换
|
||||
|
||||
```
|
||||
DTHTMLElement.attributedString
|
||||
|
|
||||
├── 处理 void 元素 (br → "\n", img → attachment, hr → "\n")
|
||||
|
|
||||
├── 插入 pageBreakBefore 标记 (如果需要)
|
||||
|
|
||||
├── 转换文本内容
|
||||
| ├── 应用 CTFont (从 fontDescriptor)
|
||||
| ├── 应用前景色 (kCTForegroundColorAttributeName)
|
||||
| ├── 应用背景色 (DTBackgroundColor)
|
||||
| └── 应用链接 (DTLink)
|
||||
|
|
||||
├── 递归处理子元素
|
||||
|
|
||||
├── 应用段落样式 (kCTParagraphStyleAttributeName)
|
||||
|
|
||||
├── 插入 pageBreakAfter 标记 (如果需要)
|
||||
|
|
||||
└── 应用 WeRead 页面属性
|
||||
├── DTHTMLVerticalCenterAttribute
|
||||
├── DTPageRelateAttribute
|
||||
├── DTPageBreakInsideAvoidAttribute
|
||||
├── DTPageBackgroundColorAttribute
|
||||
└── DTPageBackgroundImageAttribute
|
||||
```
|
||||
|
||||
### 5.2 输出
|
||||
|
||||
一个 NSAttributedString,包含:
|
||||
- 标准 CoreText 属性 (字体、颜色、段落样式)
|
||||
- DTCoreText 标准属性 (链接、附件、列表)
|
||||
- WeRead 自定义属性 (分页、居中、背景)
|
||||
|
||||
---
|
||||
|
||||
## 6. 阶段五: 分页布局 (WRCoreTextLayouter)
|
||||
|
||||
### 6.1 Typesetter 创建
|
||||
|
||||
```
|
||||
NSAttributedString
|
||||
|
|
||||
v
|
||||
CTTypesetterCreateWithAttributedString
|
||||
|
|
||||
v
|
||||
CTTypesetter (内部缓存字形分析结果)
|
||||
```
|
||||
|
||||
### 6.2 分页算法
|
||||
|
||||
```
|
||||
输入: NSAttributedString + pageSize
|
||||
|
||||
1. 计算可用宽度 = pageSize.width - edgeInsets.left - edgeInsets.right
|
||||
|
||||
2. 初始化 currentIndex = 0
|
||||
|
||||
3. 循环:
|
||||
a. lineBreakIndex = CTTypesetterSuggestLineBreak(typesetter, currentIndex, usableWidth)
|
||||
b. 如果 lineBreakIndex <= 0, 退出循环
|
||||
c. 创建 WRCoreTextLayoutFrame(range: currentIndex..<currentIndex+lineBreakIndex)
|
||||
d. currentIndex += lineBreakIndex
|
||||
|
||||
4. 返回 [WRCoreTextLayoutFrame] 数组 (每帧 = 一页)
|
||||
```
|
||||
|
||||
### 6.3 页面范围查询
|
||||
|
||||
`rangeForPageAtIndex:pageSize:` 通过模拟分页快速定位指定页的文本范围,无需创建布局帧对象。
|
||||
|
||||
### 6.4 行高建议
|
||||
|
||||
`suggestedLineFragHeights` 逐行创建临时 CTLine 测量高度,用于精确分页计算和避免孤行/寡行。
|
||||
|
||||
---
|
||||
|
||||
## 7. 阶段六: 单页布局 (WRCoreTextLayoutFrame)
|
||||
|
||||
### 7.1 CTFrame 创建
|
||||
|
||||
```
|
||||
CTTypesetter + range + CGPath(rect)
|
||||
|
|
||||
v
|
||||
CTTypesetterCreateFrame
|
||||
|
|
||||
v
|
||||
CTFrame (包含 CTLines 和 CTRuns)
|
||||
```
|
||||
|
||||
### 7.2 行提取
|
||||
|
||||
```
|
||||
CTFrameGetLines → CTLine 数组
|
||||
CTFrameGetLineOrigins → 行原点数组
|
||||
|
||||
对每个 CTLine:
|
||||
CTLineGetStringRange → 字符范围
|
||||
CTLineGetTypographicBounds → ascent/descent/leading
|
||||
创建 WRCoreTextLayoutLine 对象
|
||||
|
||||
判断每行是否为段落末尾行 (检查下一个字符是否为 '\n')
|
||||
```
|
||||
|
||||
### 7.3 avoidPageBreakInside 实现
|
||||
|
||||
```
|
||||
1. 从最后一行向前遍历
|
||||
2. 检查行的 attributedString 属性:
|
||||
- WRAvoidPageBreakInside == YES?
|
||||
- WRBlockType ∈ {table, code, list, blockquote}?
|
||||
3. 如果是受保护元素的一部分,标记需要移除
|
||||
4. 最多移除 3 行 (kMaxLinesToRemove)
|
||||
5. 调用 rebuildFrameWithoutLastLines: 更新帧
|
||||
6. 返回 YES 表示有行被移除
|
||||
```
|
||||
|
||||
### 7.4 渲染高度计算
|
||||
|
||||
```
|
||||
getRenderHeight:
|
||||
lastLine = lines.lastObject
|
||||
lastLineBottom = lastLine.origin.y - lastLine.descent
|
||||
renderedHeight = frameHeight - lastLineBottom + insets.top + insets.bottom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 阶段七: 绘制到屏幕 (WRCoreTextLayoutFrame)
|
||||
|
||||
### 8.1 绘制流程
|
||||
|
||||
```
|
||||
drawInContext:image:size:inRect:position:
|
||||
|
||||
1. CGContextSaveGState
|
||||
|
||||
2. 坐标系翻转 (CoreText 底左原点 → UIKit 顶左原点)
|
||||
CGContextTranslateCTM(0, height)
|
||||
CGContextScaleCTM(1, -1)
|
||||
|
||||
3. 应用位置偏移 (多列/多页布局)
|
||||
|
||||
4. 应用内容内边距
|
||||
|
||||
5. 绘制封面图片 (如果有)
|
||||
|
||||
6. CTFrameDraw(ctFrame, context) — 绘制文本
|
||||
|
||||
7. drawAttachmentsInContext: — 绘制图片附件
|
||||
|
||||
8. drawDecorativeElementsInContext: — 绘制装饰元素
|
||||
├── 删除线
|
||||
├── 下划线
|
||||
└── 高亮背景
|
||||
|
||||
9. CGContextRestoreGState
|
||||
```
|
||||
|
||||
### 8.2 图片绘制
|
||||
|
||||
```
|
||||
遍历 attachments 数组:
|
||||
对每个 attachment:
|
||||
获取 image, position, size
|
||||
CGContextDrawImage(context, imageRect, image.CGImage)
|
||||
```
|
||||
|
||||
### 8.3 装饰元素绘制
|
||||
|
||||
**删除线**:
|
||||
```
|
||||
遍历 strikethroughRanges:
|
||||
对每个 range:
|
||||
找到相交的行
|
||||
计算 startX, endX (CTLineGetOffsetForStringIndex)
|
||||
y = line.origin.y + ascent * 0.3
|
||||
CGContextStrokePath
|
||||
```
|
||||
|
||||
**下划线**:
|
||||
```
|
||||
遍历 underlineRanges:
|
||||
对每个 range:
|
||||
找到相交的行
|
||||
y = line.origin.y - descent
|
||||
CGContextStrokePath (蓝色)
|
||||
```
|
||||
|
||||
**高亮**:
|
||||
```
|
||||
遍历 highlightRanges:
|
||||
对每个 range:
|
||||
找到相交的行
|
||||
计算高亮矩形 (descent 到 ascent)
|
||||
CGContextFillRect (半透明黄色)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 交互层
|
||||
|
||||
### 9.1 文本选择
|
||||
|
||||
```
|
||||
characterIndexAtPoint:
|
||||
遍历所有行
|
||||
检查点是否在行的垂直范围内
|
||||
使用 CTLineGetStringIndexForPosition 定位字符
|
||||
|
||||
selectTextInRange:
|
||||
遍历所有行
|
||||
找到与 range 相交的行
|
||||
添加到 selectedLineIndices
|
||||
通过 RACSubject 发送选择通知
|
||||
```
|
||||
|
||||
### 9.2 搜索高亮
|
||||
|
||||
```
|
||||
highlightSearchResults:
|
||||
使用 NSString rangeOfString: 搜索
|
||||
收集所有匹配范围
|
||||
创建 highlightRanges (黄色半透明)
|
||||
返回匹配数量
|
||||
```
|
||||
|
||||
### 9.3 Hit Testing
|
||||
|
||||
```
|
||||
stringIndexAtPoint:
|
||||
遍历行 → 找到目标行
|
||||
CTLineGetStringIndexForPosition → 字符索引
|
||||
|
||||
rectForCharacterAtIndex:
|
||||
遍历行 → 找到包含索引的行
|
||||
CTLineGetOffsetForStringIndex → x 坐标
|
||||
返回 CGRect
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 附加功能
|
||||
|
||||
### 10.1 繁简转换
|
||||
|
||||
```
|
||||
如果 book.language ∈ {zh-Hant, zh-TW, zh-HK}:
|
||||
CFStringTransform(Hans → Latin → Hant)
|
||||
```
|
||||
|
||||
### 10.2 免费试读截断
|
||||
|
||||
```
|
||||
如果 isFreeTrial && result.length > trialCharacterLimit:
|
||||
从限制位置向前搜索段落边界 (\n 或 U+2029)
|
||||
截取子串
|
||||
追加 "\n\n...\n\n" 指示器
|
||||
```
|
||||
|
||||
### 10.3 附件注入
|
||||
|
||||
```
|
||||
如果 insertArticleToolAttachment:
|
||||
追加 NSTextAttachment {type: "articleTool"}
|
||||
|
||||
如果 insertBookChapterToolAttachment:
|
||||
追加 NSTextAttachment {type: "bookChapterTool"}
|
||||
|
||||
如果 insertRecommendView:
|
||||
追加 NSTextAttachment {type: "recommendView"}
|
||||
```
|
||||
|
||||
### 10.4 页面背景生成
|
||||
|
||||
```
|
||||
pageBackgroundImageAtRange:themeBgColor:
|
||||
检查是否为章节开头页
|
||||
创建 UIGraphicsImageContext
|
||||
填充主题背景色
|
||||
如果是章节开头: 绘制装饰边框
|
||||
否则: 绘制边距参考线
|
||||
缓存结果
|
||||
```
|
||||
|
||||
### 10.5 图片缩放
|
||||
|
||||
```
|
||||
resizedImageForImagePath:rect:position:sizePattern:darkMode:themeBgColor:
|
||||
根据 sizePattern 计算目标尺寸:
|
||||
"full" → 全宽
|
||||
"half" → 半宽
|
||||
"third" → 1/3 宽
|
||||
"quarter" → 1/4 宽
|
||||
如果 darkMode: 混合背景色
|
||||
高质量插值缩放
|
||||
缓存结果
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 性能优化
|
||||
|
||||
### 11.1 缓存策略
|
||||
|
||||
- **布局帧缓存**: NSCache, 最多 20 个 (DTCoreTextLayouter)
|
||||
- **图片缓存**: NSCache, 最多 50 个 (WRCoreTextLayouter)
|
||||
- **页面背景缓存**: NSCache, 按 range + 颜色键 (WRCoreTextLayouter)
|
||||
- **内容高度缓存**: 布局帧级别 (DTCoreTextLayoutFrame)
|
||||
|
||||
### 11.2 懒加载
|
||||
|
||||
- CTTypesetter 按需创建 (createTypesetter)
|
||||
- CTFramesetter 按需创建 (createFramesetter)
|
||||
- 行数据按需提取 (extractLines)
|
||||
- 字形数据按需提取 (extractGlyphs)
|
||||
|
||||
### 11.3 线程安全
|
||||
|
||||
所有核心类使用 NSLock 保护:
|
||||
- DTCoreTextLayouter._lock
|
||||
- DTCoreTextLayoutFrame._lock
|
||||
- DTCoreTextLayoutLine._lock
|
||||
- DTCoreTextGlyphRun._lock
|
||||
- WRCoreTextLayouter._layoutLock
|
||||
- WRCoreTextLayoutFrame._frameLock
|
||||
|
||||
### 11.4 增量更新
|
||||
|
||||
- attributedString 变化时标记 typesetterDirty
|
||||
- 仅在下次访问时重建 typesetter
|
||||
- 布局帧缓存自动失效
|
||||
|
||||
---
|
||||
|
||||
## 12. 数据流总结
|
||||
|
||||
```
|
||||
EPUB 文件
|
||||
│
|
||||
├─ container.xml → OPF 路径
|
||||
├─ content.opf → manifest + spine + metadata
|
||||
├─ toc.ncx → 目录树
|
||||
└─ *.xhtml → 章节内容
|
||||
│
|
||||
▼
|
||||
XHTML 字符串
|
||||
│ (繁简转换)
|
||||
▼
|
||||
CSS 级联合并
|
||||
│ (5 层合并)
|
||||
▼
|
||||
DTHTMLAttributedStringBuilder
|
||||
│ (SAX 解析 + DOM 构建 + 样式应用)
|
||||
▼
|
||||
DTHTMLElement 树
|
||||
│ (后处理: 图片/链接/自定义属性)
|
||||
▼
|
||||
NSAttributedString
|
||||
│ (包含标准 + 自定义属性)
|
||||
▼
|
||||
CTTypesetter
|
||||
│ (字形分析 + 行断点计算)
|
||||
▼
|
||||
WRCoreTextLayoutFrame[]
|
||||
│ (每帧 = 一页, 含 avoidPageBreakInside)
|
||||
▼
|
||||
CGContext 绘制
|
||||
│ (文本 + 图片 + 装饰)
|
||||
▼
|
||||
屏幕像素
|
||||
```
|
||||
Reference in New Issue
Block a user