Skip to content

Palettes

m4bwav edited this page Oct 26, 2026 · 2 revisions

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.

The built-in palette

$ 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 $?
0

PICO-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 $?
0
GIMP 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 palette file

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 $?
0

A 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 $?
0

Either 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 $?
1

A set's own colours

from: 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 $?
0

In 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.

Extracting N colours

--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 $?
0

Give 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 $?
2

Dithering

Ordered (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 $?
0

Error 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 $?
2

On Linux, Python 3.14.4 printed the choices without quotes: (choose from bayer2, bayer4, bayer8).

Linear light and a contrast cap

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 $?
0
grey-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 $?
0
grey-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 $?
0
grey-capped.png: 256 of 256 pixels white (100.0%)

Choosing

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.

Clone this wiki locally