Repository navigation
0.3.0
Breaking (pre-1.0 shifted scale: a breaking change bumps the minor). The output-profile preset
model changed and some symbols were removed. Workspace files stay readable and are migrated on load;
a consumer that used the removed preset internals must adapt.
Removed
Models::isOutputProfilePresetId()andModels::k_outputPresetPrefix— preset-ness is no longer
derived from an id prefix. UseModels::outputPresetDefById(id)(returns a definition pointer, or
nullptr) instead.WorkspaceSerializer's adopt/fork/presence preset handling (enforceOutputProfilePresets), the
"a preset is always present in the workspace" invariant, andWorkspaceRepairReport's
preset-collision fields. Presets are no longer written into a workspace.
Changed (breaking)
- Unmatched input pages are now rendered implicitly instead of being dropped. When a project has
canvas profiles and an input'sW×Hmatches none of them, the pipeline used to skip the page (it
never joined the strip, only a summary count noted it — and because slice numbering is continuous,
the missing page left no visible gap). It is now scaled totargetWidthwith no margin crop (the
same path a project with no profiles uses) and reported viaonInputasAppendedWithoutProfile, or
AppendedProfileNotLinked(with a Warning log naming the ids) when a same-size profile exists in
the workspace but is not linked to the project. Such a page is markedFileStatus::Processedwith an
emptycanvasProfileId.ProcessingOutcome::skippedPagesnow lists only missing / load-error inputs.
Two newInputStatusvalues are added;SkippedNoProfile/SkippedProfileNotLinkedare retained
but unemitted (reserved for a future opt-in "drop unmatched pages" mode). This deviates from the old
SPECIFICATION §7.5.1 steps 4a/4b (now amended): determinism is preserved by visibility (the input
is flagged) rather than by omission, and quick-start renders and late-added profile-less frames no
longer lose pages. See §7.5.1. - Presets are code-defined and never persisted. A preset is a baked-in template — a compile-time
catalogue (Models::k_outputPresetDefs/OutputPresetDef) materialised on demand — never
serialised intooutputProfiles. Preset-ness is provenance, tested zero-copy by
Models::outputPresetDefById(id); there is no "is a preset" field and no reserved id prefix.
Immutability is the library's own guarantee: the serializer refuses to write a preset-id profile, and
load()migrates older files (a persisted copy of a preset is dropped and its references relinked to
the canonical id; a diverged preset-id profile is given a fresh id; a user profile that merely
resembles a preset is untouched). Customising a preset means duplicating it. A future change to a
preset's content therefore no longer desyncs stored workspaces. Models::resolveOutputProfile(workspace, id)— new shared resolver returning
std::optional<OutputProfile>, unioning the workspace's own profiles with the preset catalogue
(presets take precedence). Replaces per-consumer resolution.Models::outputProfilePresets()now returnsconst std::vector<OutputProfile>&(built once) and
Models::outputProfilePresetById()materialises from the compile-time catalogue.ProcessingPipeline::run()groups its callbacks into aCore::ProcessingCallbacksstruct. The
three loose trailing callback parameters (onProgress,onLog,onSliceSaved) are replaced by a
singleconst ProcessingCallbacks& callbacks = {}(beforeonlySlices); each field is an optional
std::function, so a caller wires only what it needs. The per-sliceonSliceSavednow reports a
SliceSaved{sliceIndex, name, fullPath}(was(name, fullPath)) — the absolute 0-based slice index
lets a consumer address the output by position.ProcessingProgressandProcessingLogLevelmove
fromprocessing_pipeline.hppto the newprocessing_callbacks.hpp(a transitive include, so existing
users are unaffected).cancelstays a separate parameter.run()is also nowstatic— the
pipeline is stateless, so callProcessingPipeline::run(...)without constructing an instance.Models::Workspace's profile palettes are now encapsulated.canvasProfilesand
outputProfilesare no longer public vectors: they are read throughws.canvasProfiles()/
ws.outputProfiles()(const accessors) and mutated only through the new
Infrastructure::WorkspaceEditor. Direct writes (ws.canvasProfiles.assign(...),push_back, …) no
longer compile — a consumer that edited the vectors by hand must go through the editor. Reading is
unchanged apart from the(). This closes a real hole: an edit made in a running session is now put
through the same invariant checks (unique ids, no persisted presets,templateInfopreserved) that
a loaded file is, instead of only being validated on the next open.ProjectItem's profile-link fields are now encapsulated too.canvasProfileIdsand
outputProfileIdare no longer public: they are read throughpi.canvasProfileIds()/
pi.outputProfileId()(const accessors) and written only throughProjectItem::addCanvasProfile()
(the dimension guard) andWorkspaceEditor::setProjectOutputProfile()/removeCanvasProfileFromProject().
This closes the last bypass: a rawpi.outputProfileId = "garbage"(skipping the id validation) or a
rawpi.canvasProfileIds.push_back()(skipping the "one profile per canvas W×H" guard,
SPECIFICATION.md §7.5.2) no longer compiles.from_json(ProjectItem)is intentionally partial (like
Workspace);load()installs the links through the friend path. OtherProjectItemfields
(name/uid/inputDirectory/outputSignature/canvasProfileIdsAtRender) stay public — they carry
no cross-cutting invariant.uuid→uidacrossInputFile/OutputFile/ProjectItem, and the ids are now genuinely
unique. The field and its JSON key are renamed (these are short local ids likefile-<hex>/
out-N/proj-<hex>, never RFC 4122 UUIDs — the name over-promised). No back-compat: the old
"uuid"key is not read, so a workspace written by an older build loads with empty file/project ids
that are minted fresh on load (and the old key is dropped on the next save). Input ids are no longer
derived from the list position ("file-" + index), which could hand two files the same id across
re-scans;ProjectItem::ensureUniqueFileUids()andWorkspaceSerializer::load()mint via
Infrastructure::makeUniqueIdand de-duplicate. The GUI's project id (previously a timestamp — also
collision-prone) now goes throughmakeUniqueIdtoo.
Added
Infrastructure::ProjectEditor(platemaker/infrastructure/project_editor/project_editor.hpp)
— the authority for a single project's content, twin ofWorkspaceEditor. Owns input ordering:
setInputOrder(orderedUids)(validates a permutation, rewrites eachInputFile::order) and
moveInput(uid, ±1). It never physically reorders the stored input vector — only theorder
field — so a reorder does not churn the project structure.- The render now follows
InputFile::order, not the stored-vector order. New
ProjectItem::inputsInOrder()returns the inputs sorted byorder; the CLI passes it to
ProcessingPipeline::run(the pipeline itself is unchanged — it still renders the sequence it is
handed, with no knowledge oforder). Previously the strip was built in raw vector order, so a
reorder that only touchedordernever reached the output. - Input-composition staleness axis —
ProjectItem::inputOrderAtRender(the input uid sequence, in
order, captured at render, next tocanvasProfileIdsAtRender) and
ProjectItem::detectInputCompositionChange().sanitize()marks every outputDesynchronizedwhen
the current order/composition differs from the baseline: reordering / adding / removing an input
shifts the continuous strip so every downstream slice changes while each file stays byte-identical —
no hash could notice.WorkspaceSerializer::load()backfills the baseline of a project rendered
before this field existed from the outputs'sourceMapprovenance, so an old reorder is still caught
without a spurious re-render. WorkspaceEditor::addProject(name)— creates a project with a workspace-uniqueproj-uid and
appends it. Minting the project uid is now the lib's job: the CLI (and GUI) no longer hand-roll it
withmakeUniqueId(the last identifier a consumer still generated itself).Models::outputPresetDefById(),OutputPresetDef,k_outputPresetDefs— the compile-time preset
catalogue (single source of truth) and its zero-copy membership test.Core::ProcessingCallbacksand its event payloads (core/processing_callbacks/processing_callbacks.hpp)
— the pipeline now reports, in addition to progress/log/slice-saved:onInput(InputResult)once per
input (Appended;AppendedWithoutProfile/AppendedProfileNotLinkedfor an implicitly-rendered page,
the latter carrying the matching-but-unlinked workspace profile ids; orSkippedMissing/SkippedError.
TheSkippedNoProfile/SkippedProfileNotLinkedvalues remain in the enum but are no longer emitted —
reserved for a future opt-in skip mode);onSlicingStarted(SlicingStarted)at the phase-1→phase-2
boundary with the expected slice count;
andonSliceSkipped(SliceSkipped)for a clean slice a partial re-render leaves untouched. All are
optional. Callbacks fire synchronously on the calling thread; the library remains thread- and
GUI-agnostic.Models::FileStatus::Skipped— a new lifecycle value for an input the last render did not
include: a missing file or one that failed to load. (A page that merely matches no canvas
profile is no longer skipped — it is rendered implicitly and ends upProcessed; see the
implicit-render entry under Changed.) Distinct fromMissingandProcessed. Set by
applyProcessingResults()(above) and reported live during a render via theonInputcallback;
a consumer switching overFileStatusmust handle the new value.Infrastructure::WorkspaceEditor(platemaker/infrastructure/workspace_editor/workspace_editor.hpp)
— the single authority that mutates a workspace's profile palettes while enforcing its invariants.
Intent-level ops:addCanvasProfile/removeCanvasProfile/replaceCanvasProfiles(carries
templateInfo, mints ids, dedups) and the output-profile equivalents (replaceOutputProfilesalso
strips persisted presets); the project-link pairaddCanvasProfileToProject/
removeCanvasProfileFromProject(the symmetric remove the model lacked); andsetProjectOutputProfile
(validates the id resolves, presets included). The identifier-repair rules that used to live inside
WorkspaceSerializer::load()now live here as one copy —load()runs them via
WorkspaceEditor::installLoaded(), so an in-memory edit and a loaded file obey the same rules. Lives
in Infrastructure (not Models) so the invariant logic can mint ids without draggingid_generator
into the model layer.- CLI output-profile family, id-selected —
workspace list-presets,add-output-profile
(--from-preset ID, or from scratch with--target-width/--slice-height/--format),
mod-/rm-/list-output-profiles, and--output-profile IDonprocessandproject. The canvas
commands are renamed*-canvas-profile; the oldadd-/mod-/rm-/list-profilesnames are removed
(breaking).list-canvas-profilesandlist-output-profileseach list only their own family;
the combined view islist-all-profiles(aliaslist-profiles).processuses a selected or
project-assigned profile exactly as stored, or builds an ad-hoc profile from the inline options
when none is selected — the two never mix, so an override cannot silently edit a stored profile or a
preset. Infrastructure::buildInfo()andBuildInfo
(platemaker/infrastructure/build_info/build_info.hpp) — the library's own version, SPDX licence,
compiler and target read back at runtime from the loaded DLL. The runtime twin of the
compile-timeversion.hpp: replacing only the shared library no longer leaves a consumer
reporting a stale version, and a consumer can name the lib's licence without hardcoding it.Infrastructure::runtimeMatchesHeader()— inline consistency check comparing the compile-time
version_stringagainst the loadedbuildInfo().version, so a consumer can detect a DLL that
does not match the header it built against (zlib'sZLIB_VERSION/zlibVersion()idiom).Infrastructure::linkedComponents()andLinkedComponent— the third-party components this
build links, with versions, SPDX licences and upstream GitHub URLs (libvips, LGPL-2.1-or-later, at
its runtime version; nlohmann/json, MIT). Lets a consumer name what is linked, and under what terms,
without asserting facts about the lib's dependencies it cannot know.LinkedComponent::urlis
guaranteed to be ahttps://github.com/link.credits/in the package — an SPDX 2.3 SBOM (sbom.spdx.json) describing libplatemaker and its
bundled dependencies (versions, SPDX licences,pkg:github/...purls), plus each dependency's
licence text undercredits/licenses/. The SBOM is the machine-readable inventory expected by the
EU Cyber Resilience Act and by commercial integrators; the texts satisfy the LGPL requirement to
distribute a copy of the licence. A consumer locates the directory via theplatemaker_CREDITS_DIR
package-config variable.
Fixed
ProjectItem::sanitize()no longer discards a skipped page's status. A page the last render
skipped (no matching / linked canvas profile) is now sticky:sanitize()keepsFileStatus::Skipped
as long as the file is unchanged, instead of "un-skipping" it from the disk pass (a stale-but-matching
hash used to flip it toProcessed, a never-rendered one toPending). Because a skipped page is
terminal — nothing can be rendered for it without a profile change — it no longer sets the project
out of date, so a project with a permanently-skipped page (all outputsDone) is correctly reported
up to date instead of re-rendering on every run. An actual profile-list change is still caught by
detectCanvasConfigChange(), and editing a skipped file re-opens it (Modified).inputsAllProcessed()
likewise treatsSkippedas settled. Fixes the GUI reopening a project with all-Doneoutputs yet
always re-rendering, and skipped input tiles reverting on reopen.
Packaging
- The
find_package(platemaker)version pin now actually holds pre-1.0. The config-version file
switched fromSameMajorVersiontoSameMinorVersion: with major0,SameMajorVersiontreats
every0.yas compatible, so a consumer pinned to0.3.0would silently accept an incompatible
0.4.0— but under semver a0.MINORbump is exactly where a pre-1.0 breaking change lives (as
0.2.0and this0.3.0both were). The package now accepts only the same0.MINOR.*. - The package rejects an ABI-incompatible toolchain at
find_packagetime. The generated
platemaker-config.cmakerecords the build compiler and fails with an actionableFATAL_ERROR
when a MinGW/GCC consumer picks up an MSVC-built package (or the reverse) — the two are
ABI-incompatible (name mangling, STL layout, CRT) and cannot be linked. Previouslyfind_package
succeeded and the mismatch surfaced much later as a baffling link or load error. The message names
both compilers and points at the matching per-toolchain dev package (…-windows-mingw-…vs
…-windows-msvc-…).