Inside a pngquant PNG: 23 palette colors, 6 tRNS bytes, 7 alpha levels
A 512×512 transparent icon from Wikimedia Commons went from 23,375 bytes to 4,171 bytes through pngquant at --quality=65-80. On 2026-11-05 I opened the output chunk by chunk to see where the bytes went. Almost nothing li
A 512×512 transparent icon from Wikimedia Commons went from 23,375 bytes to 4,171 bytes through pngquant at --quality=65-80. On 2026-11-05 I opened the output chunk by chunk to see where the bytes went. Almost nothing lives in the palette: PLTE holds 23 colors in 69 bytes, tRNS is 6 bytes, and the IDAT stream is 4,015 bytes. What I wanted to write down is why tRNS is 6 bytes and not 23, and why the file still carries 7 levels of transparency.
The files are the 6 transparent PNGs (3 icons, 3 illustrations, all RGBA with real 0 to 255 alpha). They ran on an Apple M4 Mac with pngquant 3.0.3 (--quality=65-80 --speed 4), oxipng 10.2.1 (-o 4 --strip safe) and zopflipng with --filters=01234mepb. For contrast I also opened iLoveIMG's output for two of the same transparent files. When I batch-compress images in the browser I use the image compressor in ImgIng https://imging.ai/ ; that module isn't mine, and as far as I understand it belongs to the same reduce-colors-then-recompress family, but nothing below comes from it. Everything here is from one run, with no visual quality scoring.
Most of the saving happens before deflate sees the data
The original icon is RGBA with 1,035 colors and 240 alpha values. Its IDAT inflates to 1,049,088 bytes, which is exactly 512 rows of one filter byte plus 512 × 4 bytes of pixels. After quantization the image is color type 3 with 8-bit indices, so the same arithmetic gives 512 × (1 + 512) = 262,656 bytes. Deflate now gets a quarter of the data, built from 23 symbols. Lossless tools working on the RGBA original can't touch that: oxipng got the icon to 15,363 bytes and zopflipng to 15,023. Over all 6 transparent files, pngquant alone saved 77.0%, and running oxipng on its output added only 2.1 percentage points.
The palette itself is cheap. Even pngquant's default mode, which kept 256 colors for this icon, pays 768 bytes of PLTE and 62 bytes of tRNS. That file is 8,079 bytes with a 7,168-byte IDAT, so going from 23 to 256 colors grew the palette side by roughly 750 bytes and the index stream by over 3,000.
Opaque palette entries at the end cost nothing
For palette images the PNG spec gives tRNS one alpha byte per palette entry and lets it be shorter than PLTE; entries past its end count as fully opaque. An encoder that sorts its non-opaque colors to the front can therefore drop every trailing 255. My small reader prints the PLTE entry count, the raw tRNS bytes and the distinct alpha values, counting the implied 255 when tRNS is shorter than the palette.
import struct, sys
def peek(fname):
blob, pos, got = open(fname, 'rb').read(), 8, {}
while pos < len(blob):
size, tag = struct.unpack('>I4s', blob[pos:pos + 8])
got[tag] = blob[pos + 8:pos + 8 + size]
pos = pos + 12 + size
n, trns = len(got[b'PLTE']) // 3, got.get(b'tRNS', b'')
levels = set(trns) | ({255} if len(trns) < n else set())
print(f'{fname}: PLTE {n} entries ({n * 3} B), tRNS {len(trns)} B')
print(f' tRNS = {list(trns)}' if len(trns) <= 12 else f' tRNS[-4:] = {list(trns[-4:])}')
print(f' alpha levels = {len(levels)}')
for f in sys.argv[1:]: peek(f)
icon-q.png: PLTE 23 entries (69 B), tRNS 6 B
tRNS = [0, 211, 27, 110, 161, 64]
alpha levels = 7
icon-qz.png: PLTE 23 entries (69 B), tRNS 7 B
tRNS = [0, 27, 110, 161, 211, 255, 64]
alpha levels = 7
The first file is the 65–80 output. Six tRNS bytes, values 0, 211, 27, 110, 161 and 64, and the other 17 colors fully opaque, which makes 7 levels. The second one is more interesting. It is the pngquant file after zopflipng, and now a 255 sits at slot six, in front of the 64. zopflipng reordered the palette, my guess is for the sake of the index stream though I didn't check, and once an opaque entry stopped being at the tail it had to write one more tRNS byte. The file still got smaller, 3,713 bytes against 4,171.
Seven alpha levels instead of 240
The anti-aliased edge is coarsened rather than dropped. The Anime girl illustration kept 27 colors, 9 tRNS bytes and 10 alpha levels, down from 256 levels in the original. Across the six transparent files the palettes ran from 6 to 116 colors and the kept alpha from 7 to 58 levels. The apple cartoon is the top end, with 116 colors, 74 tRNS bytes and 58 levels. Each palette entry is a full RGBA tuple, so every alpha level spends a color slot and the quality range sets both budgets at once. Drop the range and you get pngquant's default, which on these transparent files came out 1.36 to 1.94 times the size of the 65–80 output. Whether 7 levels look acceptable on that icon's edge I can't say from bytes; I'd zoom it at 2× on dark and light backgrounds first, and haven't yet.
iLoveIMG made a different set of choices. On the same icon it wrote 23 colors and 7 tRNS bytes in ascending order (0, 27, 64, 109, 156, 198, 231), which is 8 levels, in a 3,924-byte file, and oxipng then found 0 bytes to save. On the Anime girl it wrote 32 colors with a tRNS covering all 32 entries, whose last value is 74, so there was no opaque tail to drop, and 13 alpha levels across them. Its pixels differ from pngquant's; which quantizer it uses, I don't know. TinyPNG describes its own approach as reducing the number of colors; I had no TinyPNG file to open, so it isn't part of this teardown.
So there are three numbers worth reading in a quantized PNG before its file size: how many entries PLTE has, how long tRNS is compared with it, and how many distinct alpha values remain. After that, run oxipng once. If it finds nothing, as with all three of iLoveIMG's PNG outputs I had (one of them a screenshot), the filter and deflate side has little left, and a smaller file would have to come from choices made at quantization.
Sample images from Wikimedia Commons: Afro Mobile Icon by Ben plc (public domain), Anime girl by oksmith (CC0), Apple cartoon with transparent background by Toby8991 (CC0).
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.