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>
298 lines
15 KiB
Markdown
298 lines
15 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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).
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs -->
|
|
|
|
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
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Apply unified max-size constraint and fix footnote sizing across 3 files</name>
|
|
<files>
|
|
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift,
|
|
Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift,
|
|
Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift
|
|
</files>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<behavior>
|
|
- 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
|
|
</behavior>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5</automated>
|
|
</verify>
|
|
<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>
|
|
<done>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</done>
|
|
</task>
|
|
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
|
<name>Task 2: Visual verification of image display fix</name>
|
|
<what-built>
|
|
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)
|
|
</what-built>
|
|
<how-to-verify>
|
|
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)
|
|
</how-to-verify>
|
|
<resume-signal>Type "approved" if all image types display correctly, or describe which image type has issues</resume-signal>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
- xcodebuild build succeeds
|
|
- Visual inspection: cover, qrbodyPic, and footnote images all display correctly
|
|
- No regressions in dark mode
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
Create `.planning/phases/08-pagination-quality-cache-performance/08-02-SUMMARY.md` when done
|
|
</output>
|