Skip to content

0.5.0

Choose a tag to compare

@github-actions github-actions released this 22 Aug 15:25
· 62 commits to main since this release

Defines a render output contract (SPECIFICATION §7.0) that keeps a consumer from racing the
pipeline's writes — the root cause of the GUI's intermittent unable to open for write render failures —
and folds in the diagnostic logger and the multi-source slice fix from the interim commits. Breaking only
because ProcessingPipeline::run() gained a trailing parameter: source-compatible via its default, but
the mangled symbol changes, so the GUI pins the version in lockstep.

Changed

  • Build requirement: libvips ≥ 8.15 on Linux. ImageIO::save() now uses VIPS_FOREIGN_KEEP_ICC
    (added in libvips 8.15) to drop source EXIF/XMP/IPTC while keeping the ICC profile. Windows is unaffected
    (the bundled web-zip is pinned to 8.18); on Linux the build consumes the system libvips, so the CMake
    pkg_check_modules now requires vips-cpp>=8.15 and fails at configure with a clear version error on
    anything older. Distros that ship <8.15 (e.g. Ubuntu 22.04 → 8.12) need a newer libvips or distro
    (Ubuntu 24.04+ ships 8.15, matching CI).
  • ProcessingPipeline::run() gains an optional trailing thumbnailCacheDir. When non-empty, the
    pipeline warms a ThumbnailCache rooted there from each slice's in-RAM pixels — before
    onSliceSaved fires — so a consumer's getOrGenerate(outputPath) is a cache hit and never re-reads a
    freshly-written output during a run. Empty (default) → no thumbnails; the CLI and headless callers pay
    nothing. Defaulted, so existing source compiles unchanged.

Added

  • Component-gated diagnostic logger (Infrastructure::Log). A runtime uint64 bitmask, one bit per
    component (ProcessingPipeline / Scaler / ScaledStrip / …), all-off by default, with a swappable sink
    (stderr by default) and no severity levels; PLATEMAKER_LOG(component, msg) builds the message only
    when the component is enabled (setEnabledComponents / enable / disable / isEnabled). The logger
    is wired into Scaler, ScaledStrip and ProcessingPipeline (each logs its geometry). The CLI gains a
    --trace=0xMASK shadow argument to flip components on.
  • Atomic output publish + OutputLockedError. ImageIO::save() now encodes to a temp sibling and
    renames it over the destination (whole-or-nothing; a reader never sees a partial slice). If another
    process holds the destination, it throws OutputLockedError, which the pipeline surfaces as the new
    ProcessingErrorCode::OutputLocked — the library does not poll/retry; the consumer owns that
    policy. Fixes intermittent unable to open for write … Invalid argument render aborts on Windows.
  • ThumbnailCache::generate(sourceFilePath, const Core::PixelBuffer&) — an in-RAM overload that
    shrinks a caller-supplied image (no file read) and files it under the source path's digest, sharing the
    atomic shrink+write core with the file-reading generate. Lets the pipeline pre-warm the cache from
    pixels it already holds. Thumbnail writes are now atomic too.

Fixed

  • Multi-source slices no longer get a black band. ScaledStrip::buildSlice() folds vips_join
    (vertical) over the same-width parts instead of vips_arrayjoin, which sized every grid cell to the
    tallest part and black-filled the rest — so any slice combining sources of unequal contributed height
    (short camera photos, a page boundary falling mid-slice) came out padded to N × maxHeight. Asserts
    built.height == the requested slice height as a post-condition, so it can never silently regress.
  • Mixed RGB/RGBA (and grayscale/CMYK) inputs no longer abort a render. A folder combining 3-band
    (RGB) and 4-band (RGBA) sources — e.g. camera photos next to a stray screenshot — killed the whole job
    the first time a slice straddled the boundary, because vips_join requires equal band counts.
    ScaledStrip::sliceAll() now normalises the strip up front by promoting to the widest band layout
    (any non-RGB colourspace → sRGB, then a fully-opaque alpha added where missing) — promote-only, so the
    user's pixels are never composited onto a background. A uniform strip is untouched. Alpha is dropped
    only later, at save, and only for JPEG, which cannot carry it (flattened over white); PNG/WebP keep
    the alpha end-to-end.
  • EXIF Orientation is now honoured on load (both pipelines). Camera JPEGs store landscape pixels
    plus an Orientation tag; the pipeline ignored it, so a portrait photo rendered on its side, 180°
    shots rendered upside‑down, and outputs inherited the source tag so viewers re‑rotated them.
    Scaler::scale(filePath) and ImageIO::load() now vips_autorot on load (idempotent for the
    untagged/1 case — a no‑op for Procreate exports), headerGeometry() reports the display dimensions
    (transposed for Orientation 5–8) so canvas‑profile matching agrees with the rendered pixels, and
    ImageIO::save() drops source EXIF/XMP/IPTC (keeping the ICC colour profile) so a rendered slice
    carries no stray camera metadata or orientation. (headerDim() was renamed headerGeometry().)
  • Input thumbnail previews now follow EXIF orientation too — even for photos already in the cache.
    ThumbnailCache::generate was switched to auto-rotate (dropped no_rotate) alongside the load fix
    above, but the on-disk cache is keyed on the source path and invalidated by mtime only: an
    old-dated camera JPEG always has an older mtime than its cached thumbnail, so a preview written by the
    earlier (sideways) code was judged fresh forever and re-served after the fix. The thumbnail filename now
    carries a cache-generation version token (<digest>.vN.png, kThumbnailCacheVersion); bumping it on
    any generation-logic change orphans the stale thumbnails so they regenerate. The mtime check is kept and
    still catches in-place content changes (re-rendered output_00N slices) — the token is additive.
  • Adding a canvas profile no longer marks a whole project out of date when the profile matches no
    page.
    Canvas profiles match purely by canvas W×H, but a page's dimensions were never recorded, so
    detectCanvasConfigChange() could only fall back to a blanket "the effective profile list changed →
    every page is suspect" — creating one profile turned an entire chapter's tiles amber and raised a scary
    "state cannot be confirmed" prompt even when nothing it matched existed in the project. Each
    InputFile now records the display W×H the render resolved against (added additively to the
    workspace JSON, 0 = unknown), and detectCanvasConfigChange() re-matches every page against the
    profiles now in effect — flagging only pages whose applied profile actually changed (a new profile that
    now matches a previously-unmatched page, a removed/reordered one, or an in-place margin edit). The match
    is scoped to the project's assigned profiles only (mirroring CanvasProfileMatcher's subA), so an
    unlinked workspace profile of the same size never desyncs a project. A legacy page with no recorded
    size keeps the honest coarse fallback until its first re-render records the size. The single W×H match
    rule is now Models::canvasSizeMatches(), shared by the matcher and the staleness re-match.
  • Internal: log.hpp include guard renamed (…LOGGING… → …LOG…) to match the header path.

Full Changelog: 0.4.1...0.5.0