docs(08): create phase plans for image display fix and pagination cache
3 plans covering QUAL-01 through QUAL-04: - 08-01: pagination cache key + wiring into rendering pipeline - 08-02: unified 1080x1920 max-size for all images, footnote width-only fix - 08-03: pagination timing instrumentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
86e6922f7b
commit
22bb86959c
@@ -6,31 +6,39 @@ wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
|
||||
autonomous: true
|
||||
requirements:
|
||||
- QUAL-01
|
||||
must_haves:
|
||||
truths:
|
||||
- "Cache key struct exists that captures viewport + typography configuration"
|
||||
- "Cache lookup and store methods exist on the cache type"
|
||||
- "Build succeeds with new cache file in project"
|
||||
- "Repeated pagination of the same chapter with unchanged viewport and typography returns cached layout frames without re-typesetting"
|
||||
- "Changing font size or viewport dimensions invalidates the cache and triggers fresh pagination"
|
||||
- "Cache lookup is transparent — downstream consumers receive the same RDEPUBTextLayoutFrame array whether cached or freshly computed"
|
||||
artifacts:
|
||||
- path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift"
|
||||
provides: "Cache key struct and cache manager stub"
|
||||
provides: "Cache key struct and cache manager for pagination layout frames"
|
||||
exports: ["RDEPUBTextRendererCache", "RDEPUBPaginationCacheKey"]
|
||||
- path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift"
|
||||
provides: "Cache wiring in pagination pipeline"
|
||||
contains: "RDEPUBTextRendererCache.shared"
|
||||
key_links:
|
||||
- from: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift"
|
||||
to: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift"
|
||||
via: "cache lookup before rd_paginatedFrames, store after"
|
||||
pattern: "RDEPUBTextRendererCache\\.shared"
|
||||
- from: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift"
|
||||
to: "RDEPUBDTCoreTextRenderer"
|
||||
via: "import (future wiring)"
|
||||
pattern: "RDEPUBTextRendererCache"
|
||||
to: "RDEPUBTextLayoutFrame"
|
||||
via: "cached value type"
|
||||
pattern: "\\[RDEPUBTextLayoutFrame\\]"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the pagination cache infrastructure: a cache key struct that captures viewport dimensions and typography configuration, plus a cache manager stub with lookup/store/invalidate methods. This is the structural foundation that plan 08-02 and future work will wire into the actual rendering pipeline.
|
||||
Create the pagination cache infrastructure and wire it into the rendering pipeline so that repeated pagination of the same chapter with unchanged viewport and typography configuration returns cached layout frames without re-typesetting.
|
||||
|
||||
Purpose: Prevents the same chapter from being fully re-typeset when viewport and typography config haven't changed (QUAL-01).
|
||||
Purpose: Prevents the same chapter from being fully re-typeset when viewport and typography config haven't changed (QUAL-01). The cache wraps the `rd_paginatedFrames` call in `RDEPUBTextBookBuilder`, which is the actual pagination bottleneck.
|
||||
|
||||
Output: `RDEPUBTextRendererCache.swift` with cache key struct and manager stub.
|
||||
Output: `RDEPUBTextRendererCache.swift` with cache key struct and manager, plus updated `RDEPUBTextBookBuilder.swift` with cache wiring.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@@ -48,47 +56,56 @@ Output: `RDEPUBTextRendererCache.swift` with cache key struct and manager stub.
|
||||
<interfaces>
|
||||
<!-- Existing types the executor needs to know about -->
|
||||
|
||||
From Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift:
|
||||
- `RDEPUBTextChapterRenderRequest` — contains `style: RDEPUBTextRenderStyle`, `context` with baseURL
|
||||
From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift:
|
||||
- `RDEPUBTextChapterRenderRequest` — contains `context: RDEPUBTextChapterContext`, `style: RDEPUBTextRenderStyle`
|
||||
- `RDEPUBTextChapterContext` — has `href: String`, `title: String`, `html: String`, `baseURL: URL?`
|
||||
- `RDEPUBTextRenderStyle` — has `font: UIFont`, `lineSpacing: CGFloat`, `textColor: UIColor?`, `backgroundColor: UIColor?`
|
||||
- `RDEPUBRenderedChapterContent` — has `attributedString: NSAttributedString`, `fragmentOffsets: [String: Int]`
|
||||
|
||||
From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift:
|
||||
- `RDEPUBTextRenderStyle` properties used in rendering: `font.pointSize`, `font.familyName`, `font.fontName`, `lineSpacing`
|
||||
From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayoutFrame.swift:
|
||||
- `struct RDEPUBTextLayoutFrame: Equatable` — value type with contentRange, breakReason, attachmentRanges, diagnostics, etc.
|
||||
|
||||
From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift (line 135, 196):
|
||||
- `buildBook(... pageSize: CGSize, ...)` — the function that iterates spine items
|
||||
- `content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)` — the pagination call to cache around
|
||||
|
||||
From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPaginationSupport.swift:
|
||||
- `rd_paginatedFrames(size: CGSize, fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame]` — extension on NSAttributedString
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create cache key struct and cache manager stub</name>
|
||||
<name>Task 1: Create cache key struct and cache manager</name>
|
||||
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift</files>
|
||||
<read_first>
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift (understand render request and style types)
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift (understand what configuration affects pagination output)
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift (understand RDEPUBTextRenderStyle, RDEPUBTextChapterRenderRequest, RDEPUBTextChapterContext)
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayoutFrame.swift (understand the value type to cache)
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift (lines 130-200 — understand pageSize and rd_paginatedFrames usage)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift` with:
|
||||
|
||||
1. `RDEPUBPaginationCacheKey` struct — captures the inputs that determine pagination output:
|
||||
- `chapterID: String` (unique chapter identifier, e.g. href or file path)
|
||||
- `viewportSize: CGSize` (the available page dimensions)
|
||||
- `fontDescriptor: String` (font family + name + pointSize, concatenated)
|
||||
- `chapterID: String` (the chapter href from `RDEPUBTextChapterContext.href`)
|
||||
- `viewportSize: CGSize` (the `pageSize` passed to `buildBook`)
|
||||
- `fontDescriptor: String` (font family + name + pointSize, concatenated, e.g. `"\(font.familyName)-\(font.fontName)-\(font.pointSize)"`)
|
||||
- `lineSpacing: CGFloat`
|
||||
- `textColorHash: Int?` (hash of textColor, if present)
|
||||
- Implement `Equatable` conformance (auto-synthesize is fine)
|
||||
- `textColorHash: Int` (hash of textColor, or 0 if nil)
|
||||
- Implement `Equatable` conformance (auto-synthesize is fine since all stored properties are Equatable)
|
||||
|
||||
2. `RDEPUBTextRendererCache` class:
|
||||
- `static let shared = RDEPUBTextRendererCache()`
|
||||
- `private var cache: [RDEPUBPaginationCacheKey: [NSAttributedString]]` — maps key to array of page attributed strings
|
||||
- `func cachedPages(for key: RDEPUBPaginationCacheKey) -> [NSAttributedString]?` — returns cached pages or nil
|
||||
- `func store(pages: [NSAttributedString], for key: RDEPUBPaginationCacheKey)` — stores pages
|
||||
- `func invalidate(for key: RDEPUBPaginationCacheKey)` — removes a specific entry
|
||||
- `func invalidateAll()` — clears entire cache
|
||||
- `var entryCount: Int` — read-only count of cached entries (for diagnostics)
|
||||
- `private var cache: [RDEPUBPaginationCacheKey: [RDEPUBTextLayoutFrame]]` — maps key to array of layout frames
|
||||
- `private let lock = NSLock()` — thread safety
|
||||
- `func cachedFrames(for key: RDEPUBPaginationCacheKey) -> [RDEPUBTextLayoutFrame]?` — lock, lookup, unlock, return
|
||||
- `func store(frames: [RDEPUBTextLayoutFrame], for key: RDEPUBPaginationCacheKey)` — lock, store, unlock
|
||||
- `func invalidate(for key: RDEPUBPaginationCacheKey)` — lock, remove, unlock
|
||||
- `func invalidateAll()` — lock, removeAll, unlock
|
||||
- `var entryCount: Int` — lock, read count, unlock (for diagnostics)
|
||||
|
||||
Make the class `final` and `Sendable`-safe (use a lock or `@MainActor` annotation since rendering is main-thread). Use `NSLock` for thread safety.
|
||||
|
||||
Do NOT wire this cache into the rendering pipeline yet — that is future work. This plan only creates the infrastructure.
|
||||
Make the class `final`. Use `NSLock` for thread safety since the cache may be accessed from different queues.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5</automated>
|
||||
@@ -97,24 +114,91 @@ Do NOT wire this cache into the rendering pipeline yet — that is future work.
|
||||
- `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift` exists
|
||||
- File contains `struct RDEPUBPaginationCacheKey: Equatable` with properties: chapterID, viewportSize, fontDescriptor, lineSpacing, textColorHash
|
||||
- File contains `final class RDEPUBTextRendererCache` with static `shared` instance
|
||||
- Class has methods: `cachedPages(for:)`, `store(pages:for:)`, `invalidate(for:)`, `invalidateAll()`
|
||||
- Cache value type is `[RDEPUBTextLayoutFrame]` (not `[NSAttributedString]`)
|
||||
- Class has methods: `cachedFrames(for:)`, `store(frames:for:)`, `invalidate(for:)`, `invalidateAll()`
|
||||
- Class has computed property `entryCount: Int`
|
||||
- Build succeeds (xcodebuild exits 0)
|
||||
</acceptance_criteria>
|
||||
<done>Cache key struct and cache manager stub exist, build compiles successfully</done>
|
||||
<done>Cache key struct and cache manager exist with correct value types, build compiles successfully</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire cache into pagination pipeline in RDEPUBTextBookBuilder</name>
|
||||
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift</files>
|
||||
<read_first>
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift (full file — understand buildBook function, pageSize parameter, rd_paginatedFrames call at line 196)
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift (created in Task 1 — understand API surface)
|
||||
</read_first>
|
||||
<action>
|
||||
Wire the cache into `RDEPUBTextBookBuilder.swift` around the `rd_paginatedFrames` call at line 196.
|
||||
|
||||
**Location:** Inside the `buildBook` function, around the existing pagination block (lines 194-198):
|
||||
```swift
|
||||
layoutFrames = content.length > 0
|
||||
? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)
|
||||
: []
|
||||
```
|
||||
|
||||
**Replace with cache-aware logic:**
|
||||
|
||||
1. Before the pagination call, build a cache key:
|
||||
```swift
|
||||
let cacheKey = RDEPUBPaginationCacheKey(
|
||||
chapterID: item.href,
|
||||
viewportSize: pageSize,
|
||||
fontDescriptor: "\(style.font.familyName)-\(style.font.fontName)-\(style.font.pointSize)",
|
||||
lineSpacing: style.lineSpacing,
|
||||
textColorHash: style.textColor?.hashValue ?? 0
|
||||
)
|
||||
```
|
||||
|
||||
2. Check cache first:
|
||||
```swift
|
||||
if let cached = RDEPUBTextRendererCache.shared.cachedFrames(for: cacheKey) {
|
||||
layoutFrames = cached
|
||||
} else {
|
||||
layoutFrames = content.length > 0
|
||||
? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)
|
||||
: []
|
||||
if !layoutFrames.isEmpty {
|
||||
RDEPUBTextRendererCache.shared.store(frames: layoutFrames, for: cacheKey)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Keep the cover chapter special-case path (lines 175-193) unchanged — it doesn't go through `rd_paginatedFrames`.
|
||||
|
||||
The `style` variable is available in scope (it's the `style: RDEPUBTextRenderStyle` parameter of `buildBook`). The `item.href` is the chapter identifier from the spine iteration.
|
||||
</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>
|
||||
- RDEPUBTextBookBuilder.swift imports or references `RDEPUBTextRendererCache`
|
||||
- Cache key is constructed from `item.href`, `pageSize`, `style.font`, `style.lineSpacing`, `style.textColor`
|
||||
- `RDEPUBTextRendererCache.shared.cachedFrames(for:)` is called before `rd_paginatedFrames`
|
||||
- `RDEPUBTextRendererCache.shared.store(frames:for:)` is called after successful pagination
|
||||
- Cover chapter special-case path is NOT modified
|
||||
- Build succeeds (xcodebuild exits 0)
|
||||
</acceptance_criteria>
|
||||
<done>Cache is wired into the pagination pipeline — repeated pagination of the same chapter with identical config returns cached frames, build compiles</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- Build succeeds with new file
|
||||
- Cache types are properly defined and accessible from other modules in the same target
|
||||
- Build succeeds with both new and modified files
|
||||
- Cache types are properly defined and accessible from RDEPUBTextBookBuilder
|
||||
- Cache key captures all inputs that affect pagination output (chapterID, viewportSize, font, lineSpacing, textColor)
|
||||
- Cache value is [RDEPUBTextLayoutFrame] matching the rd_paginatedFrames return type
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- RDEPUBPaginationCacheKey captures all inputs that affect pagination output
|
||||
- RDEPUBTextRendererCache provides a thread-safe, singleton cache surface ready for wiring
|
||||
- No existing code is modified — this is purely additive
|
||||
- RDEPUBTextRendererCache provides a thread-safe, singleton cache surface
|
||||
- Cache is wired into the actual pagination path in RDEPUBTextBookBuilder
|
||||
- Same chapter + same config = cache hit (no re-typesetting)
|
||||
- Different viewport or typography = cache miss (fresh pagination)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Reference in New Issue
Block a user