
I was dropping a sticker onto a white page for a class project, and after I ran it through a 256-colour PNG reducer the soft grey shadow under it turned into a solid black band with a knife-cut outer edge. The test file is a pink ribbon sticker I found on Wikimedia Commons (by K B, CC BY 2.0), a third-party image and not my work, and the crops below only show its top loop. My first guess was that 256 colours were not enough. The palette was already at the maximum, though, so I opened both files in NumPy and looked at the alpha channel instead of the colours.
The alpha channel went from 256 levels to 2
from PIL import Image
import numpy as np
def alpha_levels(path):
a = np.asarray(Image.open(path).convert("RGBA"))[..., 3]
soft = (a > 0) & (a < 255)
print(path, len(np.unique(a)), soft.sum(), f"{soft.mean():.1%}")
return a
before = alpha_levels("ribbon.png") # ribbon.png 256 225652 17.9%
after = alpha_levels("ribbon_256.png") # ribbon_256.png 2 0 0.0%
The original is 760×1662 and uses all 256 alpha values. 225,652 pixels are semi-transparent, which is 17.9% of the image, and almost all of them sit in the shadow. The reduced file has exactly two alpha values left, fully transparent and fully opaque, with zero pixels in between. Every soft pixel had been forced to pick a side, and the colours had nothing to do with it.
Everything was split at alpha 128
was_soft = (before > 0) & (before < 255)
print((after[was_soft] == 255).sum(), (after[was_soft] == 0).sum()) # 63769 161883
print(np.array_equal(after == 255, before >= 128)) # True
The rule turned out to be a plain threshold. Pixels with alpha 128 or more became fully opaque and the rest vanished, and a threshold at 128 matched the reducer's output on every pixel. Of the old soft pixels, 63,769 became opaque and 161,883 became transparent. The inner part of a drop shadow is dark and fairly solid, so it survives as pure black. The faint outer fade sits below 128 and gets deleted, which is exactly where the hard edge comes from.
Left is the original on white, in the middle a two-level copy, on the right a 34-level version. Look at the shadow under the loop: the middle panel has a black band and then nothing, while the right one fades out again with a few fine steps. Both copies in the figures come from a quick reducer I wrote myself to compare against, not from any tool. The 34-level file still has 256 palette entries, but 33 of them have an alpha below 255 in the tRNS chunk, and it is only 9.1% bigger than my own two-level file. To put a number on it, I composited each file onto a background and took the average RGB difference from the original over the semi-transparent pixels, on a 0 to 255 scale. On white my two-level copy is off by 44.17 and the 34-level one by 3.48.
A black background hides the problem
This is the part that confused me the longest. Top row is the original and bottom row the two-level copy, on white, black and #1976D2 blue. On black the two rows look almost the same, and the numbers agree: the error is 2.33 on black, 20.79 on blue and 44.17 on white. Only 3.4% of the soft pixels differ by more than 24 on black, against 41.0% on blue and 56.6% on white. The shadow is nearly black, so a black backdrop swallows both the band and the missing fade. Had I only checked the export on a dark canvas, I would have called it fine.
The reducer I had started with was the 8-bit PNG mode in ImgIng, run in Chromium 149 on the build I tested. Its help page says its PNG-8 keeps multi-level alpha, but that export had two levels and an error of 44.07 on white, within 0.3 of my copy. For stickers with soft shadows I now convert to WebP instead, and for that I used ImgIng (https://imging.ai/) at its default quality 84: the ribbon went from 430.7 KB to 98.8 KB, the alpha channel matched the original pixel for pixel, and the error was 0.61 on all three backgrounds. The conversion runs in the browser, and I saw zero non-GET requests during export. I keep PNG-8 for screenshots and flat icons with no soft edges. Before shipping a transparent export I run the alpha count above and look at the file on white and on a colour, not only on black.

Top comments (0)