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
**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 `<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)
### 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 `<link>` 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)
</user_constraints>
- 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>
## 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 |
</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
### 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 <img> 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 <link> 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 <img> 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)
// 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)")
func encode(with coder: NSCoder) {
coder.encode(chapters, forKey: "chapters")
}
return
}
// 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)
)
// 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
}
} 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 `<link>` 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 `<link>` 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)