I run a couple of small tool sites and take client work on the side, and the asset folder I reuse has two kinds of transparent PNGs in it: stickers with big soft drop shadows, and small flat icons whose only soft part is a thin antialiased rim. Bandwidth is a line on my bill, so I wanted that folder down to 256-colour PNGs. First I ran a trial on two third-party files, one of each kind. The sticker is a pink ribbon from Wikimedia Commons (by K B, CC BY 2.0), and I only show its top loop. The icon is the melting face from the open-source Noto Color Emoji set (Apache 2.0). Neither is my artwork. The short answer is that both break the same way, and they still need different fixes.
Two alpha levels break both kinds
A 256-colour PNG that keeps only two alpha levels splits every pixel at 128. Alpha 128 and up becomes fully opaque, everything below disappears. On the ribbon that matters a lot, because 225,652 pixels are semi-transparent, 17.9% of the image, almost all of them in the shadow. The dark inner part of the shadow survives as a solid black band and the faint outer fade is deleted, so the edge becomes a knife cut.
Left is the original on white, middle the two-level copy, right a 34-level copy, all at 2x. Under the loop, the middle panel goes from black straight to nothing, while the right one still fades out with a few fine steps. I scored each copy by compositing it onto a background and taking the average RGB difference from the original over the semi-transparent pixels, on a 0 to 255 scale. On white the two-level ribbon is off by 44.17.
The emoji surprised me more. It has only 17 alpha levels and 1,548 semi-transparent pixels, 0.59% of the image, so I expected two levels to be harmless.
This is the outer outline at 4x on blue. The two-level copy in the middle turns the curve into stairs, and the multi-level copy on the right looks like the original. The error is 29.51 on white and 39.06 on blue. On an icon that thin rim is the outline, so you notice when it goes.
Sort the folder by how much of it is soft
So the useful question is how much of each file is soft. I now run this over a folder before compressing anything.
for f in sorted(Path(sys.argv[1]).glob("*.png")):
alpha = np.array(Image.open(f).convert("RGBA").getchannel("A"))
partial = np.count_nonzero((alpha > 0) & (alpha < 255))
share = partial / alpha.size
kind = "sticker: WebP" if share > 0.05 else "icon: PNG-8 ok if alpha kept"
print(f"{f.name:18} levels={len(set(alpha.flat)):3} partial={partial:7,} ({share:.1%}) {kind}")
emoji.png levels= 17 partial= 1,548 (0.6%) icon: PNG-8 ok if alpha kept
ribbon.png levels=256 partial=225,652 (17.9%) sticker: WebP
The 5% line is my own rule of thumb from these two files, nothing official. Files under it get a palette PNG, files over it go to WebP.
Icons can stay palette PNGs with more levels
PNG-8 can store many alpha levels. The tRNS chunk gives each palette entry its own alpha, and Chromium 149, Firefox 151 and WebKit 26.5 all read my multi-level files back exactly as Pillow decodes them. To test it I wrote a quick reducer of my own, not any real tool. With 20 alpha levels the emoji error on white drops to 2.23, and the file grows from 20,370 to 21,406 bytes, 5.1% more. For an icon that's an easy trade. Dithering is no substitute. On a two-level file it doesn't touch alpha at all and only made the files 1.6 to 2.1 times bigger.
To check an export I read tRNS straight from the file.
def trns_entries(path):
blob = open(path, "rb").read()
off, color_type, trns = 8, None, b""
while off < len(blob):
ln, tag = struct.unpack(">I4s", blob[off:off + 8])
payload = blob[off + 8:off + 8 + ln]
if tag == b"IHDR":
color_type = payload[9]
elif tag == b"tRNS":
trns = payload
off += 12 + ln
return color_type, len(trns), sum(1 for v in trns if v != 255)
emoji_2lv.png: color type 3, tRNS 255 bytes, 1 entries below 255
emoji_multi.png: color type 3, tRNS 255 bytes, 33 entries below 255
One entry below 255 means only a single fully transparent colour, which is the two-level case. Careful with the other line, though. My 20-level emoji has 33 entries below 255, because palette slots are not the same as alpha values the pixels actually use. So I treat tRNS as a quick flag and use the pixel count from the first script for the real number.
Stickers go to WebP
For the ribbon, 34 levels still leaves visible steps in the shadow at 4x,. WebP keeps the full alpha channel. For the conversion I used ImgIng (https://imging.ai/) at its default quality 84 in Chromium 149. The ribbon went from 430.7 KB to 98.8 KB, alpha matched the original pixel for pixel, and the error was 0.61 on white, black and blue. It runs in the browser, and the network panel showed zero non-GET requests during export. Its 8-bit PNG mode on the build I tested kept only two alpha levels on both files, so soft-edged art doesn't go through that path for me. The emoji as WebP is 12.1 KB with all 17 alpha levels kept. Its 4.02 error comes from lossy colour, not transparency.
What this trial does not cover
Two files is small. Coloured shadows, outer glow and glassy assets are untested, and so are pngquant and other real reducers. Also check on white or a colour, never only on black. The ribbon's two-level copy scores 2.33 on black because a near-black shadow disappears into it. Before you shrink your own folder, run the soft-pixel count and look at a few results on a white page.

Top comments (0)