Repository navigation
Palettes
A palette can come from five places: the one built-in palette, a .hex or .txt file, a GIMP .gpl file, the colours of sprites you already have (from:), or N colours extracted from the image itself. pxw.py makes no network calls, so a palette from Lospec or anywhere else is a file you download and pass in.
Every example on this page quantises cleared.png: the render from Making pixel art after snap and bg, 221 colours. Each pixel goes to the nearest palette colour in OKLab, a colour space where distance follows how different two colours look.
$ python pxw.py palettes
{
"ok": true,
"palettes": {
"pico-8": 16
},
"files": "any .hex, .txt or .gpl file, or from:<png>[,<png>...] for every colour of reference sprites"
}
$ echo $?
0PICO-8's 16 colours ship because they are CC0. With --gpl the command also writes the palette as a GIMP palette, for Aseprite or GIMP:
$ python pxw.py palette cleared.png q.png --palette pico-8 --gpl pico.gpl
{
"ok": true,
"out": "q.png",
"palette": [
"000000",
"1d2b53",
"7e2553",
"008751",
"ab5236",
"5f574f",
"c2c3c7",
"fff1e8",
"ff004d",
"ffa300",
"ffec27",
"00e436",
"29adff",
"83769c",
"ff77a8",
"ffccaa"
],
"used": 8,
"before": 221
}
$ echo $?
0GIMP Palette
Name: q
Columns: 8
#
0 0 0 000000
29 43 83 1d2b53
126 37 83 7e2553
0 135 81 008751
171 82 54 ab5236
95 87 79 5f574f
194 195 199 c2c3c7
255 241 232 fff1e8
255 0 77 ff004d
255 163 0 ffa300
255 236 39 ffec27
0 228 54 00e436
41 173 255 29adff
131 118 156 83769c
255 119 168 ff77a8
255 204 170 ffccaa
The Name: line takes the output image's file name (q from q.png), not the palette's. make names its .gpl after the sprite's id and lists only the colours the sprite uses.
A .hex or .txt file is one hex colour per line:
1a1c2c
5d275d
b13e53
ef7d57
ffcd75
f4f4f4
$ python pxw.py palette cleared.png hex.png --palette mine.hex
{
"ok": true,
"out": "hex.png",
"palette": [
"1a1c2c",
"5d275d",
"b13e53",
"ef7d57",
"f4f4f4",
"ffcd75"
],
"used": 5,
"before": 221
}
$ echo $?
0A GIMP palette:
GIMP Palette
Name: mine
Columns: 4
#
26 28 44 ink
177 62 83 red
239 125 87 orange
244 244 244 white
$ python pxw.py palette cleared.png gpl.png --palette mine.gpl
{
"ok": true,
"out": "gpl.png",
"palette": [
"1a1c2c",
"b13e53",
"ef7d57",
"f4f4f4"
],
"used": 4,
"before": 221
}
$ echo $?
0Either way the colours come back sorted and without duplicates, not in the file's order. A missing file, or a name that is neither built in nor a file:
$ python pxw.py palette cleared.png x.png --palette sweetie-16
{
"ok": false,
"error": "palette not found: sweetie-16 (built-in: pico-8)"
}
$ echo $?
1from: takes every opaque colour of one or more sprites (from:a.png,b.png), so a new sprite matches a set that already exists. The 12x12 chest drawn for this run has five colours:
$ python pxw.py palette cleared.png set.png --palette from:chest.png
{
"ok": true,
"out": "set.png",
"palette": [
"281824",
"78401e",
"a46422",
"c83438",
"f8f0e2"
],
"used": 4,
"before": 221
}
$ echo $?
0In a recipe that makes a set, give each sprite the same from: palette. snap: false and the chest's already transparent border leave it as it is:
{"sprites": [
{"id": "mushroom", "input": "render.png", "palette": "from:chest.png", "crop": {"pad": 1, "size": "32x32"}},
{"id": "chest", "input": "chest.png", "snap": false, "palette": "from:chest.png"}
]} "palette": {
"colours": 5,
"used": 4,
"dither": null
},
That is the mushroom's palette step: five colours offered, four used.
--colours N (in a recipe, "palette": N or {"colours": N}) runs a median cut on the image's own colours. It is deterministic, and it merges colours that look alike afterwards, so it can return fewer than N:
$ python pxw.py palette cleared.png six.png --colours 6
{
"ok": true,
"out": "six.png",
"palette": [
"2b1b26",
"b59c7f",
"c63438",
"e7d5b3",
"e9675e",
"f8ecde"
],
"used": 6,
"before": 221
}
$ echo $?
0Give exactly one of --palette and --colours. Both, or neither, is a usage error with exit code 2 and JSON on stdout:
$ python pxw.py palette cleared.png x.png --palette pico-8 --colours 6
{
"ok": false,
"error": "give exactly one of --palette and --colours"
}
$ echo $?
2Ordered (Bayer) dithering only, off by default: --dither bayer2, bayer4 or bayer8 (in a recipe, "dither": "bayer4"). On the 128x96 painted gradient, PICO-8 without dithering used 9 colours and with bayer4 used 13. The Bayer cell's offset nudges each pixel before the match, so more palette colours get picked:
$ python pxw.py palette painting.png banded.png --palette pico-8
{
"ok": true,
"out": "banded.png",
"palette": [
"000000",
"1d2b53",
"7e2553",
"008751",
"ab5236",
"5f574f",
"c2c3c7",
"fff1e8",
"ff004d",
"ffa300",
"ffec27",
"00e436",
"29adff",
"83769c",
"ff77a8",
"ffccaa"
],
"used": 9,
"before": 11918
}
$ echo $?
0
$ python pxw.py palette painting.png dithered.png --palette pico-8 --dither bayer4
{
"ok": true,
"out": "dithered.png",
"palette": [
"000000",
"1d2b53",
"7e2553",
"008751",
"ab5236",
"5f574f",
"c2c3c7",
"fff1e8",
"ff004d",
"ffa300",
"ffec27",
"00e436",
"29adff",
"83769c",
"ff77a8",
"ffccaa"
],
"used": 13,
"before": 11918
}
$ echo $?
0Error diffusion is not offered; the make skill says it reads as noise on sprites. Any other value is refused by the argument parser, exit 2, on stderr:
$ python pxw.py palette cleared.png x.png --palette pico-8 --dither floyd
usage: pxw.py palette [-h] [--palette PALETTE] [--colours COLOURS]
[--dither {bayer2,bayer4,bayer8}]
[--dither-space {srgb,linear}] [--max-step MAX_STEP]
[--gpl GPL]
input out
pxw.py palette: error: argument --dither: invalid choice: 'floyd' (choose from 'bayer2', 'bayer4', 'bayer8')
$ echo $?
2On Linux, Python 3.14.4 printed the choices without quotes: (choose from bayer2, bayer4, bayer8).
Since 0.8.0 two options change how the dither picks colours. Both are off by default, and the default output is the same as in 0.7.0:
-
--dither-space linear(in a recipe,"dither": {"pattern": "bayer4", "space": "linear"}) chooses between each pixel's two nearest colours in the share that keeps its light. The default,srgb, adds the Bayer offset in sRGB, which brightens. -
--max-step L("max_step": L) leaves a pixel undithered where its two nearest colours differ by more than L in OKLab lightness, so the dither only joins neighbouring steps of a ramp.
A flat 128 grey on black and white shows the difference:
grey.png: 16x16, every pixel 128,128,128; bw.hex: 000000 and ffffff
$ python pxw.py palette grey.png grey-srgb.png --palette bw.hex --dither bayer4
{
"ok": true,
"out": "grey-srgb.png",
"palette": [
"000000",
"ffffff"
],
"used": 2,
"before": 1
}
$ echo $?
0grey-srgb.png: 192 of 256 pixels white (75.0%)
$ python pxw.py palette grey.png grey-linear.png --palette bw.hex --dither bayer4 --dither-space linear
{
"ok": true,
"out": "grey-linear.png",
"palette": [
"000000",
"ffffff"
],
"used": 2,
"before": 1
}
$ echo $?
0grey-linear.png: 48 of 256 pixels white (18.8%)
Grey 128 is about 22% of white's light in linear terms; the 4x4 Bayer cell has 16 levels, and the linear run came out at 3 of 16. The skill's recipe reference gives the same pair of numbers: 75% white in sRGB, about 22% in linear.
Black and white are far apart, so with --max-step 0.12 nothing is dithered and every pixel takes its nearest colour:
$ python pxw.py palette grey.png grey-capped.png --palette bw.hex --dither bayer4 --dither-space linear --max-step 0.12
{
"ok": true,
"out": "grey-capped.png",
"palette": [
"000000",
"ffffff"
],
"used": 1,
"before": 1
}
$ echo $?
0grey-capped.png: 256 of 256 pixels white (100.0%)
The make skill's order of preference: a set's own colours (from:) when the sprite joins an existing set; a named palette or file for a fresh style; {"colours": N} when nothing is fixed. The colour budget defaults to the palette's size; Gates covers the budget gate.
This wiki describes pixelwright 0.8.4 and was last updated on 2026-10-26. The plugin is MIT licensed. Report problems in the issues. On GitHub: release v0.8.4.
Using it
The releases
Contributing
Elsewhere