Skip to content

Components

Bartłomiej Mucha edited this page Sep 12, 2026 · 3 revisions

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.

Library components


Component Descriptions

Core Library — libplatemaker

Scaler

  • 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 downstream vips_extract_area + tiled encoders read out of order. See the rationale block in scaler.cpp.
  • Consequence: the full-resolution source is what occupies memory, not the scaled result. ScaledStrip is what bounds how many are alive at once.

ScaledStrip

  • Abstraction of the virtual tape

  • Accepts ScaledImage objects appended one at a time via append()

  • 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:

    1. Decoding is lazy — an appended source is decoded only when the first slice overlapping it is computed, not when it is appended.
    2. 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.
  • 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);
};

SliceResult

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
};

MarginCropper

  • Input: pixel buffer + Margins struct (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

TemplateGenerator

  • 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)

ColourCorrector (optional processing step — page domain)

  • Input: pixel buffer + ColourCorrection config
  • 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 by iccToSRGB, so this step owns only the creative grade. Alpha is split off and re-attached untouched. Pure libvips (vips_maplut for curves, vips_linear, luminance recomb for saturation), no Qt.

StripOverlayCompositor (optional processing step — strip domain)

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 each StripOverlay bitmap once (promoted to RGBA); apply() composites the overlays whose strip-Y span intersects the slice, at (x, y − stripTopY), using each overlay's BlendMode (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.

Infrastructure

ImageIO

  • Wraps libvips for reading/writing PNG, JPEG, TIFF
  • Provides PixelBuffer type that abstracts VipsImage* 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

Clone this wiki locally