Skip to content

Preview

Bartłomiej Mucha edited this page Sep 6, 2026 · 1 revision

Foundation 4 — The preview workflow

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.

Preview workflow


The strip is built from INPUT pages, not from rendered output

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.

What the library provides

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 == false entries 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.

Two tiers, so cost tracks the viewport

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 ThumbnailCache the 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.

Grading the preview

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

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.

Live previews during a render

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.


At-rest previews — the thumbnail cache

ThumbnailCache

  • Generates small preview images (200 px wide) on demand
  • Stored in .platemaker-cache/ sibling to the .platemaker.json workspace 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: PageItem intentionally does not carry a thumbnailPath field. The thumbnail path is deterministic (derived from filePath via a path digest) so it never needs to be persisted in the workspace JSON. The GUI computes it on the fly with ThumbnailCache::thumbnailPath(page.filePath).


See also: Home · Workspace · Project · Render · Preview · Canvas-Profiles · Processing-Steps · Components · CLI · Platform · Roadmap · Diagrams

Clone this wiki locally