Repository navigation
Workspace
A workspace is the unit a user opens and saves: one JSON file holding the profile palettes and every project made with them. It is deliberately self-contained and path-agnostic — the library never decides where it lives, which is what lets a consumer put it on a synced drive, beside the artwork, or anywhere else.
This page covers what a workspace holds, how it is serialised and repaired on load, and the file and directory conventions around it.
- Reads and writes
Workspaceto/from JSON via nlohmann/json - File includes a
"version"field (start at1) for future migration - On load: if version < current, run migration chain
- Persists
processedFilesmap (SHA-256 + sourceMap) for incremental processing -
Paths relative to the workspace file. The model holds every path absolute; the relative form exists
only in the file, translated by the serializer — the one piece of code that knows where the file is —
so a workspace folder can be moved, renamed or zipped and still open:
- An overlay whose asset lies inside the file's folder is written with both
assetPath(absolute, as always) andassetPathRelative. On load the relative one wins when it names a file that exists; otherwise the absolute path stands. A reader from before the key ignores it and keeps using the absolute path, so the change is additive and the schema version is unchanged. - A path with no root at all (
overlays/x.svg,pages/001.png) is read against the file's folder, in an overlay'sassetPath, an input'sfilePathand an output'ssourceMap(which records the input'sfilePathverbatim and is matched against it as a string, so the two are read alike). Neither this library nor its consumers write such paths — the CLI and the GUI store absolute ones — so this is for hand-written or packaged files. A root-relative path (/tmp/x) is left alone. -
outputDirectoryis taken as written: a CLI--outputgiven relative is resolved against the working directory at render time, and reading it against the folder instead would move the output. -
serialize()carries no relative keys — it has no location — so change detection, which compares twoserialize()results, is the same wherever the workspace is saved.
- An overlay whose asset lies inside the file's folder is written with both
- Plans a package: one root holding the workspace and every file it references, so it opens on
another machine with no path fixing. The copy is the same workspace with its paths rewritten, not a
new one: project, input and overlay uids stay (overlays anchor to input uids, so a page renamed on a
clash moves no bubble), and every
sha256stays, since the bytes are copied, not re-encoded. -
plan(workspace, workspaceFilePath)→PackagePlan: the copy's file name and JSON, the files to copy (sourceabsolute →targetin the package), and the referenced files that are not on disk.-
Layout: the workspace folder, mirrored. The library knows no subfolder of a workspace by name, so
a file in the folder keeps its path there. Only files from outside the folder are given a place —
the only names the library chooses:
inputs/for pages,external/for anything else, flat, a file name kept and suffixed(2)only on a case-insensitive clash. The folder's own files are placed first, so nothing from elsewhere displaces one;package.jsonand the workspace file's own name are reserved at the root. A file referenced twice is packed once. An unpacked package is the folder again, so a change of layout is migrated in one place — where the consumer opens a folder. - "In the folder" is decided by
relativeInside()(src/infrastructure/file/folder_paths.hpp), the same functionsave()uses forassetPathRelative, on canonical paths. - Every path in the copy is rootless, which
load()reads against the package root — no loader of its own. -
Nothing a render reproduces: no output files, no output directory (workspace or project), no
input directory, none of the render baselines (
outputSignature,processingSignature,canvasProfileIdsAtRender,inputOrderAtRender), and each input isPendingwith nosha256,contributesTo,lastProcessedorthumbnailPath. An input'ssha256is the hash its last render saw —sanitize()reads a matching one as processed — so kept, a copy with no outputs would look rendered and up to date. An overlay'ssha256names its content and stays. A package opens as a workspace not yet rendered. -
A missing file does not stop the plan. It stays declared in the copy, pointing at the place it
would have had (so dropping the file there brings it back), and is listed in
missing.
-
Layout: the workspace folder, mirrored. The library knows no subfolder of a workspace by name, so
a file in the folder keeps its path there. Only files from outside the folder are given a place —
the only names the library chooses:
- Two phases: a consumer appends its own
PackageFiles to the plan — files the model does not name — before it is archived. The library never decides which of a consumer's files belong in a package. -
write(plan, zipPath, progress, cancel)archives it (libarchive):-
Checked first: every target is relative and stays inside the package (no root, no
.., no\), no two collide (case-insensitively), and the consumer's manifest part is a JSON object. - UTF-8 names, flagged as such; ZIP64 where needed. PNG, JPEG and WebP are stored, the rest deflated.
- Written to
<zip>.tmpand renamed over the target only when whole; an error or a cancel removes the temporary file and leaves an existing package untouched. Cancel returnsfalse. -
package.jsonlast:format,library(name, version),application(the consumer's name, version anddetails— its own JSON object, recorded as is),exported(UTC),workspace(the copy's file name) andmissing.
-
Checked first: every target is relative and stays inside the package (no root, no
-
unpack(zipPath, folder, progress, cancel)→ the workspace file and thePackageManifestread back:- A new folder only — refused if it exists, so nothing is overwritten and it holds one workspace.
-
Whole or not at all: into
<folder>.partial, renamed at the end; a failure or a cancel removes it (cancel returnsnullopt). A.partialleft by a crash is refused, not reused. -
Nothing outside the folder: an entry with a root or a
.., one that is not an ordinary file, or two for one name (without case) is refused. -
It must be a package:
package.jsonnames a workspace file at the root that was unpacked too. The workspace is not loaded — that is the caller's next step, throughload()as usual.
id : uid // stable identifier — never changes after creation
name : string // e.g. "Webtoon 4-page" or "Marvel Standard" (future)
canvasSize : Size // full canvas including margins, e.g. 1600×10240
margins : Margins // top/right/bottom/left in px
safeArea : Size // computed: canvasSize minus margins (read-only)
dpi : double // resolution the artist draws at, default 300; the template records it,
// the render ignores it (it works in pixels)
visualColour : RGBA // template overlay colour, e.g. #FF69B4 at 50% alpha
templateSettings // how the template is written:
format : enum // PSD (default) | PNG
emptyLayer : bool // PSD only: an empty "Layer" above the template, default false
templates : [CanvasTemplateInfo] // the generated files, at most one per format (PNG beside PSD)
path : string // relative to the workspace folder
format : enum // PNG | PSD — what the file is
fingerprint : string // the template's signature when it was generated (canvasSignature)
sha256 : string // hash of the bytes written; empty = unknown (migrated from version 2)
generatedAt : string // ISO 8601
id : uid // stable identifier — never changes after creation
name : string // e.g. "Webtoon Standard"
targetWidth : int // e.g. 800
sliceHeight : int // e.g. 1280
lastSlicePolicy : enum // CROP | PAD_WHITE | KEEP_AS_IS
outputFormat : enum // PNG | JPEG | WEBP
jpegOptions : JpegOptions // only used when outputFormat == JPEG
startIndex : int // output file numbering start, default 1
dpi : double // resolution written into every slice, default 72 (all presets: 72);
// PNG pHYs, EXIF for JPEG and WebP — never the inputs' resolution
quality : int // 1–95, default 90
subsampling : enum // YUV_444 | YUV_422 | YUV_420
optimize : bool // default true
progressive : bool // default false
Format and its encoding options belong to the OutputProfile (the reusable,
per-project-selected output config referenced by ProjectItem.outputProfileId) —
not a separate per-project field. OutputProfile carries one option struct per
format; ImageIO::encode() applies the one matching outputFormat.
compression : int // 0–9 zlib level, default 6
interlaced : bool // Adam7 interlacing, default false
quality : int // 0–100, default 80 (ignored when lossless)
lossless : bool // default false
effort : int // 0–6 compression/method effort, default 4
The GUI Output tab edits these on the project's selected profile inline
(groupBoxPNG/groupBoxJPG/groupBoxWebP); the same profile is also editable via
Manage Output Profiles.
Distribution platforms cap output size — e.g. Webtoon: ≤ 2 MB per slice,
≤ 25 MB per chapter. The pipeline should be able to estimate average and
maximum slice size and total batch size before a run (from canvas/scaling/slice
geometry and the chosen encoder settings) and/or report actual sizes after.
The lib is the natural owner of the estimate (it knows geometry + encoding); the
GUI surfaces it and warns when a cap would be exceeded. Tracked in both
docs/TODO.md files.
version : int // schema version, start at 1
canvasProfiles : CanvasProfile[] // global palette — all defined profiles
outputProfiles : OutputProfile[] // global palette — all defined profiles
projects : ProjectItem[] // each project has its own profile selection
outputDirectory: string // workspace-level default (overridden per project)
Workspace file format: UTF-8 JSON, saved as <project-name>.platemaker.json.
Migration note (schema v1 → v2): Earlier builds stored
activeCanvasProfileNameandactiveOutputProfileNameat workspace level, plus a flatpages: PageItem[]list and a top-levelstripDirty/processedFiles. These fields are replaced byProjectItemin schema v2. TheWorkspaceSerializermigration chain must convert old documents by wrapping the flat page list into a singleProjectItemand resolving the active profile names to profile ids.
ProjectItem.stripDirty is set to true whenever: page order changes, a page is
added/removed, or the project's outputProfileId changes. When true, the entire
strip for that project is reprocessed regardless of individual file hashes.
platemaker/
├── CMakeLists.txt
├── CMakePresets.json # windows-msvc, linux-gcc, windows-mingw presets
├── vcpkg.json # manifest: vips, nlohmann-json, gtest
├── lib/ # libplatemaker — Core + Infrastructure (zero Qt)
│ ├── cmake/ # CMake package config template
│ ├── include/platemaker/
│ └── src/
├── cli/ # platemaker binary
├── tests/ # Unit tests (mock ImageIO)
└── docs/
└── SPECIFICATION.md
Output files:
<outputDirectory>/
output_001.png
output_002.png
...
Workspace: <project-name>.platemaker.json (user-chosen location).
Cache: .platemaker-cache/ sibling to .platemaker.json (auto-created, safe to delete).
See also: Home · Workspace · Project · Render · Preview · Canvas-Profiles · Processing-Steps · Components · CLI · Platform · Roadmap · Diagrams