docs(08): research pagination cache, quality, and performance sampling

This commit is contained in:
shen 2026-05-23 22:35:16 +08:00
parent 22bb86959c
commit 5125b8de51

View File

@ -1,474 +1,485 @@
# Phase 8: 图片显示修复 - Research # Phase 8: 分页质量、缓存与性能采样 - Research
**Researched:** 2026-05-23 **Researched:** 2026-05-23
**Domain:** DTCoreText image attachment sizing, WXRead alignment **Domain:** iOS/Swift EPUB reader - pagination caching, quality improvement, performance sampling
**Confidence:** HIGH **Confidence:** HIGH
## Summary ## Summary
Phase 8 plan 08-02 focuses on fixing image display in the native text rendering path (DTCoreText). Three image types have sizing issues: (1) `qrbodyPic` images can overflow page height because no max size constraint is applied, (2) cover images use screen-size baselines but lack a unified ceiling, (3) footnote images have inconsistent height calculations across three separate code paths. Phase 8 builds on Phase 6 (page geometry) and Phase 7 (custom attribute closure) to address three concerns: pagination caching, pagination quality for complex image-heavy chapters, and performance diagnostics.
The core fix is to add a unified maximum size constraint (`CGSize(width: 1080, height: 1920)`) to ALL image attachments in `prepareHTMLElementForReaderRendering`, applied BEFORE any type-specific handling. This directly mirrors WXRead's `_WRPostProcessElementTree` approach. The fix is surgical: one new code block at the top of the existing function, plus removal of redundant height logic in two other locations. **Critical finding:** Several capabilities assumed to be "not yet integrated" in CONTEXT.md and ARCHITECTURE-CONTEXT.md are actually already implemented:
- `RDEPUBTextBookBuilder.build()` already calls `content.rd_paginatedFrames(size:)` (not `ss_pageRanges`). The integration is DONE.
- CSS `<link>` inlining is already implemented via `inlineLinkedStyleSheets()` in `RDEPUBTextRendererSupport`.
- The 5-layer CSS cascade is already implemented via `makeStyleSheetLayers()` producing `RDEPUBTextStyleSheetPackage` layers.
- `RDEPUBTextPage.metadata` is already populated with `RDEPUBTextPageMetadata` from `frame.metadata`.
**Primary recommendation:** Add a unified max-size block at the top of `prepareHTMLElementForReaderRendering` that constrains all images to 1080x1920 via aspect-ratio-preserving scaling. Then simplify footnote handling to width-only (no explicit height), matching WXRead's `replace.css`. **What actually needs building:**
1. **Pagination cache** (QUAL-01): Serialize `RDEPUBTextBook` to disk, keyed by layout parameters
2. **Image/attachment rules** (QUAL-03): Formalize sizing, dark mode, and page-break-around-image rules
3. **Performance sampling** (QUAL-04): Instrument render/paginate/build timings
4. **Pagination quality** (QUAL-02): Implement `avoidPageBreakInside` line-removal, orphan/widow control, image fit enforcement
**Primary recommendation:** Focus Phase 8 on cache serialization (the largest new code), performance instrumentation (straightforward), and incremental pagination quality improvements. Do NOT re-implement CSS `<link>` inlining or 5-layer cascade -- they already work.
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Pagination cache storage | API / Backend (file system) | -- | Cache is local disk persistence, no UI involvement |
| Cache key generation | API / Backend | -- | Pure computation from layout parameters |
| Performance timing | API / Backend | -- | CFAbsoluteTimeGetCurrent at build/render points |
| Image sizing rules | API / Backend (renderer) | Browser/Client (display) | Rules applied during DTCoreText rendering |
| avoidPageBreakInside enforcement | API / Backend (layouter) | -- | CoreText line-level calculation |
| Orphan/widow control | API / Backend (layouter) | -- | CoreText line-level calculation |
| Pagination diagnostic output | API / Backend | Browser/Client (logging) | Diagnostic data flows to console/log |
<user_constraints>
## User Constraints (from CONTEXT.md) ## User Constraints (from CONTEXT.md)
### Locked Decisions ### Locked Decisions
- **D-01:** Unified max image size `CGSizeMake(1080, 1920)` for all image attachments
- **D-02:** Scale proportionally when exceeding max width (matching WXRead `_WRPostProcessElementTree`) - **D-01 [P0]:** Integrate `RDEPUBTextLayouter` into `RDEPUBTextBookBuilder` -- ALREADY DONE (see finding above)
- **D-03:** Implementation location: `prepareHTMLElementForReaderRendering`, BEFORE existing footnote/cover special handling - **D-02 [P0]:** Expose `metadata: RDEPUBTextPageMetadata` on `RDEPUBTextPage` -- ALREADY DONE
- **D-04:** Cover images keep screen-size base (`UIScreen.main.bounds.insetBy(dx: 20, dy: 28)`), constrained by unified max - **D-03 [P0]:** Pagination cache by `bookID + fontSize + lineHeightMultiple + contentInsets`, cache full `RDEPUBTextBook` to disk
- **D-05:** Keep `configureCoverIfNeeded` UIImageView path unchanged - **D-04 [P0]:** CSS `<link>` inline processing -- ALREADY DONE
- **D-06:** Cover images still processed via cover branch in `prepareHTMLElementForReaderRendering`, but must not exceed unified max - **D-05 [P0]:** 5-layer CSS cascade -- ALREADY DONE
- **D-07:** Footnote images: only set `width:1em`, no height (align with WXRead `replace.css`) - **D-06 [P1]:** Image/attachment processing rules with diagnostics
- **D-08:** Remove footnote height calculation in `prepareHTMLElementForReaderRendering` (`pointSize * 0.54`) - **D-07 [P1]:** Performance sampling -- output stable sampling data
- **D-09:** Remove height override in `normalizeInlineAttachments` (`pointSize * 0.14`)
- **D-10:** Keep CSS `width:1em` in `normalizeAttachmentHTMLMarkers`, remove `height:1em`
- **D-11:** No special handling for `qrbodyPic` — unified max size implicitly fixes overflow
- **D-12:** Keep existing CSS rules for `qrbodyPic` unchanged
### Claude's Discretion ### Claude's Discretion
- Whether to auto-add `bodyPic` class to all images (like WXRead does) - Cache storage format (file system plist/JSON vs SQLite) -- implementer decides
- Whether `wr-vertical-center-style` semantic marker is needed (Phase 7 already handles this) - CSS cascade replace.css content -- reference WXRead but don't copy exactly
- Specific implementation details of the unified max-size block - Performance sampling threshold values -- determine at implementation time
### Deferred Ideas (OUT OF SCOPE) ### Deferred Ideas (OUT OF SCOPE)
- Image caching mechanism (belongs to plan 08-01) - CoreText direct drawing migration (v1.2/v2.0)
- `wr-vertical-center-style` full implementation (Phase 7 already completed) - Font system (embedded font set, future version)
- Image tap-to-zoom feature (new capability, not in scope) - Character-level position precision (P2)
</user_constraints> - Chapter data model cohesion (P2)
- TTS / DRM / Pencil / multi-column layout (P3)
<phase_requirements> <phase_requirements>
## Phase Requirements ## Phase Requirements
| ID | Description | Research Support | | ID | Description | Research Support |
|----|-------------|------------------| |----|-------------|------------------|
| QUAL-01 | Cache layout frame / pagination results | Not in scope for 08-02 (belongs to 08-01) | | QUAL-01 | Cache mechanism for layout frames/pagination results to avoid redundant full-layout | Cache key design from WXRead `WRChapterPageCount.currentCacheKeyWithBookId:`, serialization strategy for `RDEPUBTextBook` |
| QUAL-02 | Improve pagination quality for complex illustrated chapters — reduce bad page breaks, orphan/widow lines, abnormal whitespace around images | Image sizing fix directly addresses image-related pagination quality | | QUAL-02 | Improve pagination quality for complex image-heavy chapters -- reduce bad breaks, orphan/widow lines, image whitespace issues, code/table truncation | WXRead `avoidPageBreakInsideByRemovingLastLinesIfNeeded` pattern, `WRCoreTextLayoutConfig` widow/orphan settings |
| QUAL-03 | Image/attachment sizing strategy, dark mode adaptation, page background — must have verifiable processing rules, not rely on DTCoreText defaults | Unified max size + explicit CSS rules provide verifiable rules | | QUAL-03 | Image/attachment sizing rules, dark mode adaptation, page background info with verifiable rules | WXRead `wr-vertical-center-style`, `DTPageBreakInsideAvoid`, existing `prepareHTMLElementForReaderRendering` |
| QUAL-04 | Pagination improvements must not significantly degrade first-screen time, re-pagination time, or interaction smoothness | Image sizing is a lightweight operation in willFlushCallback, negligible performance impact | | QUAL-04 | No significant degradation of first-screen time, repagination time, or interaction fluidity; output stable diagnostic/sampling data | Performance sampling points at render/paginate/build, `CFAbsoluteTimeGetCurrent` pattern |
</phase_requirements> </phase_requirements>
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Image max-size constraint | Swift `willFlushCallback` (renderer) | CSS `replaceCSS` | Swift handles absolute pixel ceiling; CSS handles responsive width |
| Footnote image sizing | CSS (`width:1em` in HTML preprocessing) | Swift (vertical alignment) | CSS controls width; Swift only sets alignment |
| Cover image sizing | Swift (`prepareHTMLElementForReaderRendering`) | `configureCoverIfNeeded` (view layer) | Swift constrains in attributed string; view layer only extracts for UIImageView display |
| Page-break control around images | Layouter (pagination) | CSS (`page-break-inside: avoid`) | Layouter decides actual breaks; CSS provides hints |
## Standard Stack ## Standard Stack
### Core ### Core (no new external dependencies)
This phase uses only existing project dependencies and Apple frameworks:
| Library | Version | Purpose | Why Standard | | Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------| |---------|---------|---------|--------------|
| DTCoreText | (CocoaPod, project-local) | HTML/CSS to NSAttributedString | Existing project dependency; provides `DTTextAttachment`, `willFlushCallback` | | Foundation | iOS SDK | NSKeyedArchiver, JSONEncoder, FileManager | System framework, no alternative |
| UIKit | iOS SDK | `UIScreen.main.bounds` for cover sizing | Platform standard | | CoreText | iOS SDK | CTFramesetter, CTFrame line inspection | Already used by RDEPUBTextLayouter |
| DTCoreText | (existing) | HTML-to-NSAttributedString | Already integrated |
| CryptoKit / CommonCrypto | iOS SDK | SHA256 for cache key hashing | System framework |
### Supporting ### Supporting
| Library | Version | Purpose | When to Use | | Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------| |---------|---------|---------|-------------|
| Foundation | iOS SDK | `NSRegularExpression` for HTML preprocessing | Already used throughout `RDEPUBTextRendererSupport` | | os.signpost | iOS 15+ | Performance instrumentation with Instruments integration | If structured performance logging desired |
### Alternatives Considered ### Alternatives Considered
| Instead of | Could Use | Tradeoff | | Instead of | Could Use | Tradeoff |
|------------|-----------|----------| |------------|-----------|----------|
| Swift-side max size in `willFlushCallback` | CSS `max-height` in `replaceCSS` | CSS approach is simpler but DTCoreText CSS parser may not support `max-height` reliably for attachments; Swift is authoritative | | NSKeyedArchiver for cache | JSONEncoder | JSON is human-readable but can't handle NSAttributedString natively; NSKeyedArchiver handles it but produces opaque binary |
| Removing `DTMaxImageSize` from builder options | Keeping both layers | Keeping both is safer — DTMaxImageSize is the first line of defense at attachment creation time; willFlushCallback is the second pass | | File system cache | SQLite | File system is simpler for whole-object caching; SQLite adds complexity for no gain when caching complete objects |
| SHA256 cache key | Simple string concatenation | SHA256 produces fixed-length keys safe for filenames; raw string could exceed filesystem limits |
**Installation:** No new packages needed.
## Package Legitimacy Audit ## Package Legitimacy Audit
No new packages are installed in this phase. All changes are to existing Swift source files. No external packages are installed in this phase. All dependencies are existing project dependencies or Apple system frameworks.
| Package | Registry | Age | Downloads | Source Repo | slopcheck | Disposition | | Package | Registry | Age | Downloads | Source Repo | slopcheck | Disposition |
|---------|----------|-----|-----------|-------------|-----------|-------------| |---------|----------|-----|-----------|-------------|-----------|-------------|
| (none) | — | — | — | — | — | No new packages | | (none) | -- | -- | -- | -- | -- | N/A |
## Architecture Patterns ## Architecture Patterns
### Current Image Processing Pipeline ### Current Data Flow (already working)
``` ```
HTML input RDEPUBReaderController.paginate(restoreLocation:)
| -> RDEPUBTextBookBuilder.build(parser:publication:pageSize:style:)
v -> for each spine item:
normalizeAttachmentHTMLMarkers() <-- HTML regex: adds inline styles to <img> tags -> RDEPUBTextRendererSupport.makeChapterRenderRequest(...)
| (footnote: width:1em;height:1em) -> inlineLinkedStyleSheets() [CSS <link> inlining - DONE]
| (cover: adds rd-front-cover-image class) -> makeStyleSheetLayers() [5-layer cascade - DONE]
v -> injectPaginationSemanticMarkers() [semantic markers - DONE]
DTHTMLAttributedStringBuilder <-- Creates DTTextAttachment with DTMaxImageSize -> renderer.renderChapter(request:) [DTCoreText render]
| (currently screenBounds.size, ~353x757) -> content.rd_paginatedFrames(size:) [4-level semantic pagination - DONE]
| DTCoreText sets displaySize = min(original, max) -> RDEPUBTextPage with metadata [metadata exposure - DONE]
v -> RDEPUBTextBook(chapters:, pages:)
willFlushCallback <-- prepareHTMLElementForReaderRendering() -> applyTextBook(restoreLocation:)
| Currently: footnote → height calc, cover → screen scale
| MISSING: unified max-size for ALL images
v
normalizeReadingAttributes() <-- normalizeAttachmentDisplayIfNeeded()
| Currently: footnote → height override (0.14*pt)
| cover → maxWidth override
v
configureCoverIfNeeded() <-- View layer: extracts cover image for UIImageView
|
v
normalizeInlineAttachments() <-- View layer: footnote height override (0.14*pt)
``` ```
### Recommended Pipeline After Fix ### Recommended Cache Integration Point
``` ```
HTML input RDEPUBReaderController.paginate(restoreLocation:)
| -> compute cacheKey from bookID + layout params
v -> if cached RDEPUBTextBook exists for key:
normalizeAttachmentHTMLMarkers() <-- CHANGED: footnote <img> only gets width:1em (no height) -> applyTextBook(cachedBook, restoreLocation:) [CACHE HIT - skip build]
v -> else:
DTHTMLAttributedStringBuilder <-- CHANGED: DTMaxImageSize CGSizeMake(1080, 1920) -> builder.build(...) [existing flow]
v -> save RDEPUBTextBook to cache [CACHE WRITE]
willFlushCallback <-- prepareHTMLElementForReaderRendering() -> applyTextBook(textBook, restoreLocation:)
| NEW: unified max-size block for ALL images first
| THEN: footnote → only verticalAlignment + inline
| THEN: cover → screen-size scale (capped by max)
v
normalizeReadingAttributes() <-- normalizeAttachmentDisplayIfNeeded()
| CHANGED: footnote path removed (no height override)
| cover path can remain as fallback
v
configureCoverIfNeeded() <-- UNCHANGED
v
normalizeInlineAttachments() <-- CHANGED: footnote height override removed
``` ```
### Pattern 1: Unified Max-Size Block in `prepareHTMLElementForReaderRendering` ### Recommended Project Structure
**What:** A new code block at the top of the function, before any type-specific handling, that constrains ALL image attachments to 1080x1920. ```
Sources/RDReaderView/EPUBTextRendering/
├── RDEPUBTextBookBuilder.swift # existing - add cache integration
├── RDEPUBTextBookCache.swift # NEW - cache key, read/write, eviction
├── RDEPUBTextLayouter.swift # existing - add avoidPageBreakInside enforcement
├── RDEPUBTextLayoutFrame.swift # existing - no changes needed
├── RDEPUBTextPaginationSupport.swift # existing - no changes needed
├── RDEPUBTextRenderer.swift # existing - no changes needed
├── RDEPUBTextRendererSupport.swift # existing - add image rule formalization
├── RDEPUBDTCoreTextRenderer.swift # existing - no changes needed
├── RDEPUBTextPerformanceSampler.swift # NEW - timing instrumentation
└── RDPlainTextBookBuilder.swift # existing - no changes needed
```
**When to use:** Every image attachment that enters `willFlushCallback`. ### Pattern 1: Cache Key Generation
**What:** Generate a deterministic cache key from layout parameters that change pagination results.
**When to use:** Before every `builder.build()` call.
**Example:** **Example:**
```swift ```swift
// Source: WXRead WREpubTypesetter.m §672-701 (_WRPostProcessElementTree) // Reference: WXRead WRChapterPageCount.currentCacheKeyWithBookId:
// Placed BEFORE existing footnote/cover handling in prepareHTMLElementForReaderRendering // WXRead encodes: bookId + fontSize + lineSpacing + pageWidth + pageHeight
static func prepareHTMLElementForReaderRendering( struct RDEPUBTextBookCacheKey {
_ element: DTHTMLElement, let bookID: String
style: RDEPUBTextRenderStyle let fontSize: CGFloat
) { let lineHeightMultiple: CGFloat
guard let attachment = element.textAttachment else { return } let contentInsets: UIEdgeInsets
let pageSize: CGSize
// --- NEW: Unified max image size (aligns with WXRead _WRPostProcessElementTree) --- var stringValue: String {
let maxImageSize = CGSize(width: 1080, height: 1920) let raw = "\(bookID)|\(fontSize)|\(lineHeightMultiple)|\(contentInsets.top)|\(contentInsets.left)|\(contentInsets.bottom)|\(contentInsets.right)|\(pageSize.width)|\(pageSize.height)"
let originalSize = attachment.originalSize // SHA256 for safe filename
if originalSize.width > maxImageSize.width || originalSize.height > maxImageSize.height { let data = Data(raw.utf8)
let scale = min(maxImageSize.width / max(originalSize.width, 1), let hash = SHA256.hash(data: data)
maxImageSize.height / max(originalSize.height, 1)) return hash.map { String(format: "%02x", $0) }.joined()
attachment.displaySize = CGSize(
width: round(originalSize.width * scale),
height: round(originalSize.height * scale)
)
} }
// ... existing footnote/cover handling follows ...
} }
``` ```
**Source:** [VERIFIED: Doc/WXRead/decompiled/WRChapterPageCount.m lines 64-90]
### Pattern 2: Footnote Width-Only Sizing ### Pattern 2: avoidPageBreakInside Line Removal
**What:** Footnote images get `width:1em` in HTML preprocessing, with no explicit height. DTCoreText calculates height from aspect ratio.
**When to use:** In `normalizeAttachmentHTMLMarkers` for `qqreader-footnote` images.
**What:** When a block marked `avoidPageBreakInside` would cross a page boundary, remove trailing lines from the current page to push the entire block to the next page.
**When to use:** During `RDEPUBTextLayouter.adjustedRange()` after the 4-level semantic break detection.
**Example:** **Example:**
```swift ```swift
// Source: WXRead replace.css §100-103 (.qqreader-footnote { width: 1em; }) // Reference: WXRead WRCoreTextLayoutFrame.avoidPageBreakInsideByRemovingLastLinesIfNeeded
// Changed: removed "height:1em" from styleFragments // If the proposed page end falls inside an avoidPageBreakInside block:
// 1. Find the block's start location
normalized = replaceMatches( // 2. If block start >= minimumEnd, break at block start
using: footnoteRegex, // 3. If block start < minimumEnd (block is too large), fall through to frameLimit
in: normalized
) { tag in
mergeHTMLAttributes(
into: tag,
requiredClass: nil,
styleFragments: [
"width:1em",
// "height:1em" <-- REMOVED per D-10
"vertical-align:middle",
"display:inline-block"
]
)
}
``` ```
**Source:** [VERIFIED: Doc/WXRead/decompiled/WRCoreTextLayoutFrame.h line 280]
### Pattern 3: Performance Sampling
**What:** Measure wall-clock time for render, paginate, and full-build operations.
**When to use:** At the entry/exit of `builder.build()`, `renderer.renderChapter()`, and `content.rd_paginatedFrames()`.
**Example:**
```swift
struct RDEPUBTextPerformanceSample {
var chapterHref: String
var renderDuration: TimeInterval // DTHTMLAttributedStringBuilder
var paginateDuration: TimeInterval // CTFramesetter pagination
var totalDuration: TimeInterval // end-to-end
var attributedStringLength: Int
var pageCount: Int
var cacheHit: Bool
}
// Measurement pattern:
let start = CFAbsoluteTimeGetCurrent()
// ... operation ...
let duration = CFAbsoluteTimeGetCurrent() - start
```
**Source:** [ASSUMED] -- standard iOS performance measurement pattern
### Anti-Patterns to Avoid ### Anti-Patterns to Avoid
- **Separate max-size blocks for each image type:** WXRead applies one unified block to ALL images. Do not duplicate the scaling logic for qrbodyPic, cover, and footnote separately. - **Storing NSAttributedString via JSONEncoder:** NSAttributedString is not Codable. Use NSKeyedArchiver or store the raw HTML + re-render parameters instead.
- **Removing `DTMaxImageSize` from builder options:** The DTCoreText-level max size is the first line of defense (applied during attachment creation). The `willFlushCallback` max size is the second pass. Keep both. - **Cache key that includes theme colors:** Theme (dark/light) changes CSS output but the pagination structure (NSRanges) is the same for the same font/size/insets. However, the attributed string content differs. Cache should store the full RDEPUBTextBook including styled content, so theme must be part of the key OR cache should store structure only and re-render content.
- **Setting explicit height on footnote images:** WXRead only sets `width:1em` on `.qqreader-footnote`. DTCoreText's `setDisplaySize:withMaxDisplaySize:` handles width-only cases by calculating height from aspect ratio (line 231-236 of DTTextAttachment.m). - **Measuring on main thread:** All build/paginate work happens on `DispatchQueue.global(qos: .userInitiated)`. Timing must be captured there, not dispatched back to main.
- **Infinite cache growth:** Must implement eviction (LRU or size-based) to prevent disk space issues.
## Don't Hand-Roll ## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why | | Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----| |---------|-------------|-------------|-----|
| Aspect ratio calculation | Custom scaling math | DTCoreText's `DTCGSizeThatFitsKeepingAspectRatio` or the existing `min(w/maxW, h/maxH)` pattern | Consistent with DTCoreText internals | | SHA256 hashing | Custom hash function | CryptoKit or CommonCrypto | Cryptographic correctness, no collision risk |
| Image type detection | New classification system | Existing CSS class + filename detection (`lowercasedClasses`, `lowercasedPath`) | Already works, battle-tested in Phase 7 | | File system cache directory | Custom path logic | `FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)` | Standard iOS cache location, auto-managed by OS |
| Thread-safe cache access | Manual locks | `DispatchQueue` or `NSLock` | Standard concurrency pattern |
| Orphan/widow detection | Line counting heuristics | CTFrame line origins + paragraph range analysis | CoreText provides exact line positions |
**Key insight:** The hardest part of this phase is NOT the algorithms -- it's serializing `RDEPUBTextBook` which contains `NSAttributedString` (not Codable) and `NSRange` (not natively Codable in Swift). The serialization strategy is the most consequential design decision.
## Common Pitfalls ## Common Pitfalls
### Pitfall 1: DTMaxImageSize vs willFlushCallback Ordering ### Pitfall 1: NSAttributedString Serialization
**What goes wrong:** If the max-size constraint is only in `willFlushCallback`, DTCoreText's initial `setDisplaySize:withMaxDisplaySize:` uses the old (screen-size) max. The `willFlushCallback` then overrides it. This works but the intermediate state could confuse debugging. **What goes wrong:** Attempting to JSONEncode an `RDEPUBTextBook` fails silently because `NSAttributedString` and `NSRange` are not Codable.
**Why it happens:** DTMaxImageSize is consumed at attachment creation time; willFlushCallback runs after. **Why it happens:** `RDEPUBTextPage.content` is `NSAttributedString`, and `RDEPUBTextPageMetadata` contains `[NSRange]` fields.
**How to avoid:** Update BOTH: change `DTMaxImageSize` in `dtOptions()` to `CGSize(width: 1080, height: 1920)` AND add the max-size block in `prepareHTMLElementForReaderRendering`. The willFlushCallback block is the authoritative constraint; DTMaxImageSize is the first-pass optimization. **How to avoid:** Use `NSKeyedArchiver` for the whole object, OR decompose into Codable parts (store HTML + parameters, re-render on load). The second approach is more resilient to code changes but slower on cache load.
**Warning signs:** If only one is updated, images may briefly appear at wrong size before being corrected. **Warning signs:** `JSONEncoder.encode()` returns nil or throws.
### Pitfall 2: Footnote Aspect Ratio Without Explicit Height ### Pitfall 2: Cache Invalidation on Code Changes
**What goes wrong:** After removing `height:1em` from HTML preprocessing AND removing the height calculation in `prepareHTMLElementForReaderRendering`, footnote images might render with incorrect aspect ratio if `originalSize` is not available. **What goes wrong:** After updating the pagination engine code, cached results are stale but the cache key hasn't changed.
**Why it happens:** Some images in EPUBs may not have `width`/`height` attributes, so `originalSize` could be `CGSizeZero`. **Why it happens:** Cache key only encodes user-facing parameters (font, size, insets), not code version.
**How to avoid:** DTCoreText's `setDisplaySize:withMaxDisplaySize:` (line 218-237 of DTTextAttachment.m) handles the case where `originalSize` has width but not height (and vice versa) by calculating from aspect ratio. If `originalSize` is completely zero, the image falls back to the `DTMaxImageSize` constraint. This should be safe, but verify with the demo sample. **How to avoid:** Include a schema version number in the cache key. Bump the version when pagination logic changes.
**Warning signs:** Footnote images appearing at 0x0 or unreasonably large sizes. **Warning signs:** Pagination quality doesn't improve after code changes until cache is manually cleared.
### Pitfall 3: Cover Image Double-Scaling ### Pitfall 3: NSRange Codable in Metadata
**What goes wrong:** The unified max-size block scales the image, then the cover-specific block scales it again, resulting in an image that's too small. **What goes wrong:** `RDEPUBTextPageMetadata` conforms to `Codable` but contains `NSRange` fields (`blockRange`, `attachmentRanges`) which don't have native Codable conformance.
**Why it happens:** Two sequential scaling operations compound. **Why it happens:** `NSRange` is an Objective-C struct, not a Swift Codable type.
**How to avoid:** The cover-specific block in `prepareHTMLElementForReaderRendering` already uses `UIScreen.main.bounds.insetBy(dx: 20, dy: 28)` as its max, which is approximately 353x757 points. This is SMALLER than 1080x1920. So the unified max-size block will be a no-op for cover images (they're already within bounds). The cover block then applies its own tighter constraint. This is correct behavior — no double-scaling occurs. **How to avoid:** Add `Codable` conformance for `NSRange` via an extension that encodes as `{"location": Int, "length": Int}`, or use `NSKeyedArchiver` for the whole metadata.
**Warning signs:** Cover images appearing smaller than expected. **Warning signs:** Compiler error or runtime crash when encoding metadata.
### Pitfall 4: normalizeAttachmentDisplayIfNeeded Still Overrides ### Pitfall 4: Image Pages Crossing Boundaries
**What goes wrong:** After fixing `prepareHTMLElementForReaderRendering`, the `normalizeAttachmentDisplayIfNeeded` function (called from `normalizeReadingAttributes`) still overrides footnote and cover sizes. **What goes wrong:** A large image that doesn't fit on the current page causes the page to have excessive whitespace or the image gets truncated.
**Why it happens:** This function runs AFTER `willFlushCallback` in the pipeline. **Why it happens:** CoreText treats `NSTextAttachment` as inline content and will break the line at the attachment, but doesn't check if the image height exceeds remaining page space.
**How to avoid:** Per D-08/D-09, remove the footnote height override in this function too. The cover path in this function can remain as a fallback (it uses `maxWidth = max(UIScreen.main.bounds.width - 48, font.lineHeight * 8)` which is similar to the cover block in `prepareHTMLElementForReaderRendering`). **How to avoid:** Before pagination, check attachment heights against remaining page space. If an image won't fit, break the page before the image paragraph.
**Warning signs:** If footnote images still appear at wrong size after the fix, this function is the likely culprit. **Warning signs:** Pages with only 1-2 lines of text followed by a large gap.
### Pitfall 5: Cache Thread Safety
**What goes wrong:** Concurrent reads/writes to the cache directory cause file corruption or crashes.
**Why it happens:** `builder.build()` runs on a background queue, and multiple `paginate()` calls can overlap (pagination token pattern handles cancellation but not serialization).
**How to avoid:** Use a serial dispatch queue for all cache operations, or use `NSFileCoordinator` for file-level coordination.
**Warning signs:** Intermittent crashes on `NSKeyedUnarchiver.unarchiveObject`.
## Code Examples ## Code Examples
### WXRead Reference: _WRPostProcessElementTree (Objective-C) ### Cache Key Construction
```objc
// Source: Doc/WXRead/decompiled/WREpubTypesetter.m §672-701
if (element.textAttachment) {
DTTextAttachment *attachment = element.textAttachment;
CGSize maxSize = CGSizeMake(1080, 1920);
NSNumber *maxW = options[@"maxImageWidth"];
if (maxW) maxSize.width = maxW.floatValue;
if (attachment.originalSize.width > maxSize.width) {
CGFloat scale = maxSize.width / attachment.originalSize.width;
attachment.displaySize = CGSizeMake(
attachment.originalSize.width * scale,
attachment.originalSize.height * scale
);
}
element.textAttachment.attributes = @{
WREpubTypesetterVerticalCenterStyleAttribute: @(2)
};
[element addClass:@"bodyPic"];
}
```
**Key observations:**
1. Max size is applied to ALL images, unconditionally (no class check)
2. Only width is checked against max (`originalSize.width > maxSize.width`), not height
3. `bodyPic` class is auto-added to every image
4. `wr-vertical-center-style: 2` is set on every image
### DTCoreText's setDisplaySize:withMaxDisplaySize: (Objective-C)
```objc
// Source: ReadViewDemo/Pods/DTCoreText/Core/Source/DTTextAttachment.m §216-245
- (void)setDisplaySize:(CGSize)displaySize withMaxDisplaySize:(CGSize)maxDisplaySize {
if (_originalSize.width!=0 && _originalSize.height!=0) {
if (displaySize.width==0 && displaySize.height==0) {
displaySize = _originalSize;
} else if (displaySize.width==0 && displaySize.height!=0) {
CGFloat factor = _originalSize.height / displaySize.height;
displaySize.width = round(_originalSize.width / factor);
} else if (displaySize.width!=0 && displaySize.height==0) {
CGFloat factor = _originalSize.width / displaySize.width;
displaySize.height = round(_originalSize.height / factor);
}
}
if (maxDisplaySize.width>0 && maxDisplaySize.height>0) {
if (maxDisplaySize.width < displaySize.width || maxDisplaySize.height < displaySize.height) {
displaySize = DTCGSizeThatFitsKeepingAspectRatio(displaySize, maxDisplaySize);
}
}
}
```
**Key observation for footnote width-only approach:** When `displaySize.width != 0` and `displaySize.height == 0`, DTCoreText calculates height from the aspect ratio. This means setting only `width:1em` in CSS (which translates to a display width) will automatically produce correct height. This confirms D-07 is safe.
### Recommended: Updated prepareHTMLElementForReaderRendering
```swift ```swift
// Source: Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift §217-262 // Based on WXRead WRChapterPageCount.currentCacheKeyWithBookId:
static func prepareHTMLElementForReaderRendering( // Source: Doc/WXRead/decompiled/WRChapterPageCount.m lines 64-90
_ element: DTHTMLElement, import CryptoKit
style: RDEPUBTextRenderStyle
) {
guard let attachment = element.textAttachment else { return }
// --- NEW: Unified max image size (WXRead alignment) --- struct RDEPUBTextBookCacheKey: Hashable {
let maxImageSize = CGSize(width: 1080, height: 1920) let bookID: String
let originalSize = attachment.originalSize let fontSize: CGFloat
if originalSize.width > 0, originalSize.height > 0, let lineHeightMultiple: CGFloat
(originalSize.width > maxImageSize.width || originalSize.height > maxImageSize.height) { let contentInsets: UIEdgeInsets
let scale = min(maxImageSize.width / originalSize.width, let pageSize: CGSize
maxImageSize.height / originalSize.height) let schemaVersion: Int // bump when pagination logic changes
attachment.displaySize = CGSize(
width: round(originalSize.width * scale), var filename: String {
height: round(originalSize.height * scale) let components = [
) bookID,
String(format: "%.1f", fontSize),
String(format: "%.2f", lineHeightMultiple),
String(format: "%.0f", contentInsets.top),
String(format: "%.0f", contentInsets.left),
String(format: "%.0f", contentInsets.bottom),
String(format: "%.0f", contentInsets.right),
String(format: "%.0f", pageSize.width),
String(format: "%.0f", pageSize.height),
"v\(schemaVersion)"
]
let raw = components.joined(separator: "_")
let hash = SHA256.hash(data: Data(raw.utf8))
return hash.map { String(format: "%02x", $0) }.joined() + ".cache"
}
}
```
### NSKeyedArchiver Serialization for RDEPUBTextBook
```swift
// NSAttributedString supports NSCoding via NSKeyedArchiver
// RDEPUBTextBook/Chapter/Page need NSCoding conformance or wrapper
// Option A: Store as NSCoding-compatible wrapper
final class RDEPUBTextBookArchive: NSObject, NSCoding {
let chapters: [RDEPUBTextChapterArchive]
init(chapters: [RDEPUBTextChapterArchive]) { self.chapters = chapters }
required init?(coder: NSCoder) {
guard let chapters = coder.decodeObject(forKey: "chapters") as? [RDEPUBTextChapterArchive] else { return nil }
self.chapters = chapters
} }
let lowercasedClasses = (((attachment.attributes["class"] as? String) func encode(with coder: NSCoder) {
?? (element.attributes["class"] as? String) ?? "")).lowercased() coder.encode(chapters, forKey: "chapters")
let lowercasedPath = attachment.contentURL?.lastPathComponent.lowercased()
?? ((attachment.attributes["src"] as? String)
?? (element.attributes["src"] as? String) ?? "").lowercased()
let pointSize = max(element.fontDescriptor.pointSize, style.font.pointSize)
// Footnote: width-only sizing, no explicit height (WXRead alignment)
if lowercasedClasses.contains("qqreader-footnote") || lowercasedPath == "note.png" {
attachment.verticalAlignment = .center
element.displayStyle = .inline
// Note: NO height calculation here. Width is set via HTML preprocessing
// (normalizeAttachmentHTMLMarkers adds width:1em). DTCoreText calculates
// height from aspect ratio automatically.
if !didLogFootnoteAttachment {
didLogFootnoteAttachment = true
print("[EPUB][Attachment] footnote classes=\(lowercasedClasses) path=\(lowercasedPath) original=\(string(from: originalSize)) display=\(string(from: attachment.displaySize)) font=\(pointSize)")
} }
return
} }
// Cover: screen-size base, already constrained by unified max above // Each RDEPUBTextChapterArchive stores:
if lowercasedClasses.contains("rd-front-cover-image") || lowercasedPath == "cover.jpg" { // - attributedContent as NSData (NSKeyedArchiver)
let maxSize = UIScreen.main.bounds.insetBy(dx: 20, dy: 28).size // - page ranges as [[location, length]]
let currentOriginalSize = attachment.originalSize // - metadata as JSON Data
if currentOriginalSize.width > 0, currentOriginalSize.height > 0 { ```
let scale = min(maxSize.width / currentOriginalSize.width, maxSize.height / currentOriginalSize.height)
if scale < 1.0 { // Only re-scale if larger than screen ### Performance Sample Collection
attachment.displaySize = CGSize( ```swift
width: round(currentOriginalSize.width * scale), // No existing pattern in codebase -- standard iOS approach
height: round(currentOriginalSize.height * scale) final class RDEPUBTextPerformanceSampler {
) struct Sample {
let chapterHref: String
let renderDuration: TimeInterval
let paginateDuration: TimeInterval
let pageCount: Int
let attributedStringLength: Int
let cacheHit: Bool
} }
} else {
attachment.displaySize = CGSize(width: round(maxSize.width), height: round(maxSize.height)) private(set) var samples: [Sample] = []
}
attachment.verticalAlignment = .baseline func record(_ sample: Sample) {
element.displayStyle = .block samples.append(sample)
if !didLogCoverAttachment { print("[PERF] \(sample.chapterHref): render=\(String(format: "%.1f", sample.renderDuration*1000))ms paginate=\(String(format: "%.1f", sample.paginateDuration*1000))ms pages=\(sample.pageCount) cache=\(sample.cacheHit ? "HIT" : "MISS")")
didLogCoverAttachment = true
print("[EPUB][Attachment] cover classes=\(lowercasedClasses) path=\(lowercasedPath) original=\(string(from: currentOriginalSize)) display=\(string(from: attachment.displaySize))")
} }
func summary() -> String {
let totalRender = samples.reduce(0) { $0 + $1.renderDuration }
let totalPaginate = samples.reduce(0) { $0 + $1.paginateDuration }
let hitRate = samples.filter(\.cacheHit).count
return "[PERF] chapters=\(samples.count) render=\(String(format: "%.0f", totalRender*1000))ms paginate=\(String(format: "%.0f", totalPaginate*1000))ms cacheHits=\(hitRate)/\(samples.count)"
} }
} }
``` ```
## State of the Art ## State of the Art
| Old Approach | Current Approach | When Changed | Impact | | Old Approach (assumed) | Current Approach (actual) | When Changed | Impact |
|--------------|------------------|--------------|--------| |------------------------|---------------------------|--------------|--------|
| DTMaxImageSize = screen bounds | DTMaxImageSize = 1080x1920 | This phase | All images get correct ceiling at creation time | | `ss_pageRanges` for pagination | `rd_paginatedFrames` via `RDEPUBTextLayouter` | Phase 7 | 4-level semantic break engine already active |
| Footnote: explicit height calc (0.54*pt in renderer, 0.14*pt in view) | Footnote: width-only from CSS, height auto-calculated | This phase | Consistent with WXRead, simpler, no conflicting overrides | | No CSS `<link>` handling | `inlineLinkedStyleSheets()` in renderer support | Phase 7 or earlier | EPUB external stylesheets already resolved |
| Cover: screen-size only, no unified max | Cover: screen-size + unified max ceiling | This phase | Rare edge case: very large cover images get proper constraint | | Flat CSS injection | 5-layer cascade via `RDEPUBTextStyleSheetPackage` | Phase 7 | default/replace/dark/epub/user layers already built |
| qrbodyPic: no max constraint | qrbodyPic: constrained by unified max | This phase | Fixes height overflow for image-heavy chapters | | No page metadata | `RDEPUBTextPageMetadata` on every page | Phase 7 | breakReason, blockKinds, semanticHints already available |
**Deprecated/outdated:** **Deprecated/outdated in CONTEXT.md:**
- `pointSize * 0.54` footnote height calculation in `prepareHTMLElementForReaderRendering` — replaced by width-only approach - D-01 (integrate layouter): Already done. `RDEPUBTextBookBuilder` line 196 calls `content.rd_paginatedFrames(size:)`.
- `pointSize * 0.14` footnote height override in `normalizeInlineAttachments` — removed entirely - D-02 (expose metadata): Already done. `RDEPUBTextPage.metadata` is populated from `frame.metadata`.
- `pointSize * 0.14` footnote height override in `normalizeAttachmentDisplayIfNeeded` — removed entirely - D-04 (CSS `<link>` inline): Already done. `RDEPUBTextRendererSupport.inlineLinkedStyleSheets()` handles this.
- D-05 (5-layer cascade): Already done. `makeStyleSheetLayers()` produces the 5-layer package.
## Assumptions Log ## Assumptions Log
| # | Claim | Section | Risk if Wrong | | # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------| |---|-------|---------|---------------|
| A1 | DTCoreText correctly calculates height from aspect ratio when only width is set on an attachment (verified via DTTextAttachment.m source code reading) | Pattern 2: Footnote Width-Only | LOW — code at line 231-236 explicitly handles this case | | A1 | `NSKeyedArchiver` can serialize `NSAttributedString` with DTCoreText custom attributes | Cache serialization | Cache would fail silently; fallback to re-render every time |
| A2 | WXRead's `_WRPostProcessElementTree` checks only width, not height, against maxSize (verified via decompiled source) | WXRead Reference | LOW — source code at line 680 confirms `originalSize.width > maxSize.width` | | A2 | `CFAbsoluteTimeGetCurrent()` has sufficient precision for millisecond-level timing | Performance sampling | Sub-millisecond operations may show as 0ms |
| A3 | The `normalizeAttachmentDisplayIfNeeded` function at line 467-505 of RDEPUBTextRendererSupport.swift needs its footnote height override removed (per D-08/D-09) | Pitfall 4 | MEDIUM — if not removed, it will override the willFlushCallback changes | | A3 | Cache key schema version bump is sufficient for invalidation on code changes | Cache invalidation | Stale cache could persist if version is forgotten |
| A4 | CTFrame line origins can detect orphan/widow conditions | Pagination quality | Orphan/widow control would need different approach |
## Open Questions (RESOLVED) ## Open Questions
1. **(RESOLVED) Should we also update `DTMaxImageSize` in `dtOptions()` from screen bounds to 1080x1920?** 1. **Cache storage format: NSKeyedArchiver vs decomposed JSON?**
- What we know: `dtOptions()` currently uses `UIScreen.main.bounds.insetBy(dx: 20, dy: 28).size` (~353x757) - What we know: `NSAttributedString` supports `NSCoding`. `RDEPUBTextBook` does NOT conform to `NSCoding` or `Codable`.
- What's unclear: Whether changing this affects non-image attachments or has side effects - What's unclear: Whether DTCoreText custom attributes survive `NSKeyedArchiver` round-trip.
- Resolution: Yes, update to `CGSize(width: 1080, height: 1920)`. This is the first-pass constraint at attachment creation time. The willFlushCallback block is the authoritative second pass. Aligning both to the same max size eliminates confusion. Plan 08-02 Task 1 includes this change. - Recommendation: Test `NSKeyedArchiver` first. If custom attributes are lost, fall back to storing raw HTML + re-render parameters (slower cache load but more robust).
2. **(RESOLVED) Should we auto-add `bodyPic` class to all images like WXRead does?** 2. **Should cache store full attributed content or just page ranges?**
- What we know: WXRead adds `bodyPic` class to every image in `_WRPostProcessElementTree` - What we know: Full `RDEPUBTextBook` includes `NSAttributedString` per page (memory-heavy). Page ranges alone would require re-slicing on load.
- What's unclear: Whether ReadViewSDK's CSS rules depend on `bodyPic` being present for proper centering - What's unclear: Memory impact of caching full attributed strings for large books.
- Resolution: Not required for the immediate fix. The `replaceCSS` already handles `img, svg, video, canvas` with `max-width: 100%; height: auto`, and specific selectors like `.bodyPic img` for centering. Adding the class would be a nice-to-have for full WXRead alignment but is not blocking. Deferred to future WXRead alignment work. - Recommendation: Cache full `RDEPUBTextBook` (matches D-03 decision). Add memory pressure monitoring.
3. **(RESOLVED) Should `normalizeAttachmentDisplayIfNeeded` (line 467-505) be updated or removed entirely?** 3. **Orphan/widow control: how aggressive?**
- What we know: This function runs in `normalizeReadingAttributes` (post-willFlushCallback) and overrides footnote/cover sizes - What we know: WXRead has `avoidOrphans`/`avoidWidows` in `WRCoreTextLayoutConfig`. Current `RDEPUBTextLayouter` has no such config.
- What's unclear: Whether removing the footnote path is sufficient, or whether the cover path should also be simplified - What's unclear: Whether to add a configuration struct or hard-code reasonable defaults.
- Resolution: Remove the footnote height override path (D-08). The cover path can remain as a fallback since it produces similar results to the `prepareHTMLElementForReaderRendering` cover block. Plan 08-02 Task 1 includes the footnote path removal. - Recommendation: Add `RDEPUBTextLayoutConfig` with `avoidOrphans: Bool = true` and `avoidWidows: Bool = true`. Default to enabled.
## Environment Availability ## Environment Availability
> No external dependencies required — all changes are to existing Swift source files.
| Dependency | Required By | Available | Version | Fallback | | Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------| |------------|------------|-----------|---------|----------|
| Xcode / Swift compiler | Build & run | ✓ | (project toolchain) | — | | Xcode / iOS SDK | Build & run | ✓ | (system) | -- |
| DTCoreText (CocoaPod) | Image attachment API | ✓ | (project-local) | — | | DTCoreText | HTML rendering | ✓ | (existing dependency) | -- |
| Demo EPUB sample | Visual verification | ✓ | 宝山辽墓材料与释读 | — | | CoreText | Pagination | ✓ | (system framework) | -- |
| CryptoKit | SHA256 for cache key | ✓ | iOS 13+ | CommonCrypto |
| FileManager | Cache directory | ✓ | (system framework) | -- |
**Missing dependencies with no fallback:** None **Missing dependencies with no fallback:** None
**Missing dependencies with fallback:** None **Missing dependencies with fallback:** None
## Validation Architecture ## Validation Architecture
### Test Framework ### Test Framework
| Property | Value | | Property | Value |
|----------|-------| |----------|-------|
| Framework | None (no unit test target for EPUBTextRendering) | | Framework | None detected -- no test target in project |
| Config file | none | | Config file | none |
| Quick run command | `xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16'` | | Quick run command | N/A |
| Full suite command | Same as quick run (build-only verification) | | Full suite command | N/A |
### Phase Requirements -> Test Map
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? | | Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------| |--------|----------|-----------|-------------------|-------------|
| QUAL-02 | Images don't overflow page | manual visual | Build + run demo + navigate to 图文章节 | N/A | | QUAL-01 | Cache hit skips rebuild | manual/runtime | Print cache HIT/MISS in console | N/A |
| QUAL-03 | Image sizing rules are verifiable | code review | Verify max-size constant is 1080x1920 | N/A | | QUAL-02 | avoidPageBreakInside enforced | manual/runtime | Inspect page diagnostics for semanticBoundary breaks | N/A |
| QUAL-04 | No performance degradation | manual timing | Compare pagination time before/after | N/A | | QUAL-03 | Image sizing rules applied | manual/runtime | Verify image dimensions in diagnostic output | N/A |
| QUAL-04 | Performance samples output | manual/runtime | Check console for [PERF] lines | N/A |
### Sampling Rate ### Sampling Rate
- **Per task commit:** `xcodebuild build` (compilation check) - **Per task commit:** Build succeeds (`build_sim`)
- **Per wave merge:** Build + run demo + visual inspection of 图文章节 - **Per wave merge:** Runtime log includes [PERF] summary and cache HIT/MISS
- **Phase gate:** Visual verification that all three image types display correctly - **Phase gate:** Full book builds with cache, second open shows HIT; complex chapter pagination quality visibly improved
### Wave 0 Gaps ### Wave 0 Gaps
- [ ] No automated image size assertion tests — manual visual verification required - [ ] No test framework exists. All verification is manual/runtime via console output and simulator build.
- [ ] No snapshot/regression test infrastructure for image rendering - [ ] If automated tests desired, would need to create test target first (out of scope for Phase 8).
## Security Domain
No new security concerns introduced. Cache files are stored in the app's caches directory (sandboxed). No network requests, no user data exposure.
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | N/A |
| V3 Session Management | no | N/A |
| V4 Access Control | no | N/A |
| V5 Input Validation | no | N/A |
| V6 Cryptography | no | SHA256 is one-way hash for cache key, not security-sensitive |
## Sources ## Sources
### Primary (HIGH confidence) ### Primary (HIGH confidence)
- `Doc/WXRead/decompiled/WREpubTypesetter.m` §672-701 — `_WRPostProcessElementTree` max size logic - Project source code: `Sources/RDReaderView/EPUBTextRendering/*.swift` -- direct code inspection
- `Doc/WXRead/decompiled/WRCoreTextLayoutFrame.m` §615-635 — cover image drawing - `Doc/WXRead/decompiled/WRChapterPageCount.h/.m` -- cache key pattern reference
- `Doc/WXRead/resources/css/replace.css` §91-103 — `.bodyPic`, `.qrbodyPic`, `.qqreader-footnote` CSS - `Doc/WXRead/decompiled/WRCoreTextLayouter.h` -- layout config reference
- `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift` §217-262, §302-343, §345-404, §467-505 — current implementation - `Doc/WXRead/decompiled/WRCoreTextLayoutFrame.h/.m` -- avoidPageBreakInside reference
- `ReadViewDemo/Pods/DTCoreText/Core/Source/DTTextAttachment.m` §216-245 — `setDisplaySize:withMaxDisplaySize:` implementation - `Doc/WXRead/resources/css/replace.css` -- CSS rule reference
- `ReadViewDemo/Pods/DTCoreText/Core/Source/DTTextAttachment.h``originalSize`, `displaySize`, `verticalAlignment` API
### Secondary (MEDIUM confidence) ### Secondary (MEDIUM confidence)
- `ReadViewDemo/ReadViewDemo/book/宝山辽墓材料与释读_副本/OEBPS/Text/Chapter_5.xhtml``qrbodyPic` HTML structure - `Doc/架构对比分析_WXRead_vs_ReadViewSDK.md` -- gap analysis (some gaps already closed)
- `ReadViewDemo/ReadViewDemo/book/宝山辽墓材料与释读_副本/OEBPS/Text/cover.xhtml` — cover HTML structure
- `ReadViewDemo/ReadViewDemo/book/宝山辽墓材料与释读_副本/OEBPS/Styles/stylesheets.css` — EPUB's own CSS rules ### Tertiary (LOW confidence)
- A1 (NSKeyedArchiver + DTCoreText attributes): Needs runtime verification
## Metadata ## Metadata
**Confidence breakdown:** **Confidence breakdown:**
- Standard stack: HIGH — DTCoreText is the existing project dependency, API is well-understood - Standard stack: HIGH -- no new dependencies, all system/existing frameworks
- Architecture: HIGH — pipeline is clearly documented, code paths are traced - Architecture: HIGH -- integration points clearly visible in code
- Pitfalls: HIGH — all pitfalls identified from direct source code reading - Pitfalls: HIGH -- serialization issues are well-understood Cocoa patterns
- Cache design: MEDIUM -- NSKeyedArchiver with DTCoreText custom attributes needs runtime test
**Research date:** 2026-05-23 **Research date:** 2026-05-23
**Valid until:** 2026-06-23 (stable — DTCoreText and project code are not changing rapidly) **Valid until:** 2026-06-23 (30 days -- stable codebase, incremental changes)