# pixfix — complete plain-text manual
Rust CLI + desktop app (pixfix) that turns AI-generated "pixel art" — where
the apparent pixels are misaligned, noisy, and rendered at fractional sizes —
into clean, grid-aligned game assets.
Install: cargo install pixfix
Binary releases: https://github.com/lovelaced/pixfix/releases
Browser demo: https://lovelaced.github.io/pixfix/#demo — the pipeline
compiled to WebAssembly, entirely client-side (images never leave the page).
## PIPELINE (stage order)
1. Grid detection — estimates pitch (pixels per logical pixel, may be
fractional, e.g. 10.667) and phase per axis from gradient profiles.
Below a confidence floor it declines to snap and reports a best guess.
Overrides: --grid-size (fractional ok), --grid-phase X,Y.
2. AA removal (opt-in via --aa-threshold) — snaps interpolation-artifact
pixels to their dominant neighbor, multi-pass, with a dither guard.
3. Background/chroma mask — border-clustered background detection and/or
global chroma keys build a transparency mask at full resolution.
4. Grid normalization — snap mode paints each block its winning color at
original resolution; reduced modes emit one pixel per block. Alpha
binarizes per block by default. Masked pixels never vote.
5. Mask applied — background/keyed pixels become transparent.
6. Dither detection — alternating color pairs found on the logical grid.
7. Quantization — seeded weighted k-means over unique colors, or snapping
to a fixed palette; detected dither pairs are pinned so they survive.
8. Output sizing — snap keeps original dims; reduced modes emit logical
size x round(pitch) (crisp integer upscale) or logical size with
--logical-size. --target-width/--target-height force exact dims.
## COMMANDS
pixfix process [OUTPUT] [flags]
Normalize one image. OUTPUT defaults to _normalized.
(extension always comes from --output-format, default png — never from
the input). INPUT "-" reads stdin; OUTPUT "-" writes PNG to stdout
(not combinable with --json).
Animated GIFs (frame count > 1) are normalized as one animation: the
grid is detected once on the first frame, the background color is
resolved once, and one palette is extracted from all frames together,
so nothing flickers between frames. Per-frame timing is preserved and
the output is always an animated GIF (derived name
_normalized.gif; --output-format may only be gif or left unset,
anything else exits 2). Static GIFs take the single-image path.
pixfix analyze [--sheet] [flags]
Inspect without writing anything: grid pitch/phase/confidence, logical
size, unique colors, detected background and border coverage. --sheet
adds the auto-split sprite count. Honors grid overrides.
pixfix batch [flags]
INPUT is a directory or glob (extensions match case-insensitively).
Parallel; one bad file never aborts the run. Output paths are planned up
front and collisions rejected (use --preserve-dirs to keep the input
tree, --suffix to change naming, '' allowed). Partial failure exits 6.
pixfix sheet [OUTPUT] [flags]
Sprite sheets. Fixed grid: --tile-width/--tile-height (+ --spacing,
--margin). Auto-split: omit tile dims; tuned by --separator-threshold,
--min-sprite-size, --pad. --output-dir also saves individual sprites;
--no-normalize splits/reassembles without the pipeline. The sheet-level
grid is detected once and rebased per tile.
pixfix palette list|extract|fetch
list: built-in palettes (pico-8, sweetie-16, endesga-32, endesga-64,
gameboy, nes). extract --colors N [-o file.hex] [--overwrite]:
k-means medoid palette from an image, sorted, deterministic.
fetch [-o file.hex] [--refresh]: download from lospec.com,
cached in the platform cache dir.
pixfix tui [INPUT]
Interactive terminal editor; requires building with --features tui.
## GLOBAL FLAGS
--json one JSON result document on stdout; NDJSON progress + logs
on stderr. Stable schema; fields always present (null when
inapplicable).
-q, --quiet nothing but errors.
-v/-vv/-vvv tracing verbosity (stderr). RUST_LOG also honored.
--config P explicit config file (missing path = error, exit 2).
--no-config ignore config files entirely, including cwd discovery.
## PIPELINE FLAGS (process/analyze/batch/sheet)
Note: `-h` prints a curated subset; `--help` lists every flag below.
Grid:
--grid-size N force pitch; fractional accepted (10.667)
--grid-phase X,Y force phase (requires --grid-size)
--no-grid-detect skip detection (requires --grid-size)
--max-grid-candidate N max pitch searched (default scales with image)
--min-confidence 0..1 confidence floor (default 0.35) below which
detection declines to snap and only reports its
best guess; 0 accepts anything. Config key:
[grid] min_confidence.
--coarsen N multiply the FINAL pitch (auto-detected or from
--grid-size) by an integer >= 1. Exact semantics:
detection finds the render quantum (the grid the
generator painted on); the intended artistic
resolution is often a multiple of it. Phase is
preserved so coarse blocks align with the fine
grid. No effect when detection declines (nothing
to multiply). Reported grid values (incl. --json)
show the multiplied pitch; grid_best_guess keeps
the raw detection. Config key: [grid] coarsen.
Downscale:
--downscale-mode M snap (default) | center-weighted | majority-vote
| center-pixel. snap keeps resolution; others
reduce to logical pixels.
--logical-size reduced modes: emit logical resolution instead of
the default integer re-upscale by round(pitch)
--keep-alpha preserve per-pixel alpha instead of binarizing
each block to fully opaque/transparent
Anti-aliasing:
--aa-threshold 0..1 enable AA removal; HIGHER = more aggressive
--aa-passes N max passes (default 3), for wide AA ramps
Color (sources are mutually exclusive):
--palette NAME | --palette-file F.hex | --lospec SLUG | --colors N
--no-quantize skip quantization (conflicts with sources)
--seed N RNG seed; same input + seed = identical bytes
--flatten-dither disable dither detection, pinning, and AA guard
Background:
--remove-bg / --no-remove-bg border-clustered background removal
--bg-color HEX explicit background color (else auto-detected;
gradients/vignettes handled by OKLAB clustering)
--bg-threshold 0..1 border coverage needed for auto-detect (0.4)
--bg-tolerance N OKLAB match tolerance (0.05)
--flood-fill / --no-flood-fill border-connected only (default) vs
global replacement
Chroma keying:
--chroma-key HEX remove this color EVERYWHERE (green-screen
style, interior regions included). Repeatable
for multiple keys. Combines with --remove-bg.
--chroma-tolerance N OKLAB match tolerance (0.05)
Output:
--target-width N / --target-height N exact output dims
--output-format png|webp|bmp|gif encoding + derived extension
(animated inputs: gif only)
--overwrite / --no-overwrite overwrite policy (no-clobber
default, everywhere)
Diagnostics:
--debug-overlay PATH (process only) also write a PNG of the source
dimmed with each block tinted red in proportion
to how contested its color vote was; a wrong
pitch/phase tints the whole frame, while tinting
confined to noisy regions means the grid is
right. Skipped (with a note) when no grid was
applied.
Batch only:
--suffix S output stem suffix (default _normalized; '' ok)
--preserve-dirs recreate input tree under the output dir
## EXIT CODES
0 success
2 usage or config error (clap errors and config problems)
3 input could not be read
4 processing/detection/encoding failed
5 output already exists (pass --overwrite)
6 batch completed with some files failed
## JSON OUTPUT (--json)
process/sheet document fields:
input, output paths as given
grid {pitch_x, pitch_y, phase:[x,y], confidence,
source:"detected"|"override"} or null
grid_best_guess same shape; set when detection declined to snap
logical_size [w,h] or null
output_size [w,h]
frames frame count: 1 for stills, N for animated GIFs
palette "pico-8" | "file:..." | "lospec:..." | "auto:N" | null
colors_before, colors_after unique color counts (null if skipped)
aa_pixels_changed, aa_passes AA stats
bg_removed bool; bg_color "#RRGGBB" or null;
bg_pixels_removed count or null
dither_pairs [{color_a, color_b, alternating_blocks}]
low_confidence_blocks, total_blocks contested-vote stats
warnings human-readable strings
duration_ms pipeline wall time
sheet adds: sprites, tile_size [w,h], sprites_dir.
analyze document: input, size, frames, grid, grid_best_guess,
logical_size, unique_colors, detected_bg, bg_border_coverage,
sheet_sprites, coarsen_candidates, suggested_coarsen, warnings.
frames is 1 for stills; animated GIFs are analyzed on their first
frame and report the animation's frame count.
coarsen_candidates: when a grid is accepted, analyze also snaps at
1x/2x/4x of the detected pitch and reports each factor's contested-block
rate ({factor, pitch_x, logical_size, contested_rate}). The detected
pitch is the render quantum; a low rate at a multiple means the content
also reads cleanly at that coarser artistic resolution.
suggested_coarsen: largest factor whose rate stays within 15 percentage
points of the 1x baseline and below 50% absolute; null when only 1x
qualifies. The choice between qualifying factors is taste — use the
published rates.
batch document: {summary: {total, succeeded, failed, skipped},
files: [{input, output, status:"ok"|"failed"|"skipped", error}]}.
Batch NDJSON progress on stderr: {"event":"batch_started",...} then
{"event":"file_done","index":..,"total":..,"input":..,"output":..,
"status":..,"error":..} per file.
## CONFIG FILE (.pixfix.toml, cwd-discovered or --config)
Precedence: CLI flags > config file > built-in defaults.
[grid] size (fractional ok), phase_x, phase_y, max_candidate,
coarsen, skip
[aa] threshold, skip
[quantize] colors, palette, skip
[background] enabled, color, border_threshold, color_tolerance,
flood_fill, chroma_keys = ["FF00FF", ...], chroma_tolerance
[output] overwrite
[sheet] separator_threshold, min_sprite_size, pad
## RECIPES
Inspect, then process only if a grid was found:
pixfix analyze in.png --json | jq -e '.grid != null' \
&& pixfix process in.png out.png --json
Force a known grid on a low-confidence image:
pixfix process in.png out.png --grid-size 10.667
Detected pitch is the render quantum; snap at 2x for era-honest resolution:
pixfix process in.png out.png --coarsen 2
True-resolution asset with a fixed palette and transparent background:
pixfix process in.png out.png --downscale-mode majority-vote \
--logical-size --palette pico-8 --remove-bg
Normalize an animated GIF (one grid + one palette across all frames,
timing preserved):
pixfix process anim.gif out.gif --colors 16
Green-screen removal (interior regions too), two keys:
pixfix process in.png out.png \
--chroma-key 00FF00 --chroma-key FF00FF
Reproducible batch with preserved directory structure:
pixfix batch "art/**/*.png" out/ --preserve-dirs --seed 7
Pipe through without touching disk:
cat in.png | pixfix process - - --quiet > out.png