Repository navigation
Preview
How a consumer shows a chapter, and authors edits against it, before any render exists.
This is the one part of the specification that had no home in the original document. It existed as
five fragments a thousand lines apart — a word in the layer diagram, the ThumbnailCache component,
a note on PageItem, two clauses of the render output contract, and an unticked roadmap box — so a
reader could not assemble it without reading everything. It is written out here.
A viewer stacks each input put through the library's page domain — EXIF-upright, margin-cropped, scaled to the output profile's target width — rather than reassembling the committed slices. Three consequences, and they are the whole reason for the design:
- It works before the first render. A grade or a bubble has to be authored before it is baked. On an output feed there is nothing to look at until a render exists, which is precisely backwards.
- Rendering does not change the view. Edits preview against the input, so the render bakes exactly what was on screen. On an output feed the render bakes the grade into the slices and the preview then grades them a second time.
- The unit of work is the page. That is the unit a grade's per-page exclusions and an overlay's page anchor both address. An output slice can straddle two pages, so on that feed neither could be honoured at display time at all.
Slices are deliberately absent. They are a publishing artifact — files to upload — and a viewer
draws a continuous strip and hides the seams anyway. What the author still needs is the slice
grid: seam guides drawn every sliceHeight down the strip, marking the line a bubble must not be
split by.
Two calls, named for how they differ, because that is the only thing worth knowing when choosing between them:
| Call | Reads | Covers | Cost |
|---|---|---|---|
layoutPagesFromHeaders() |
file headers | every page | ~1 ms per page |
decodePageToRgba() |
decodes pixels | one page | a full decode + scale |
Neither is approximate. layoutPagesFromHeaders() runs the real page domain and reads the dimensions
off the result; every libvips operation it builds is lazy, settling the output size from the header
and decoding nothing because nothing pulls a pixel. The numbers are therefore the render's own,
by construction, rather than arithmetic that could drift from it — which is why the two calls live
beside render() and go through the same PageRenderer.
Each entry comes back as a PagePreviewGeometry: scaled width/height, the source display size,
the canvasProfileId and InputStatus the render would report, the inputUid an overlay anchors
to — and readable.
A consumer must skip
readable == falseentries when stacking, exactly as the render skips them. Miss that and every page below the first unreadable one sits at the wrong strip offset. One bad file must not cost the consumer the other ninety-nine, so the call never throws for a page it cannot read: it returns the entry flagged, with zero dimensions.
A chapter is far larger than a screen, so a viewer resolves each page at the cheapest fidelity that will do:
-
Proxy tier — the input page's thumbnail from the
ThumbnailCachethe consumer has already warmed elsewhere. Drawn instantly, so a page is never blank. -
Sharp tier —
decodePageToRgba()for pages in view plus a small prefetch margin, off the UI thread, into a byte-capped LRU cache. Off-screen pages are evicted, so memory tracks the viewport rather than the chapter length.
decodePageToRgba() deliberately returns the page ungraded, and takes no colour argument at all.
Its consumer re-grades on every slider move, and re-decoding for that would cost roughly a hundred
times what grading the buffer costs. Because the colour step never influences how a page is read, a
page fetched once stays a valid baseline for every grade the user tries on it.
The consumer grades with ColourCorrector::applyToRgba() — the same engine the render uses, so the
preview is not an approximation of the committed result.
The grade is a derived copy, never applied to the resident page. The decoded page stays ungraded in its cache; the graded pixmap is cached separately. That is what keeps the baseline valid, and it means scrolling inside a page does not re-grade it.
Per-page exclusions are honoured at display time. A page in excludedInputUids renders ungraded
in the preview, by the same rule the render applies — and this is the clearest illustration of why
the preview's unit of work is the page rather than the slice.
Overlays preview for free: each is a separate item drawn above the strip at its resolved position, so
typing in a bubble repaints without any file round-trip. An overlay carrying an anchorInputUid is
placed relative to that page's top edge in the strip the viewer just laid out — the same arithmetic
render() performs, which is why PagePreviewGeometry carries inputUid: it lets a consumer build
the uid → strip-Y map Models::resolveOverlayAnchors() takes without joining back through paths.
An overlay whose anchor page is not in the strip is an orphan. The render reports and skips it; a viewer should show it rather than silently re-home or delete it.
The render can warm a consumer's thumbnail cache from each slice's in-RAM pixels before it announces the slice, so the consumer never opens a freshly written output mid-run. That is guarantee G3 of the render output contract, with the matching consumer duty C3 — both on the Rendering a project page.
- Generates small preview images (200 px wide) on demand
- Stored in
.platemaker-cache/sibling to the.platemaker.jsonworkspace file - Safe to delete at any time — regenerated transparently on next access
- Zero Qt dependency — provides only synchronous, blocking methods
Usage policy: ThumbnailCache is caller-driven and is never invoked by the
CLI binary. Thumbnails are a pure GUI concern. When the Qt GUI needs to display
a thumbnail for a PageItem it calls ThumbnailCache::getOrGenerate(page.filePath).
For non-blocking behaviour the GUI wraps that call in QtConcurrent::run() itself —
libplatemaker makes no threading decisions.
Design note:
PageItemintentionally does not carry athumbnailPathfield. The thumbnail path is deterministic (derived fromfilePathvia a path digest) so it never needs to be persisted in the workspace JSON. The GUI computes it on the fly withThumbnailCache::thumbnailPath(page.filePath).
See also: Home · Workspace · Project · Render · Preview · Canvas-Profiles · Processing-Steps · Components · CLI · Platform · Roadmap · Diagrams