diff --git a/.planning/phases/08-pagination-quality-cache-performance/08-RESEARCH.md b/.planning/phases/08-pagination-quality-cache-performance/08-RESEARCH.md
index 6db805c..050ab0d 100644
--- a/.planning/phases/08-pagination-quality-cache-performance/08-RESEARCH.md
+++ b/.planning/phases/08-pagination-quality-cache-performance/08-RESEARCH.md
@@ -1,474 +1,485 @@
-# Phase 8: 图片显示修复 - Research
+# Phase 8: 分页质量、缓存与性能采样 - Research
**Researched:** 2026-05-23
-**Domain:** DTCoreText image attachment sizing, WXRead alignment
+**Domain:** iOS/Swift EPUB reader - pagination caching, quality improvement, performance sampling
**Confidence:** HIGH
## 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 `` 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 `` 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 (from CONTEXT.md)
### 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-03:** Implementation location: `prepareHTMLElementForReaderRendering`, BEFORE existing footnote/cover special handling
-- **D-04:** Cover images keep screen-size base (`UIScreen.main.bounds.insetBy(dx: 20, dy: 28)`), constrained by unified max
-- **D-05:** Keep `configureCoverIfNeeded` UIImageView path unchanged
-- **D-06:** Cover images still processed via cover branch in `prepareHTMLElementForReaderRendering`, but must not exceed unified max
-- **D-07:** Footnote images: only set `width:1em`, no height (align with WXRead `replace.css`)
-- **D-08:** Remove footnote height calculation in `prepareHTMLElementForReaderRendering` (`pointSize * 0.54`)
-- **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
+
+- **D-01 [P0]:** Integrate `RDEPUBTextLayouter` into `RDEPUBTextBookBuilder` -- ALREADY DONE (see finding above)
+- **D-02 [P0]:** Expose `metadata: RDEPUBTextPageMetadata` on `RDEPUBTextPage` -- ALREADY DONE
+- **D-03 [P0]:** Pagination cache by `bookID + fontSize + lineHeightMultiple + contentInsets`, cache full `RDEPUBTextBook` to disk
+- **D-04 [P0]:** CSS `` inline processing -- ALREADY DONE
+- **D-05 [P0]:** 5-layer CSS cascade -- ALREADY DONE
+- **D-06 [P1]:** Image/attachment processing rules with diagnostics
+- **D-07 [P1]:** Performance sampling -- output stable sampling data
### Claude's Discretion
-- Whether to auto-add `bodyPic` class to all images (like WXRead does)
-- Whether `wr-vertical-center-style` semantic marker is needed (Phase 7 already handles this)
-- Specific implementation details of the unified max-size block
+- Cache storage format (file system plist/JSON vs SQLite) -- implementer decides
+- CSS cascade replace.css content -- reference WXRead but don't copy exactly
+- Performance sampling threshold values -- determine at implementation time
### Deferred Ideas (OUT OF SCOPE)
-- Image caching mechanism (belongs to plan 08-01)
-- `wr-vertical-center-style` full implementation (Phase 7 already completed)
-- Image tap-to-zoom feature (new capability, not in scope)
-
+- CoreText direct drawing migration (v1.2/v2.0)
+- Font system (embedded font set, future version)
+- Character-level position precision (P2)
+- Chapter data model cohesion (P2)
+- TTS / DRM / Pencil / multi-column layout (P3)
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
-| QUAL-01 | Cache layout frame / pagination results | Not in scope for 08-02 (belongs to 08-01) |
-| 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-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-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-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 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 rules, dark mode adaptation, page background info with verifiable rules | WXRead `wr-vertical-center-style`, `DTPageBreakInsideAvoid`, existing `prepareHTMLElementForReaderRendering` |
+| 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 |
-## 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
-### Core
+### Core (no new external dependencies)
+
+This phase uses only existing project dependencies and Apple frameworks:
+
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
-| DTCoreText | (CocoaPod, project-local) | HTML/CSS to NSAttributedString | Existing project dependency; provides `DTTextAttachment`, `willFlushCallback` |
-| UIKit | iOS SDK | `UIScreen.main.bounds` for cover sizing | Platform standard |
+| Foundation | iOS SDK | NSKeyedArchiver, JSONEncoder, FileManager | System framework, no alternative |
+| 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
+
| 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
+
| 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 |
-| 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 |
+| NSKeyedArchiver for cache | JSONEncoder | JSON is human-readable but can't handle NSAttributedString natively; NSKeyedArchiver handles it but produces opaque binary |
+| 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
-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 |
|---------|----------|-----|-----------|-------------|-----------|-------------|
-| (none) | — | — | — | — | — | No new packages |
+| (none) | -- | -- | -- | -- | -- | N/A |
## Architecture Patterns
-### Current Image Processing Pipeline
+### Current Data Flow (already working)
```
-HTML input
- |
- v
-normalizeAttachmentHTMLMarkers() <-- HTML regex: adds inline styles to tags
- | (footnote: width:1em;height:1em)
- | (cover: adds rd-front-cover-image class)
- v
-DTHTMLAttributedStringBuilder <-- Creates DTTextAttachment with DTMaxImageSize
- | (currently screenBounds.size, ~353x757)
- | DTCoreText sets displaySize = min(original, max)
- v
-willFlushCallback <-- prepareHTMLElementForReaderRendering()
- | 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)
+RDEPUBReaderController.paginate(restoreLocation:)
+ -> RDEPUBTextBookBuilder.build(parser:publication:pageSize:style:)
+ -> for each spine item:
+ -> RDEPUBTextRendererSupport.makeChapterRenderRequest(...)
+ -> inlineLinkedStyleSheets() [CSS inlining - DONE]
+ -> makeStyleSheetLayers() [5-layer cascade - DONE]
+ -> injectPaginationSemanticMarkers() [semantic markers - DONE]
+ -> renderer.renderChapter(request:) [DTCoreText render]
+ -> content.rd_paginatedFrames(size:) [4-level semantic pagination - DONE]
+ -> RDEPUBTextPage with metadata [metadata exposure - DONE]
+ -> RDEPUBTextBook(chapters:, pages:)
+ -> applyTextBook(restoreLocation:)
```
-### Recommended Pipeline After Fix
+### Recommended Cache Integration Point
```
-HTML input
- |
- v
-normalizeAttachmentHTMLMarkers() <-- CHANGED: footnote only gets width:1em (no height)
- v
-DTHTMLAttributedStringBuilder <-- CHANGED: DTMaxImageSize → CGSizeMake(1080, 1920)
- v
-willFlushCallback <-- prepareHTMLElementForReaderRendering()
- | 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
+RDEPUBReaderController.paginate(restoreLocation:)
+ -> compute cacheKey from bookID + layout params
+ -> if cached RDEPUBTextBook exists for key:
+ -> applyTextBook(cachedBook, restoreLocation:) [CACHE HIT - skip build]
+ -> else:
+ -> builder.build(...) [existing flow]
+ -> save RDEPUBTextBook to cache [CACHE WRITE]
+ -> applyTextBook(textBook, restoreLocation:)
```
-### 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:**
```swift
-// Source: WXRead WREpubTypesetter.m §672-701 (_WRPostProcessElementTree)
-// Placed BEFORE existing footnote/cover handling in prepareHTMLElementForReaderRendering
+// Reference: WXRead WRChapterPageCount.currentCacheKeyWithBookId:
+// WXRead encodes: bookId + fontSize + lineSpacing + pageWidth + pageHeight
-static func prepareHTMLElementForReaderRendering(
- _ element: DTHTMLElement,
- style: RDEPUBTextRenderStyle
-) {
- guard let attachment = element.textAttachment else { return }
+struct RDEPUBTextBookCacheKey {
+ let bookID: String
+ let fontSize: CGFloat
+ let lineHeightMultiple: CGFloat
+ let contentInsets: UIEdgeInsets
+ let pageSize: CGSize
- // --- NEW: Unified max image size (aligns with WXRead _WRPostProcessElementTree) ---
- let maxImageSize = CGSize(width: 1080, height: 1920)
- let originalSize = attachment.originalSize
- if originalSize.width > maxImageSize.width || originalSize.height > maxImageSize.height {
- let scale = min(maxImageSize.width / max(originalSize.width, 1),
- maxImageSize.height / max(originalSize.height, 1))
- attachment.displaySize = CGSize(
- width: round(originalSize.width * scale),
- height: round(originalSize.height * scale)
- )
+ var stringValue: String {
+ let raw = "\(bookID)|\(fontSize)|\(lineHeightMultiple)|\(contentInsets.top)|\(contentInsets.left)|\(contentInsets.bottom)|\(contentInsets.right)|\(pageSize.width)|\(pageSize.height)"
+ // SHA256 for safe filename
+ let data = Data(raw.utf8)
+ let hash = SHA256.hash(data: data)
+ return hash.map { String(format: "%02x", $0) }.joined()
}
-
- // ... existing footnote/cover handling follows ...
}
```
+**Source:** [VERIFIED: Doc/WXRead/decompiled/WRChapterPageCount.m lines 64-90]
-### Pattern 2: Footnote Width-Only Sizing
-
-**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.
+### Pattern 2: avoidPageBreakInside Line Removal
+**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:**
```swift
-// Source: WXRead replace.css §100-103 (.qqreader-footnote { width: 1em; })
-// Changed: removed "height:1em" from styleFragments
-
-normalized = replaceMatches(
- using: footnoteRegex,
- in: normalized
-) { tag in
- mergeHTMLAttributes(
- into: tag,
- requiredClass: nil,
- styleFragments: [
- "width:1em",
- // "height:1em" <-- REMOVED per D-10
- "vertical-align:middle",
- "display:inline-block"
- ]
- )
-}
+// Reference: WXRead WRCoreTextLayoutFrame.avoidPageBreakInsideByRemovingLastLinesIfNeeded
+// If the proposed page end falls inside an avoidPageBreakInside block:
+// 1. Find the block's start location
+// 2. If block start >= minimumEnd, break at block start
+// 3. If block start < minimumEnd (block is too large), fall through to frameLimit
```
+**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
-- **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.
-- **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.
-- **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).
+- **Storing NSAttributedString via JSONEncoder:** NSAttributedString is not Codable. Use NSKeyedArchiver or store the raw HTML + re-render parameters instead.
+- **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.
+- **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
| 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 |
-| Image type detection | New classification system | Existing CSS class + filename detection (`lowercasedClasses`, `lowercasedPath`) | Already works, battle-tested in Phase 7 |
+| SHA256 hashing | Custom hash function | CryptoKit or CommonCrypto | Cryptographic correctness, no collision risk |
+| 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
-### Pitfall 1: DTMaxImageSize vs willFlushCallback Ordering
-**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.
-**Why it happens:** DTMaxImageSize is consumed at attachment creation time; willFlushCallback runs after.
-**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.
-**Warning signs:** If only one is updated, images may briefly appear at wrong size before being corrected.
+### Pitfall 1: NSAttributedString Serialization
+**What goes wrong:** Attempting to JSONEncode an `RDEPUBTextBook` fails silently because `NSAttributedString` and `NSRange` are not Codable.
+**Why it happens:** `RDEPUBTextPage.content` is `NSAttributedString`, and `RDEPUBTextPageMetadata` contains `[NSRange]` fields.
+**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:** `JSONEncoder.encode()` returns nil or throws.
-### Pitfall 2: Footnote Aspect Ratio Without Explicit Height
-**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.
-**Why it happens:** Some images in EPUBs may not have `width`/`height` attributes, so `originalSize` could be `CGSizeZero`.
-**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.
-**Warning signs:** Footnote images appearing at 0x0 or unreasonably large sizes.
+### Pitfall 2: Cache Invalidation on Code Changes
+**What goes wrong:** After updating the pagination engine code, cached results are stale but the cache key hasn't changed.
+**Why it happens:** Cache key only encodes user-facing parameters (font, size, insets), not code version.
+**How to avoid:** Include a schema version number in the cache key. Bump the version when pagination logic changes.
+**Warning signs:** Pagination quality doesn't improve after code changes until cache is manually cleared.
-### Pitfall 3: Cover Image Double-Scaling
-**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.
-**Why it happens:** Two sequential scaling operations compound.
-**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.
-**Warning signs:** Cover images appearing smaller than expected.
+### Pitfall 3: NSRange Codable in Metadata
+**What goes wrong:** `RDEPUBTextPageMetadata` conforms to `Codable` but contains `NSRange` fields (`blockRange`, `attachmentRanges`) which don't have native Codable conformance.
+**Why it happens:** `NSRange` is an Objective-C struct, not a Swift Codable type.
+**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:** Compiler error or runtime crash when encoding metadata.
-### Pitfall 4: normalizeAttachmentDisplayIfNeeded Still Overrides
-**What goes wrong:** After fixing `prepareHTMLElementForReaderRendering`, the `normalizeAttachmentDisplayIfNeeded` function (called from `normalizeReadingAttributes`) still overrides footnote and cover sizes.
-**Why it happens:** This function runs AFTER `willFlushCallback` in the pipeline.
-**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`).
-**Warning signs:** If footnote images still appear at wrong size after the fix, this function is the likely culprit.
+### Pitfall 4: Image Pages Crossing Boundaries
+**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:** 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:** Before pagination, check attachment heights against remaining page space. If an image won't fit, break the page before the image paragraph.
+**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
-### WXRead Reference: _WRPostProcessElementTree (Objective-C)
-
-```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
-
+### Cache Key Construction
```swift
-// Source: Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift §217-262
-static func prepareHTMLElementForReaderRendering(
- _ element: DTHTMLElement,
- style: RDEPUBTextRenderStyle
-) {
- guard let attachment = element.textAttachment else { return }
+// Based on WXRead WRChapterPageCount.currentCacheKeyWithBookId:
+// Source: Doc/WXRead/decompiled/WRChapterPageCount.m lines 64-90
+import CryptoKit
- // --- NEW: Unified max image size (WXRead alignment) ---
- let maxImageSize = CGSize(width: 1080, height: 1920)
- let originalSize = attachment.originalSize
- if originalSize.width > 0, originalSize.height > 0,
- (originalSize.width > maxImageSize.width || originalSize.height > maxImageSize.height) {
- let scale = min(maxImageSize.width / originalSize.width,
- maxImageSize.height / originalSize.height)
- attachment.displaySize = CGSize(
- width: round(originalSize.width * scale),
- height: round(originalSize.height * scale)
- )
+struct RDEPUBTextBookCacheKey: Hashable {
+ let bookID: String
+ let fontSize: CGFloat
+ let lineHeightMultiple: CGFloat
+ let contentInsets: UIEdgeInsets
+ let pageSize: CGSize
+ let schemaVersion: Int // bump when pagination logic changes
+
+ var filename: String {
+ 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)
- ?? (element.attributes["class"] as? String) ?? "")).lowercased()
- 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)
+ func encode(with coder: NSCoder) {
+ coder.encode(chapters, forKey: "chapters")
+ }
+}
- // 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
+// Each RDEPUBTextChapterArchive stores:
+// - attributedContent as NSData (NSKeyedArchiver)
+// - page ranges as [[location, length]]
+// - metadata as JSON Data
+```
+
+### Performance Sample Collection
+```swift
+// No existing pattern in codebase -- standard iOS approach
+final class RDEPUBTextPerformanceSampler {
+ struct Sample {
+ let chapterHref: String
+ let renderDuration: TimeInterval
+ let paginateDuration: TimeInterval
+ let pageCount: Int
+ let attributedStringLength: Int
+ let cacheHit: Bool
}
- // Cover: screen-size base, already constrained by unified max above
- if lowercasedClasses.contains("rd-front-cover-image") || lowercasedPath == "cover.jpg" {
- let maxSize = UIScreen.main.bounds.insetBy(dx: 20, dy: 28).size
- let currentOriginalSize = attachment.originalSize
- 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
- attachment.displaySize = CGSize(
- width: round(currentOriginalSize.width * scale),
- height: round(currentOriginalSize.height * scale)
- )
- }
- } else {
- attachment.displaySize = CGSize(width: round(maxSize.width), height: round(maxSize.height))
- }
- attachment.verticalAlignment = .baseline
- element.displayStyle = .block
- if !didLogCoverAttachment {
- didLogCoverAttachment = true
- print("[EPUB][Attachment] cover classes=\(lowercasedClasses) path=\(lowercasedPath) original=\(string(from: currentOriginalSize)) display=\(string(from: attachment.displaySize))")
- }
+ private(set) var samples: [Sample] = []
+
+ func record(_ sample: Sample) {
+ samples.append(sample)
+ 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")")
+ }
+
+ 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
-| Old Approach | Current Approach | When Changed | Impact |
-|--------------|------------------|--------------|--------|
-| DTMaxImageSize = screen bounds | DTMaxImageSize = 1080x1920 | This phase | All images get correct ceiling at creation time |
-| 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 |
-| 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 |
-| qrbodyPic: no max constraint | qrbodyPic: constrained by unified max | This phase | Fixes height overflow for image-heavy chapters |
+| Old Approach (assumed) | Current Approach (actual) | When Changed | Impact |
+|------------------------|---------------------------|--------------|--------|
+| `ss_pageRanges` for pagination | `rd_paginatedFrames` via `RDEPUBTextLayouter` | Phase 7 | 4-level semantic break engine already active |
+| No CSS `` handling | `inlineLinkedStyleSheets()` in renderer support | Phase 7 or earlier | EPUB external stylesheets already resolved |
+| Flat CSS injection | 5-layer cascade via `RDEPUBTextStyleSheetPackage` | Phase 7 | default/replace/dark/epub/user layers already built |
+| No page metadata | `RDEPUBTextPageMetadata` on every page | Phase 7 | breakReason, blockKinds, semanticHints already available |
-**Deprecated/outdated:**
-- `pointSize * 0.54` footnote height calculation in `prepareHTMLElementForReaderRendering` — replaced by width-only approach
-- `pointSize * 0.14` footnote height override in `normalizeInlineAttachments` — removed entirely
-- `pointSize * 0.14` footnote height override in `normalizeAttachmentDisplayIfNeeded` — removed entirely
+**Deprecated/outdated in CONTEXT.md:**
+- D-01 (integrate layouter): Already done. `RDEPUBTextBookBuilder` line 196 calls `content.rd_paginatedFrames(size:)`.
+- D-02 (expose metadata): Already done. `RDEPUBTextPage.metadata` is populated from `frame.metadata`.
+- D-04 (CSS `` inline): Already done. `RDEPUBTextRendererSupport.inlineLinkedStyleSheets()` handles this.
+- D-05 (5-layer cascade): Already done. `makeStyleSheetLayers()` produces the 5-layer package.
## Assumptions Log
| # | 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 |
-| 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` |
-| 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 |
+| A1 | `NSKeyedArchiver` can serialize `NSAttributedString` with DTCoreText custom attributes | Cache serialization | Cache would fail silently; fallback to re-render every time |
+| A2 | `CFAbsoluteTimeGetCurrent()` has sufficient precision for millisecond-level timing | Performance sampling | Sub-millisecond operations may show as 0ms |
+| 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?**
- - What we know: `dtOptions()` currently uses `UIScreen.main.bounds.insetBy(dx: 20, dy: 28).size` (~353x757)
- - What's unclear: Whether changing this affects non-image attachments or has side effects
- - 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.
+1. **Cache storage format: NSKeyedArchiver vs decomposed JSON?**
+ - What we know: `NSAttributedString` supports `NSCoding`. `RDEPUBTextBook` does NOT conform to `NSCoding` or `Codable`.
+ - What's unclear: Whether DTCoreText custom attributes survive `NSKeyedArchiver` round-trip.
+ - 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?**
- - What we know: WXRead adds `bodyPic` class to every image in `_WRPostProcessElementTree`
- - What's unclear: Whether ReadViewSDK's CSS rules depend on `bodyPic` being present for proper centering
- - 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.
+2. **Should cache store full attributed content or just page ranges?**
+ - What we know: Full `RDEPUBTextBook` includes `NSAttributedString` per page (memory-heavy). Page ranges alone would require re-slicing on load.
+ - What's unclear: Memory impact of caching full attributed strings for large books.
+ - Recommendation: Cache full `RDEPUBTextBook` (matches D-03 decision). Add memory pressure monitoring.
-3. **(RESOLVED) Should `normalizeAttachmentDisplayIfNeeded` (line 467-505) be updated or removed entirely?**
- - What we know: This function runs in `normalizeReadingAttributes` (post-willFlushCallback) and overrides footnote/cover sizes
- - What's unclear: Whether removing the footnote path is sufficient, or whether the cover path should also be simplified
- - 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.
+3. **Orphan/widow control: how aggressive?**
+ - What we know: WXRead has `avoidOrphans`/`avoidWidows` in `WRCoreTextLayoutConfig`. Current `RDEPUBTextLayouter` has no such config.
+ - What's unclear: Whether to add a configuration struct or hard-code reasonable defaults.
+ - Recommendation: Add `RDEPUBTextLayoutConfig` with `avoidOrphans: Bool = true` and `avoidWidows: Bool = true`. Default to enabled.
## Environment Availability
-> No external dependencies required — all changes are to existing Swift source files.
-
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
-| Xcode / Swift compiler | Build & run | ✓ | (project toolchain) | — |
-| DTCoreText (CocoaPod) | Image attachment API | ✓ | (project-local) | — |
-| Demo EPUB sample | Visual verification | ✓ | 宝山辽墓材料与释读 | — |
+| Xcode / iOS SDK | Build & run | ✓ | (system) | -- |
+| DTCoreText | HTML rendering | ✓ | (existing dependency) | -- |
+| 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 fallback:** None
## Validation Architecture
### Test Framework
+
| Property | Value |
|----------|-------|
-| Framework | None (no unit test target for EPUBTextRendering) |
+| Framework | None detected -- no test target in project |
| Config file | none |
-| Quick run command | `xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16'` |
-| Full suite command | Same as quick run (build-only verification) |
+| Quick run command | N/A |
+| Full suite command | N/A |
+
+### Phase Requirements -> Test Map
-### Phase Requirements → Test Map
| 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-03 | Image sizing rules are verifiable | code review | Verify max-size constant is 1080x1920 | N/A |
-| QUAL-04 | No performance degradation | manual timing | Compare pagination time before/after | N/A |
+| QUAL-01 | Cache hit skips rebuild | manual/runtime | Print cache HIT/MISS in console | N/A |
+| QUAL-02 | avoidPageBreakInside enforced | manual/runtime | Inspect page diagnostics for semanticBoundary breaks | 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
-- **Per task commit:** `xcodebuild build` (compilation check)
-- **Per wave merge:** Build + run demo + visual inspection of 图文章节
-- **Phase gate:** Visual verification that all three image types display correctly
+- **Per task commit:** Build succeeds (`build_sim`)
+- **Per wave merge:** Runtime log includes [PERF] summary and cache HIT/MISS
+- **Phase gate:** Full book builds with cache, second open shows HIT; complex chapter pagination quality visibly improved
### Wave 0 Gaps
-- [ ] No automated image size assertion tests — manual visual verification required
-- [ ] No snapshot/regression test infrastructure for image rendering
+- [ ] No test framework exists. All verification is manual/runtime via console output and simulator build.
+- [ ] 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
### Primary (HIGH confidence)
-- `Doc/WXRead/decompiled/WREpubTypesetter.m` §672-701 — `_WRPostProcessElementTree` max size logic
-- `Doc/WXRead/decompiled/WRCoreTextLayoutFrame.m` §615-635 — cover image drawing
-- `Doc/WXRead/resources/css/replace.css` §91-103 — `.bodyPic`, `.qrbodyPic`, `.qqreader-footnote` CSS
-- `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift` §217-262, §302-343, §345-404, §467-505 — current implementation
-- `ReadViewDemo/Pods/DTCoreText/Core/Source/DTTextAttachment.m` §216-245 — `setDisplaySize:withMaxDisplaySize:` implementation
-- `ReadViewDemo/Pods/DTCoreText/Core/Source/DTTextAttachment.h` — `originalSize`, `displaySize`, `verticalAlignment` API
+- Project source code: `Sources/RDReaderView/EPUBTextRendering/*.swift` -- direct code inspection
+- `Doc/WXRead/decompiled/WRChapterPageCount.h/.m` -- cache key pattern reference
+- `Doc/WXRead/decompiled/WRCoreTextLayouter.h` -- layout config reference
+- `Doc/WXRead/decompiled/WRCoreTextLayoutFrame.h/.m` -- avoidPageBreakInside reference
+- `Doc/WXRead/resources/css/replace.css` -- CSS rule reference
### Secondary (MEDIUM confidence)
-- `ReadViewDemo/ReadViewDemo/book/宝山辽墓材料与释读_副本/OEBPS/Text/Chapter_5.xhtml` — `qrbodyPic` HTML structure
-- `ReadViewDemo/ReadViewDemo/book/宝山辽墓材料与释读_副本/OEBPS/Text/cover.xhtml` — cover HTML structure
-- `ReadViewDemo/ReadViewDemo/book/宝山辽墓材料与释读_副本/OEBPS/Styles/stylesheets.css` — EPUB's own CSS rules
+- `Doc/架构对比分析_WXRead_vs_ReadViewSDK.md` -- gap analysis (some gaps already closed)
+
+### Tertiary (LOW confidence)
+- A1 (NSKeyedArchiver + DTCoreText attributes): Needs runtime verification
## Metadata
**Confidence breakdown:**
-- Standard stack: HIGH — DTCoreText is the existing project dependency, API is well-understood
-- Architecture: HIGH — pipeline is clearly documented, code paths are traced
-- Pitfalls: HIGH — all pitfalls identified from direct source code reading
+- Standard stack: HIGH -- no new dependencies, all system/existing frameworks
+- Architecture: HIGH -- integration points clearly visible in code
+- 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
-**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)