Derived artifacts
Refine, pack, mount, export, hand-edit companions, approval provenance, and recovery.
PixelKiln turns validated source PNGs into application-ready bundles without a provider call:
pack: deterministic grid sheet plus frame atlas, or Aseprite sheet JSON, or a GodotSpriteFramesresource;mount: declared stable cells in a new or existing sheet, same formats;export: structural tile atlas plus generic, Tiled, or Godot metadata;refine: native-grid PNG or ordered frame set plus palette, audit, and human-review record.
Every CLI bundle includes generated output, metadata, and a
<base>.pixelkiln.json provenance companion.
Pack
pixelkiln pack --style neon
pixelkiln pack --style ground --output-role tile-00 --output-role tile-01
pixelkiln pack --style mixed --primary-only
Manifest mode normally reads the outputs recorded by the lockfile. An asset
with a source (or a sourceByStyle entry for this style) is packed from that
file instead (a hand edit made with pixelkiln edit, a palette remap, an
alignment fix), and an asset with a source but no lock entry is packed from it
too, the same rule mount applies. Structural sets keep their lock outputs.
When a style declares quality, it instead requires every selected refinement
record to be current and approved, then packs those derived PNGs. Frames sort by asset id for
byte-stable layouts. Structural sets preserve provider order and qualify ids by
role (terrain/tile-03). The grid cell is the largest source sprite; smaller
frames retain their real dimensions at the cell's top-left — except for a
character style, whose frames are bottom-centred in the cell instead, so a
base's rotations and a taller state or v3-interpolated animation frame (a
crouch loop can come back on a canvas larger than the rotations; see
docs/ENDPOINTS.md, "Characters, measured") share a floor rather than a
top edge. --format aseprite records that offset as a genuine trim
(sourceSize the cell, spriteSourceSize the frame's placement within it)
so an engine reading the sheet lands every frame on the same floor too.
All standard non-interlaced PNG color modes and bit depths are decoded and normalized to RGBA. Corrupt/interlaced inputs are reported as skipped. If no input is readable, packing fails rather than emitting an empty sheet.
The generic atlas lists sets when a multi-output asset is on the sheet:
"sets": [
{ "id": "dancer", "kind": "frames", "fps": 12, "frames": ["dancer/frame-00", "dancer/frame-01"] },
{ "id": "terrain", "kind": "members", "frames": ["terrain/tile-00", "terrain/tile-01"] }
]
--format aseprite writes that same information as Aseprite's sheet JSON
(each set a frameTag, frame durations from the fps) and --format godot
as a SpriteFrames .tres (each frame set a looping animation at its fps,
every other frame a one-frame animation). The sheet PNG is the same file in
every format; the companion records options.format.
Explicit mode packs sources from any project and requires no manifest:
pixelkiln pack --inputs sprites.json --out dist/sheet
[
{ "id": "effect_euphoric", "path": "art/effect_euphoric.png" },
{ "id": "aura_golden", "path": "../shared/aura_golden.png" }
]
Paths resolve from the inputs JSON directory. Duplicate ids fail before file decoding because consumers must have one unambiguous frame per id.
Refine
refine can run the profiles declared in a manifest or process one --from
PNG. Both modes call a pinned local Pixel Art Fixer installation, reconstruct
the detected native grid, apply a closed palette without dithering, and write
the PNG beside a quality companion.
Manifest mode owns the output path and policy. ComfyUI frames writes numbered
role-stable PNGs under one set-level record:
pixelkiln refine --style environment
pixelkiln refine check --style environment
It preserves a current pending or approved record on repeat. A profile or source
change rebuilds the result and resets approval; --force deliberately does the
same for current work.
Path mode is useful for one-off input:
pixelkiln refine \
--from candidates/fortress.png \
--out art/fortress-native.png \
--palette "#141b1e,#23312a,#4e3b34,#865c45,#c6a766,#f1bb70" \
--fixer-python .pixelkiln/pixelfixer/bin/python
The companion uses artifact kind refine. Its options store the tool revision,
detected step, phase, native dimensions, confidence, exact palette, no-dither
policy, audit thresholds and result, plus pending or approved review state. A
frame record also stores fps, ordered roles, and every frame's measurements.
pixelkiln refine check verifies the fingerprint and current source/output
hashes. pixelkiln refine approve rewrites only the companion after a named
person completes the native-scale review.
An actual rebuild resets approval to pending. A manual PNG edit, palette or tool
metadata edit, changed source, or altered approval invalidates the record.
For one-off refinement, use the PNG as an asset source when mounting it. A
manifest quality profile routes approved output into pack and mount
automatically and binds the quality record into their provenance.
Mount
pack moves cells when the sorted input set changes. mount is for sheets
whose cell coordinates are already load-bearing in scene files, saved data, or
engine configuration.
Declare style.mount and per-asset cell values in the manifest. A base sheet
is optional and may equal the output. Only declared cells are cleared/replaced;
all other base pixels survive byte-for-byte. A sprite larger than its cell is
reported and skipped rather than cropped. Two assets cannot own one cell.
Use asset source (or sourceByStyle for one style) when mounting a
palette-remapped, aligned, hand-touched, or otherwise post-processed file
instead of the raw lock output; pixelkiln edit sets it up for you. A style quality
profile takes precedence for participating cells, but only after approval. Use
outputRole to choose one member of a structural set. See
manifest reference.
The same rule applies to an approved ComfyUI frame set: pack includes every
frame, while mount needs one explicit outputRole for the declared cell.
Export
pixelkiln export --style ground --only terrain --format generic
pixelkiln export --style ground --only terrain --format tiled
pixelkiln export --style ground --only terrain --format godot
Generic JSON retains all provider rules and normalized masks. Tiled and Godot translate recognized four-edge/four-corner semantics and reject unknown or lossy mappings. See TILES.md for the format contracts.
Hand-edit companion
A hand edit saved from the gallery's in-browser editor leaves a small record
beside it, <edit>.edit.json (beside the stem for a frame set), that is not a
provenance companion and is not transactional:
{
"version": 2,
"editor": "pixelorama@v1.2.2-stable",
"protocol": 3,
"savedAt": "2026-09-12T10:00:00.000Z",
"project": "walk.pxo",
"outputs": [
{ "role": "frame-00", "basedOn": "<sha256 of the generated frame>", "sha256": "<sha256 as saved>" },
{ "role": "frame-01", "basedOn": "…", "sha256": "…" }
]
}
One outputs entry per file, in output order; a single image has one with
role: null. basedOn lets the gallery report regenerated-since by
comparing hashes rather than modification times (null when the edit started
from untracked art); sha256 shows when another tool rewrote a file after
the editor did; project names the layered Pixelorama file kept beside the
edit, which the next in-browser edit opens so layers survive. A version 1
companion (one image, flat basedOn/sha256) is still read. An edit made
with a desktop editor has no companion and is judged by file times. Commit
the companion and the .pxo with the edit if you want that history; nothing
else reads them.
Provenance companion
The companion is engine-neutral and versioned:
{
"format": "pixelkiln-artifact-bundle",
"version": 1,
"kind": "pack",
"fingerprint": "…",
"sources": [
{ "id": "anvil", "path": "../art/anvil.png", "sha256": "…", "included": true }
],
"options": { "columns": 8, "order": "id", "style": "base" },
"outputs": [
{ "path": "sheet.png", "sha256": "…" },
{ "path": "sheet.json", "sha256": "…" }
]
}
Source paths are relative to the companion. Options are recursively key-sorted
before hashing. The fingerprint covers sources, options, and output hashes.
Manifest-driven bundles conservatively include the project manifest and
lockfile, so newly declared or recorded sources make an older artifact stale.
Profile-driven pack and mount bundles also include one $quality/<assetId>
source per refinement record. Changing an approval or its bound bytes makes the
downstream bundle stale without hiding which gate changed.
Verify without rebuilding:
import { verifyArtifactBundle } from "pixelkiln"
const verification = await verifyArtifactBundle("dist/sheet.pixelkiln.json")
if (!verification.current) {
console.error(verification.changedSources, verification.changedOutputs)
}
Ownership and manual edits
PixelKiln never silently claims a differing existing destination:
- No companion + identical desired bytes: adopt it, preserve its mtime, and add provenance.
- No companion + differing bytes: refuse the entire bundle.
- Valid companion + matching recorded output: update normally.
- Valid companion + manually changed output: refuse the entire bundle.
- Invalid or altered companion: refuse takeover.
After reviewing the difference, --force explicitly replaces and takes
ownership of every changing member:
pixelkiln pack --style neon --force
Library consumers get the same policy from
writeManagedArtifactBundle(..., { force: true }).
Transactional writes
Changed members are compared first, staged beside their destinations, and only then promoted. Duplicate normalized destinations fail before mutation. Byte-identical members are not rewritten. If an ordinary write/promotion error occurs, newly promoted files are removed and previous backups are restored. Any recovery failure retains the backup and reports its exact path.
Abrupt termination recovery
Managed writes create an immutable <companion>.transaction journal before
staging. After every member is promoted, a separate durable commit marker is
created before cleanup.
On the next invocation:
- journal without marker: restore the prior complete bundle and remove stages;
- journal with marker: retain the fully committed new bundle and finish cleanup;
- live owning process: refuse concurrent recovery;
- destination or temp path outside the current bundle: refuse unsafe recovery and retain the journal for inspection.
The journal is short-lived and should not normally appear in Git status. A hard-exit integration test covers both sides of the commit boundary.
Library persistence
packSprites, packStyle, mountSprites, mountStyle, and exportTileset
return bytes, metadata, skipped-input details, and source hashes. Use
writeManagedArtifactBundle for the CLI's ownership, provenance, transaction,
and recovery policy. writeArtifactBundle is the lower-level staged/rollback
primitive for callers that supply their own ownership policy.
