Repository navigation
Components
Class by class: what each one does, and — where it matters more — what it deliberately does not.
Every class here is small and single-purpose by policy; see docs/CODING_STYLE.md in the library
repository for the rule and for the two documented exceptions to it.
- Input: file path + target width (px)
- Output:
ScaledImage(pixel buffer + original file path for provenance) - Algorithm: Lanczos3 via
vips_resize()— aspect-preserving, anti-aliased - Stateless and thread-safe
- Loads with
VIPS_ACCESS_RANDOM, which caches the decoded source in RAM; the resize on top of it stays lazy.vips_thumbnail()(sequential access) is deliberately not used — it makes downstreamvips_extract_area+ tiled encoders read out of order. See the rationale block inscaler.cpp. - Consequence: the full-resolution source is what occupies memory, not the scaled result.
ScaledStripis what bounds how many are alive at once.
-
Abstraction of the virtual tape
-
Accepts
ScaledImageobjects appended one at a time viaappend() -
Produces exactly
floor(totalHeight / sliceHeight)full slices + 1 tail (if remainder > 0) -
LastSlicePolicy:
CROP|PAD_WHITE|KEEP_AS_IS -
Tracks
SourceMap— which input file and Y-range contributed to each output slice -
Memory policy: only the sources overlapping the slice currently being built hold decoded pixels — the minimum the slice cannot be assembled without. This is deliberately not a fixed number:
- Sources taller than
sliceHeight(the usual case): 1, or 2 when the slice straddles a source boundary. - Sources shorter than
sliceHeight: as many as it takes to fill the slice. A 200px leftover tail + a 500px extra panel + a 100px spacer + 480px off the next page all feed one 1280px slice → 4 live sources. Inputs are not required to be at least one slice tall; this is how a small panel or some breathing room between panels can be inserted without redrawing the surrounding pages.
It rests on two mechanisms, both required:
- Decoding is lazy — an appended source is decoded only when the first slice overlapping it is computed, not when it is appended.
-
sliceAll()releases as it advances — slices go in increasing Y, so a source lying entirely above the current slice can never contribute again and its buffer is dropped.
- Sources taller than
-
sliceAll()therefore consumes the strip (single-use) and streams slices to a callback rather than returning them: collecting them would be harmless in itself (slices are lazy graphs, not pixels), but saving must interleave with slicing for the release to bound anything.
class ScaledStrip {
public:
using SliceFn = std::function<bool(SliceResult&&)>; // false → stop
void append(ScaledImage image);
int totalHeight() const;
void sliceAll(int sliceHeight, LastSlicePolicy policy,
const CancellationToken& cancel, const SliceFn& onSlice);
};struct SourceSegment {
std::string inputFilePath;
int srcY; // Y offset within the scaled source image
int height; // number of px taken from this source
};
struct SliceResult {
PixelBuffer image;
int index; // 0-based output index
int stripTopY; // this slice's top Y in the continuous strip (strip domain)
std::vector<SourceSegment> sourceMap; // provenance — used for incremental hashing
};- Input: pixel buffer +
Marginsstruct (top/right/bottom/left in px) - Output: cropped pixel buffer (safe area only)
- Pure crop, no resampling — runs before Scaler in the margin-aware pipeline
- Input:
CanvasProfile+OutputProfile - Output: PNG image file
- Renders: white background, semi-transparent margin zone overlay, safe-area border, horizontal slice lines
- Uses libvips draw operations (no Qt dependency)
- Input: pixel buffer +
ColourCorrectionconfig - Output: graded pixel buffer (brightness/contrast/saturation + per-channel tone curves)
- Point operation, applied per input page before scale/append; a neutral config returns the buffer
unchanged. ICC→sRGB is done at load (
ImageIO::decode(convertToSRGB)), gated byiccToSRGB, so this step owns only the creative grade. Alpha is split off and re-attached untouched. Pure libvips (vips_maplutfor curves,vips_linear, luminance recomb for saturation), no Qt.
Rasterises overlay assets and composites the ones intersecting a slice onto it. Format-agnostic:
rasterizeOverlayAtWidth() resolves the loader with vips_foreign_find_load() before opening the file,
so an SVG re-renders sharp through librsvg while a raster is resampled — the loaders take a ratio, not
a target width (and scale is svgload-only; passing it to pngload fails outright), so it probes the
asset's natural width once and renders at the ratio that lands on the one asked for.
rasterizeOverlays() takes Models::PlacedOverlay — already resolved to absolute strip pixels by
Models::resolveOverlayAnchors(), each carrying its own rendered width. Size and placement therefore
come out of one call against one target width and cannot disagree; there is no scale for a caller to
remember to pass to two functions.
Three calls exist for consumers rather than for the render itself, because a preview must show what the
render will bake rather than an approximation of it: rasterizeOverlay() (one asset → PixelBuffer),
rasterizeOverlayRgba() and rasterizeSvgRgba() (→ OverlayRaster: plain straight-alpha RGBA bytes,
for a consumer with no libvips of its own). The SVG form takes the document in memory, so a preview
never depends on a file having been written and read back — unreliable on a synced drive immediately
after a write.
- Input: an output slice + its
stripTopY+ the loaded overlays; Output: the slice with overlays composited -
load()decodes eachStripOverlaybitmap once (promoted to RGBA);apply()composites the overlays whose strip-Y span intersects the slice, at(x, y − stripTopY), using each overlay'sBlendMode(vips_composite2). libvips clips a layer straddling a slice cut, so it lands on both adjacent slices; a slice no overlay touches is returned unchanged. Format-agnostic — it composites bytes, it does not render text.
- Wraps libvips for reading/writing PNG, JPEG, TIFF
- Provides
PixelBuffertype that abstractsVipsImage*with RAII - Handles colour profile normalisation on load
-
Supported output formats (carried over from Clip2l prototype):
- PNG (default)
- JPEG — with configurable quality (1–95, default 90), subsampling (4:4:4 / 4:2:2 / 4:2:0), optimize flag, progressive flag
- WebP
- Per-file error handling: if a single input file fails to load, log the error and skip it rather than aborting the entire batch. Report skipped files in the final summary.
See also: Home · Workspace · Project · Render · Preview · Canvas-Profiles · Processing-Steps · Components · CLI · Platform · Roadmap · Diagrams