Repository navigation
0.5.0
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 usesVIPS_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_modulesnow requiresvips-cpp>=8.15and 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 trailingthumbnailCacheDir. When non-empty, the
pipeline warms aThumbnailCacherooted there from each slice's in-RAM pixels — before
onSliceSavedfires — so a consumer'sgetOrGenerate(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 runtimeuint64bitmask, 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=0xMASKshadow 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 throwsOutputLockedError, which the pipeline surfaces as the new
ProcessingErrorCode::OutputLocked— the library does not poll/retry; the consumer owns that
policy. Fixes intermittentunable to open for write … Invalid argumentrender 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-readinggenerate. 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()foldsvips_join
(vertical) over the same-width parts instead ofvips_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 toN × maxHeight. Asserts
built.height == the requested slice heightas 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, becausevips_joinrequires 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
Orientationis 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)andImageIO::load()nowvips_autoroton load (idempotent for the
untagged/1case — 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 renamedheaderGeometry().) - Input thumbnail previews now follow EXIF orientation too — even for photos already in the cache.
ThumbnailCache::generatewas switched to auto-rotate (droppedno_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-renderedoutput_00Nslices) — 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
InputFilenow records the display W×H the render resolved against (added additively to the
workspace JSON,0= unknown), anddetectCanvasConfigChange()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 (mirroringCanvasProfileMatcher'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 nowModels::canvasSizeMatches(), shared by the matcher and the staleness re-match. - Internal:
log.hppinclude guard renamed (…LOGGING…→…LOG…) to match the header path.
Full Changelog: 0.4.1...0.5.0