--- phase: 08-pagination-quality-cache-performance plan: 02 type: execute wave: 1 depends_on: [] files_modified: - Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift - Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift - Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift autonomous: false requirements: - QUAL-02 - QUAL-03 - QUAL-04 must_haves: truths: - "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" artifacts: - path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift" provides: "Unified max-size constraint in prepareHTMLElementForReaderRendering, cleaned-up footnote handling" contains: "CGSize(width: 1080, height: 1920)" - path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift" provides: "Updated DTMaxImageSize from screen bounds to 1080x1920" contains: "CGSize(width: 1080, height: 1920)" key_links: - from: "RDEPUBDTCoreTextRenderer.dtOptions" to: "DTMaxImageSize" via: "builder option" pattern: "DTMaxImageSize.*CGSize.*1080" - from: "RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering" to: "DTTextAttachment.displaySize" via: "unified max-size scaling" pattern: "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). @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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): ```swift 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): ```swift DTMaxImageSize: NSValue(cgSize: screenBounds.size) // ~353x757 ``` From RDEPUBTextContentView.swift (current implementation, lines 263-282): ```swift private func normalizeInlineAttachments(in content: NSMutableAttributedString, basePointSize: CGFloat) { // footnote: targetHeight = pointSize * 0.14, displaySize set } ``` From RDEPUBTextRendererSupport.swift (current implementation, lines 467-505): ```swift 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: ```swift 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: ```swift "width:1em", "height:1em", ``` Change to: ```swift "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: ```swift 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 - 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 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 ## 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 | - xcodebuild build succeeds - Visual inspection: cover, qrbodyPic, and footnote images all display correctly - No regressions in dark mode - 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) Create `.planning/phases/08-pagination-quality-cache-performance/08-02-SUMMARY.md` when done