Give a photograph 256 colours and watch every compromise become a byte. Your image stays in this browser while median cut and fixed-point refinement build its palette, CIEDE2000 measures the changed pixels, and a real GIF encoder exposes its LZW dictionary and exact file size. The concession is visible beside the result: lower per-pixel error need not look better, and these bytes belong to this encoder rather than the smallest GIF possible.
A GIF does not remember the millions of colours a camera can hand it. For each image block it points every pixel at a table with no more than 256 entries. Choosing that table loses colour. LZW comes later and loses nothing: it codes the chosen table indices as repeated phrases.
First, make the standard answer
Before this page accepts an image, a deterministic 24,000-index fixture crosses every GIF code-width boundary. The independently written decoder has to return every index. These values come from Appendix F of CompuServe's 31 July 1990 GIF89a programming reference.
...Clear code
...EOI code
...first free
...starting bits
...largest code used
m = 8. Clear = 2m = 256. EOI = Clear + 1 = 257. First free = Clear + 2 = 258. Initial width = m + 1 = 9 bits. The 12-bit ceiling is 212 - 1 = 4095 = 0xFFF.
transition
encoder boundary
trace position
running
Each trace token is code/width. The stream starts with Clear 256, ends with EOI 257, packs least-significant bits first, splits payload into blocks of at most 255 bytes, adds a zero block, then trailer 0x3B.
The fixture alternates high-entropy stretches with runs, on purpose. A stream of pure noise crosses every width boundary but never once reaches the case Welch describes on page 16 of his 1984 paper, where a code equals the decoder's own next free code. That branch is the only place a decoder built on the literal reading of Appendix F diverges, so a fixture that misses it can be passed by a wrong decoder. This one reaches it ... times and fills and clears the full 12-bit table ... times.
The file stays here
This is inspectable evidence, not a cryptographic proof that page code is kind. The inline Content Security Policy sets connect-src 'none', forms and workers are disabled, all program and font files are same-origin, and the instrument has no request API path. Open your browser's Network panel if you want the stronger view. form-action 'none' is deliberately absolute, which has a visible cost: the search box in the site banner above cannot submit from this page. Use Ask the Wasteland directly.
...HTTP(S) resource entries since navigation
...new entries after baseline
loadlast measured action
Measuring load baseline
Give it pixels it has never seen
Drop one PNG, JPEG, WebP, BMP, GIF87a, or GIF89a. Within the displayed byte cap, the complete File is read into memory. Non-GIF files go through this browser's image decoder. GIF bytes also go through this page's bounded parser, including local tables, interlace passes, transparency, disposal, and every image block within the displayed animation caps.
Or drop one file here. Nothing is posted.
Preparing the shipped fallback specimen...
fallback......
Safety caps: 268,435,456 file bytes in memory, then 4096 × 4096 = 16,777,216 decoded pixels. Files above either are not called invalid. This instrument declines them before the corresponding large allocation.
The uploaded GIF, before re-encoding
original byte class
bytes or count
The palette, then the bill
Median cut begins with a 15-bit histogram, five high bits from each sRGB byte, and splits occupied boxes at weighted medians. The middle view is that raw heuristic. The right view alternates nearest-centre assignment and centroid updates, the fixed-point step Heckbert describes. No dithering is used.
... active palette entries. If the image cannot split into K occupied boxes, remaining table entries are deterministic black padding and are marked as padding in their swatch titles.
CIEDE2000 error map
browser decodes emitted GIF
independent index decoder
...
The decoder does not share dictionary mutation code with the encoder. The browser rendering is visible corroboration, not this check.
CIEDE2000 is a per-pixel difference formula. It misses contouring, spatial structure, viewing size, and preference. A lower mean does not guarantee a better-looking image. The map and 95th percentile stay beside the mean so one average cannot hide the tail.
A heuristic can still keep an account
Heckbert does not call median cut optimal. The refinement reaches a fixed point, which can be only a local minimum. What it can promise here is narrower: for each accepted step, squared byte-RGB distortion D does not rise.
Raw median cut
D
...
mean CIEDE2000
...
GIF bytes
...
Refined, ... step(s)
D
...
mean CIEDE2000
...
GIF bytes
...
iteration
D
change
A centroid may stay where it was if its cluster empties. Real-valued centres set assignment and D; emitted palette triplets are rounded to bytes. Stable assignments or 20 accepted iterations stop the process.
The promise is about D and nothing else, and the shipped fallback shows why that matters. At K = 4 the refinement lowers D from 1,821,097,567 to 1,651,870,986, which is what it guarantees, and in the same step the mean CIEDE2000 gets worse, from 19.942 to 19.989, while the file gets smaller, from 11,119 to 10,459 bytes. Squared byte-RGB distance and a perceptual difference formula are two different objectives, and moving one is not moving the other. Run the sweep on your own file and read the two columns separately.
Colour error is not file size
Run eight complete encodes of the same pixels. More colours can lower per-pixel error while making the index stream less repetitive and the LZW payload larger. That is a possibility, not a law. The curve below belongs to your specimen and this deterministic encoder.
Run the sweep to draw the measured frontier.
K
mean error
95th percentile
complete GIF
LZW payload
Every byte has a drawer
Re-encoded file ledger
class
bytes
LZW stream facts
Widths observed: ...
Sub-blocks: ...
Final EOI width: ...
Self-referential codes in your stream: ...
Round trip: see the independent decoder above
The encoder emits Clear when its 12-bit table is full. Deferred clearing is legal, but is not this encoder's policy. The image is not interlaced. Palette padding is black and deterministic.
The dictionary it built
These are the entries the independently written decoder created while reading your file's compressed stream, not a diagram of LZW. Each row is one dictionary entry: an existing code, one appended palette index, and the phrase that entry now stands for. ...
code
prefix code
+ index
phrase length
phrase, as palette indices
Codes 0 through ... are the literal palette indices and are not dictionary entries. Clear and End of Information sit immediately above them, and the first entry a stream can create is the code after those two.