ReadViewSDK/.planning/phases/08-pagination-quality-cache-performance/08-02-PLAN.md
shen 86e6922f7b docs(08): create phase plan — image display fix, cache stub, perf sampling
Three plans for Phase 8 (分页质量、缓存与性能采样):
- 08-01: Pagination cache key struct and manager stub (QUAL-01)
- 08-02: Unified 1080x1920 max-size constraint for all images, footnote width-only sizing (QUAL-02, QUAL-03, QUAL-04)
- 08-03: Pagination timing instrumentation with [EPUB][Perf] diagnostic output (QUAL-04)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 18:30:56 +08:00

15 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
08-pagination-quality-cache-performance 02 execute 1
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift
Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift
Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift
false
QUAL-02
QUAL-03
QUAL-04
truths artifacts key_links
All image attachments are constrained to max 1080x1920 via aspect-ratio-preserving scaling
Footnote images use width-only sizing (width:1em in CSS), no explicit height
DTMaxImageSize in dtOptions is 1080x1920, not screen bounds
Cover images still display correctly using screen-size base, capped by unified max
qrbodyPic images no longer overflow page height
path provides contains
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift Unified max-size constraint in prepareHTMLElementForReaderRendering, cleaned-up footnote handling CGSize(width: 1080, height: 1920)
path provides contains
Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift Updated DTMaxImageSize from screen bounds to 1080x1920 CGSize(width: 1080, height: 1920)
from to via pattern
RDEPUBDTCoreTextRenderer.dtOptions DTMaxImageSize builder option DTMaxImageSize.*CGSize.*1080
from to via pattern
RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering DTTextAttachment.displaySize unified max-size scaling maxImageSize.*1080
Fix image display for all three image types (qrbodyPic, cover, footnote) by applying a unified maximum size constraint (1080x1920) aligned with WXRead's `_WRPostProcessElementTree`, and simplifying footnote sizing to width-only per WXRead's `replace.css`.

Purpose: Complex illustrated chapters currently have images that overflow pages, inconsistent footnote sizing, and no unified ceiling. This fixes QUAL-02 (pagination quality), QUAL-03 (verifiable image rules), and addresses QUAL-04 (lightweight operation, no perf impact).

Output: Updated RDEPUBTextRendererSupport.swift (main changes), RDEPUBDTCoreTextRenderer.swift (DTMaxImageSize), RDEPUBTextContentView.swift (remove footnote override).

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-pagination-quality-cache-performance/08-CONTEXT.md @.planning/phases/08-pagination-quality-cache-performance/08-RESEARCH.md

From DTCoreText (CocoaPod, DTTextAttachment):

  • attachment.originalSize: CGSize — the image's intrinsic pixel size
  • attachment.displaySize: CGSize — the size used for layout (set by code)
  • attachment.verticalAlignment: DTTextAttachmentVerticalAlignment — .center, .baseline, etc.
  • element.displayStyle: DTHTMLElementDisplayStyle — .inline, .block
  • element.fontDescriptor: UIFontDescriptor — font info from the element
  • element.textAttachment: DTTextAttachment? — the attachment on the element

From RDEPUBTextRendererSupport.swift (current implementation, lines 217-262):

static func prepareHTMLElementForReaderRendering(
    _ element: DTHTMLElement,
    style: RDEPUBTextRenderStyle
) {
    guard let attachment = element.textAttachment else { return }
    // ... class/path detection, pointSize calculation ...
    // footnote: height calc pointSize * 0.54, displaySize set
    // cover: UIScreen.main.bounds.insetBy(dx: 20, dy: 28).size scaling
}

From RDEPUBDTCoreTextRenderer.swift (current implementation, line 100):

DTMaxImageSize: NSValue(cgSize: screenBounds.size)  // ~353x757

From RDEPUBTextContentView.swift (current implementation, lines 263-282):

private func normalizeInlineAttachments(in content: NSMutableAttributedString, basePointSize: CGFloat) {
    // footnote: targetHeight = pointSize * 0.14, displaySize set
}

From RDEPUBTextRendererSupport.swift (current implementation, lines 467-505):

private static func normalizeAttachmentDisplayIfNeeded(
    in attributes: inout [NSAttributedString.Key: Any],
    font: UIFont
) {
    // footnote: targetHeight = font.pointSize * 0.14
    // cover: maxWidth = max(UIScreen.main.bounds.width - 48, font.lineHeight * 8)
}

WXRead reference (Doc/WXRead/decompiled/WREpubTypesetter.m §672-701):

  • Checks only originalSize.width > maxSize.width (not height)
  • Max size is CGSizeMake(1080, 1920)
  • Scale: maxSize.width / originalSize.width
  • Sets displaySize to scaled size
Task 1: Apply unified max-size constraint and fix footnote sizing across 3 files Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift, Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift, Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift (full file — contains prepareHTMLElementForReaderRendering at L217, replaceCSS at L302, normalizeAttachmentHTMLMarkers at L345, normalizeAttachmentDisplayIfNeeded at L467) - Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift (L90-112 — dtOptions with DTMaxImageSize) - Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift (L263-282 — normalizeInlineAttachments) - Doc/WXRead/decompiled/WREpubTypesetter.m §672-701 (reference: _WRPostProcessElementTree max size logic) - Doc/WXRead/resources/css/replace.css §91-103 (reference: .qqreader-footnote CSS) - Test: DTMaxImageSize in dtOptions() is CGSize(width: 1080, height: 1920), not screen bounds - Test: prepareHTMLElementForReaderRendering applies unified max-size scaling before any type-specific handling - Test: Footnote branch in prepareHTMLElementForReaderRendering has NO height calculation (no pointSize * 0.54) - Test: Footnote branch only sets verticalAlignment = .center and displayStyle = .inline, then returns - Test: normalizeAttachmentHTMLMarkers footnote regex adds styleFragments with "width:1em" but NOT "height:1em" - Test: normalizeInlineAttachments does NOT contain pointSize * 0.14 height override for footnotes - Test: normalizeAttachmentDisplayIfNeeded does NOT contain font.pointSize * 0.14 height override for footnotes Apply changes across three files. Each change is described with its exact location and target state.

File 1: RDEPUBDTCoreTextRenderer.swift — line 100

Change DTMaxImageSize value from NSValue(cgSize: screenBounds.size) to NSValue(cgSize: CGSize(width: 1080, height: 1920)). The screenBounds variable at line 92 can remain (used elsewhere or for future reference), but DTMaxImageSize must use the new constant. Per D-01.

File 2: RDEPUBTextRendererSupport.swift — 4 locations

Location A: prepareHTMLElementForReaderRendering (L217-262) — Add unified max-size block

Insert a new block immediately after guard let attachment = element.textAttachment else { return } (line 221), BEFORE the existing class/path detection (line 223). The block:

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)
    )
}

Per D-01, D-02, D-03. This applies to ALL images before any type-specific handling.

Location B: prepareHTMLElementForReaderRendering footnote branch (L228-241) — Remove height calculation

Current code at lines 229-233:

let targetHeight = max(round(pointSize * 0.54), 1)
let originalSize = attachment.originalSize
let aspectRatio = originalSize.height > 0 ? originalSize.width / originalSize.height : 1
let targetWidth = max(round(targetHeight * max(aspectRatio, 0.1)), 1)
attachment.displaySize = CGSize(width: targetWidth, height: targetHeight)

Replace with: remove the height/width calculation entirely. Keep only attachment.verticalAlignment = .center and element.displayStyle = .inline. The return at the end stays. Per D-07, D-08. Width is already set via HTML preprocessing (width:1em in normalizeAttachmentHTMLMarkers). DTCoreText auto-calculates height from aspect ratio when only width is set.

Also remove the pointSize local variable usage from this branch — pointSize is still used by the logging line, so keep the variable declaration but it's no longer used for sizing.

Location C: normalizeAttachmentHTMLMarkers (L359-361) — Remove height:1em from footnote style fragments

Current styleFragments at line 360-361:

"width:1em",
"height:1em",

Change to:

"width:1em",

Remove "height:1em" line. Per D-10.

Location D: normalizeAttachmentDisplayIfNeeded (L477-482) — Remove footnote height override

Lines 477-483 currently compute footnote displaySize with font.pointSize * 0.14. Remove the entire if lowercasedClasses.contains("qqreader-footnote") || lowercasedPath == "note.png" block (lines 477-483). The footnote is already handled correctly by the unified max-size + width-only CSS approach in prepareHTMLElementForReaderRendering and normalizeAttachmentHTMLMarkers. Per D-08, D-09.

Keep the cover path (lines 484-494) — it acts as a fallback and does not conflict.

File 3: RDEPUBTextContentView.swift — normalizeInlineAttachments (L263-282)

Remove the footnote height override logic inside normalizeInlineAttachments. Lines 270-278:

guard classes.contains("qqreader-footnote") || source.contains("note.png") else { return }
let pointSize = max(basePointSize, 1)
let targetHeight = max(round(pointSize * 0.14), 1)
...
attachment.displaySize = CGSize(width: targetWidth, height: targetHeight)

Replace the entire DTTextAttachment branch body with just a guard + return (skip footnote attachments entirely — they're already sized correctly from the renderer pipeline). The function should still iterate but do nothing for footnotes. Per D-09.

Build verification: After all changes, run xcodebuild build to confirm compilation. xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5 <acceptance_criteria> - RDEPUBDTCoreTextRenderer.swift: DTMaxImageSize line contains CGSize(width: 1080, height: 1920) (not screenBounds.size) - RDEPUBTextRendererSupport.swift: prepareHTMLElementForReaderRendering contains let maxImageSize = CGSize(width: 1080, height: 1920) BEFORE any class/path detection - RDEPUBTextRendererSupport.swift: prepareHTMLElementForReaderRendering footnote branch does NOT contain pointSize * 0.54 or any targetHeight/targetWidth calculation - RDEPUBTextRendererSupport.swift: prepareHTMLElementForReaderRendering footnote branch contains attachment.verticalAlignment = .center and element.displayStyle = .inline - RDEPUBTextRendererSupport.swift: normalizeAttachmentHTMLMarkers footnote styleFragments contain "width:1em" but NOT "height:1em" - RDEPUBTextRendererSupport.swift: normalizeAttachmentDisplayIfNeeded does NOT contain pointSize * 0.14 or font.pointSize * 0.14 for footnote path - RDEPUBTextContentView.swift: normalizeInlineAttachments does NOT contain pointSize * 0.14 height override for footnotes - xcodebuild build exits 0 </acceptance_criteria> All three image sizing code paths are updated: unified 1080x1920 max-size in prepareHTMLElementForReaderRendering, DTMaxImageSize set to 1080x1920, footnote height overrides removed from all three locations, footnote CSS uses width-only (width:1em), build compiles

Task 2: Visual verification of image display fix Unified image sizing constraint applied across the native text rendering pipeline: - All images constrained to max 1080x1920 (WXRead alignment) - Footnote images: width-only sizing (width:1em), height auto-calculated from aspect ratio - Cover images: screen-size base, capped by unified max - qrbodyPic images: constrained by unified max (no more height overflow)

Changed files:

  • RDEPUBDTCoreTextRenderer.swift (DTMaxImageSize)
  • RDEPUBTextRendererSupport.swift (prepareHTMLElementForReaderRendering, normalizeAttachmentHTMLMarkers, normalizeAttachmentDisplayIfNeeded)
  • RDEPUBTextContentView.swift (normalizeInlineAttachments)
  1. Build and run ReadViewDemo on simulator
  2. Open the demo book "宝山辽墓材料与释读"
  3. Navigate to the cover page — verify cover image displays correctly (not cropped, not stretched, centered)
  4. Navigate to Chapter 5 (has qrbodyPic images) — verify images display fully within page bounds, no height overflow
  5. Navigate to a chapter with footnote images — verify footnote images are inline with text, same height as text characters, not oversized
  6. Switch to dark mode — verify images still display correctly
  7. Check that page breaks around images are reasonable (no orphan images at page tops/bottoms) Type "approved" if all image types display correctly, or describe which image type has issues

<threat_model>

Trust Boundaries

Boundary Description
EPUB HTML input -> DTCoreText parser Untrusted HTML/CSS from EPUB files enters the rendering pipeline
DTCoreText attachment -> willFlushCallback Image dimensions from EPUB metadata could be arbitrary

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-08-01 Tampering Image size from EPUB metadata mitigate Unified max-size constraint (1080x1920) caps any image regardless of declared dimensions
T-08-02 Denial of Service Maliciously large images mitigate DTMaxImageSize (first pass) + willFlushCallback max-size (second pass) ensure no image exceeds 1080x1920 display size
T-08-SC Tampering npm/pip/cargo installs accept No new packages installed in this phase
</threat_model>
- xcodebuild build succeeds - Visual inspection: cover, qrbodyPic, and footnote images all display correctly - No regressions in dark mode

<success_criteria>

  • All images constrained to 1080x1920 max (QUAL-03: verifiable processing rules)
  • Complex illustrated chapters display without image overflow or unreasonable whitespace (QUAL-02)
  • Image sizing is a lightweight operation in willFlushCallback with negligible perf impact (QUAL-04)
  • Footnote images are inline, text-height, width-only (WXRead alignment) </success_criteria>
Create `.planning/phases/08-pagination-quality-cache-performance/08-02-SUMMARY.md` when done