Skip to content

0.3.0

Choose a tag to compare

@ShadobaDev ShadobaDev released this 29 Jul 20:33
· 112 commits to main since this release

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() and Models::k_outputPresetPrefix — preset-ness is no longer
    derived from an id prefix. Use Models::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, and WorkspaceRepairReport'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's W×H matches 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 to targetWidth with no margin crop (the
    same path a project with no profiles uses) and reported via onInput as AppendedWithoutProfile, 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 marked FileStatus::Processed with an
    empty canvasProfileId. ProcessingOutcome::skippedPages now lists only missing / load-error inputs.
    Two new InputStatus values are added; SkippedNoProfile / SkippedProfileNotLinked are 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 into outputProfiles. 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 returns const std::vector<OutputProfile>& (built once) and
    Models::outputProfilePresetById() materialises from the compile-time catalogue.
  • ProcessingPipeline::run() groups its callbacks into a Core::ProcessingCallbacks struct. The
    three loose trailing callback parameters (onProgress, onLog, onSliceSaved) are replaced by a
    single const ProcessingCallbacks& callbacks = {} (before onlySlices); each field is an optional
    std::function, so a caller wires only what it needs. The per-slice onSliceSaved now reports a
    SliceSaved{sliceIndex, name, fullPath} (was (name, fullPath)) — the absolute 0-based slice index
    lets a consumer address the output by position. ProcessingProgress and ProcessingLogLevel move
    from processing_pipeline.hpp to the new processing_callbacks.hpp (a transitive include, so existing
    users are unaffected). cancel stays a separate parameter. run() is also now static — the
    pipeline is stateless, so call ProcessingPipeline::run(...) without constructing an instance.
  • Models::Workspace's profile palettes are now encapsulated. canvasProfiles and
    outputProfiles are no longer public vectors: they are read through ws.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, templateInfo preserved) that
    a loaded file is, instead of only being validated on the next open.
  • ProjectItem's profile-link fields are now encapsulated too. canvasProfileIds and
    outputProfileId are no longer public: they are read through pi.canvasProfileIds() /
    pi.outputProfileId() (const accessors) and written only through ProjectItem::addCanvasProfile()
    (the dimension guard) and WorkspaceEditor::setProjectOutputProfile() / removeCanvasProfileFromProject().
    This closes the last bypass: a raw pi.outputProfileId = "garbage" (skipping the id validation) or a
    raw pi.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. Other ProjectItem fields
    (name/uid/inputDirectory/outputSignature/canvasProfileIdsAtRender) stay public — they carry
    no cross-cutting invariant.
  • uuid → uid across InputFile / OutputFile / ProjectItem, and the ids are now genuinely
    unique.
    The field and its JSON key are renamed (these are short local ids like file-<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() and WorkspaceSerializer::load() mint via
    Infrastructure::makeUniqueId and de-duplicate. The GUI's project id (previously a timestamp — also
    collision-prone) now goes through makeUniqueId too.

Added

  • Infrastructure::ProjectEditor (platemaker/infrastructure/project_editor/project_editor.hpp)
    — the authority for a single project's content, twin of WorkspaceEditor. Owns input ordering:
    setInputOrder(orderedUids) (validates a permutation, rewrites each InputFile::order) and
    moveInput(uid, ±1). It never physically reorders the stored input vector — only the order
    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 by order; the CLI passes it to
    ProcessingPipeline::run (the pipeline itself is unchanged — it still renders the sequence it is
    handed, with no knowledge of order). Previously the strip was built in raw vector order, so a
    reorder that only touched order never reached the output.
  • Input-composition staleness axis — ProjectItem::inputOrderAtRender (the input uid sequence, in
    order, captured at render, next to canvasProfileIdsAtRender) and
    ProjectItem::detectInputCompositionChange(). sanitize() marks every output Desynchronized when
    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' sourceMap provenance, so an old reorder is still caught
    without a spurious re-render.
  • WorkspaceEditor::addProject(name) — creates a project with a workspace-unique proj- uid and
    appends it. Minting the project uid is now the lib's job: the CLI (and GUI) no longer hand-roll it
    with makeUniqueId (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::ProcessingCallbacks and 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 / AppendedProfileNotLinked for an implicitly-rendered page,
    the latter carrying the matching-but-unlinked workspace profile ids; or SkippedMissing / SkippedError.
    The SkippedNoProfile / SkippedProfileNotLinked values 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;
    and onSliceSkipped(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 up Processed; see the
    implicit-render entry under Changed.) Distinct from Missing and Processed. Set by
    applyProcessingResults() (above) and reported live during a render via the onInput callback;
    a consumer switching over FileStatus must 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 (replaceOutputProfiles also
    strips persisted presets); the project-link pair addCanvasProfileToProject /
    removeCanvasProfileFromProject (the symmetric remove the model lacked); and setProjectOutputProfile
    (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 dragging id_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 ID on process and project. The canvas
    commands are renamed *-canvas-profile; the old add-/mod-/rm-/list-profiles names are removed
    (breaking). list-canvas-profiles and list-output-profiles each list only their own family;
    the combined view is list-all-profiles (alias list-profiles). process uses 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() and BuildInfo
    (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-time version.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_string against the loaded buildInfo().version, so a consumer can detect a DLL that
    does not match the header it built against (zlib's ZLIB_VERSION / zlibVersion() idiom).
  • Infrastructure::linkedComponents() and LinkedComponent — 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::url is
    guaranteed to be a https://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 under credits/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 the platemaker_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() keeps FileStatus::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 to Processed, a never-rendered one to Pending). 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 outputs Done) 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 treats Skipped as settled. Fixes the GUI reopening a project with all-Done outputs 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 from SameMajorVersion to SameMinorVersion: with major 0, SameMajorVersion treats
    every 0.y as compatible, so a consumer pinned to 0.3.0 would silently accept an incompatible
    0.4.0 — but under semver a 0.MINOR bump is exactly where a pre-1.0 breaking change lives (as
    0.2.0 and this 0.3.0 both were). The package now accepts only the same 0.MINOR.*.
  • The package rejects an ABI-incompatible toolchain at find_package time. The generated
    platemaker-config.cmake records the build compiler and fails with an actionable FATAL_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. Previously find_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-…).