DEV Community

NullPointerZen
NullPointerZen

Posted on

toBlob('image/webp', 1.0) is lossless, and 3.5 bigger than 0.999

I came into tech from content work, not a CS degree, and most of what I build is small internal stuff for the editors I used to sit next to. One of those tools takes a screenshot and makes it fit the upload limit on our CMS. It has a "keep it sharp" option, and for that option I passed 1.0 as the WebP quality, on the logic that 1.0 is simply the top of the lossy scale, one notch above 0.99. Files from that option came out far bigger than I expected, and at first I blamed the screenshots. They were ordinary text pages. So I sat down and measured what the top of the scale actually does.

What happens at WebP quality 1.0

The machine was an Apple M4 with 16 GB, running a Chromium 149 open-source build, and everything went through the browser's own canvas.toBlob. My test image was a 1440×2400 screenshot of a help page, mostly text and flat UI. At quality 0.99 the WebP was 214,968 bytes. At 0.995 it was 222,292, and 0.999 gave exactly the same 222,292. Then 1.0 gave 771,126 bytes, about 3.5 times the 0.999 file. That jump didn't look like "a bit more quality", so I decoded the 1.0 file back to pixels and compared it with the source. Out of 13,824,000 channel values, zero were different. Every pixel matched. In this browser, quality 1.0 for WebP isn't the best lossy setting. It switches the encoder to lossless mode.

Repro page: WebP at 0.999 and at 1.0 on the same screenshot, byte sizes and a pixel diff against the source

Look at the two size lines first, then the diff counter underneath. The 1.0 file is the big one, and its diff against the source is 0. The 0.999 file is a normal lossy WebP. The screenshot is from a single run.

A photo makes the jump even bigger. On a 3840×2160 landscape photo (one of the stock wallpapers that ship with the OS), 0.98 came out at 1805 KB and 1.0 at 7787 KB, 4.3 times as much. JPEG doesn't do this. For JPEG, 0.995, 0.999 and 1.0 all produced the same 791,712-byte file for the screenshot, so there's no hidden mode at the top, just the last step of the normal scale. As a non-specialist, that's the part I find a bit unfair. It's two formats, one function and the same argument, and at 1.0 they mean different things. Nothing in the call tells you. I only tested Chromium 149 on this one laptop, so I can't say what other browsers do with 1.0.

For comparison I used ImgIng (https://imging.ai/) on the same M4 Mac, Chromium 149. Under the quality slider its compressor lists a recommended value per content type, and for WebP the highest one in its table is 84, for graphics and text. That's a long way below 100, which matched what I'd just measured: past the high 90s you're mostly paying bytes.

Where it bit me a second time

My sharp option wasn't the only place 1.0 could sneak in. The same tool has a "fit under N KB" mode that binary-searches the quality over the integers 30 to 100. With a loose limit, say 2 MB for that screenshot, every step fits, so the search keeps moving up and ends at 100, which is 1.0. It handed back the 753 KB lossless file. A float version of the same search stopped at 0.995 instead and returned 217 KB. The same limit and the same image gave a result three and a half times bigger, only because one search could reach the top value and the other couldn't. The fix I went with is to cap the search at 0.99. I also check the original first, and if it's already under the limit I don't re-encode it at all.

Lossless WebP isn't a bad thing, and for some screenshots it's what you want. I just want it to be a choice somebody makes on purpose. In my tool it's now its own checkbox labelled "lossless", and the quality slider stops at 0.99.

If you pass quality to toBlob anywhere, search your code for 1.0 and for any slider or search that can reach 100 on WebP. Clamp it at 0.99 unless you mean lossless.

Top comments (0)