DEV Community

yue xing
yue xing

Posted on

canvas.toBlob won't tell you it ignored your format or your quality

For a side project I added a "shrink before upload" step: draw the picture onto a canvas, call toBlob with image/webp, send the result. It worked on my machine. Then I ran the same code in three browser engines and one of them handed back a file bigger than the one I started with. Nothing in the console. No exception, no null, no warning. That silence is what this post is about.

Quick background if you haven't used it: canvas.toBlob(callback, type, quality) takes whatever is painted on the canvas and encodes it into a file. type is a MIME string like image/webp. quality is a number between 0 and 1 that only matters for lossy formats like JPEG and WebP. My test setup: the three engine builds that ship with Playwright 1.61.1 (Chromium 149, WebKit 26.5, Firefox 151), and a synthetic 2000x1500 test image I generated with a script — gradients, noise, semi-transparent shapes — which is 6,329,764 bytes as a PNG. The comparison run I used was the format converter at ImgIng (https://imging.ai/), fed the same synthetic samples on all three engines.

Why does toBlob return a PNG when I asked for WebP?

If the engine can't encode the type you asked for, it quietly gives you a PNG instead. I tried 11 types on a small 64x48 canvas. Chromium 149 can only produce PNG, JPEG and WebP. Firefox 151 adds BMP. The WebKit 26.5 build can produce AVIF, HEIC, GIF and BMP, and yet not WebP. Every unsupported type falls back to PNG, and toDataURL and OffscreenCanvas.convertToBlob make the same call as toBlob.

Green cells are real output in the requested format, red cells are a silent PNG fallback. Look at the WebKit column on the image/webp row, and at the last row, where the typo image/jpg is red in all three engines

PNG is lossless, so a photo-like image gets big fast. In that WebKit build, asking for WebP gave me a 7,993,002-byte PNG: larger than the source, and 19.8 times the size of Chromium's default WebP. My code had named it something.webp. The name was a lie my own code told. The easiest way to hit this by accident is typing image/jpg instead of image/jpeg. All three engines treat that as unknown and fall back to PNG. Case doesn't matter though; IMAGE/JPEG still gives you a JPEG.

The comparison tool did something I ended up copying in spirit. On page load it runs toBlob once per format on a 4x4 canvas and checks what actually comes back, instead of guessing from the user agent. In the WebKit build, where WebP fails, it switches to a self-hosted libwebp compiled to WASM (a compiled program that runs inside the browser) and produced 500,178 bytes. In Chromium and Firefox it just uses the native canvas WebP, and the output was byte-for-byte identical to my own toBlob call.

What happens if quality is a string?

The second cause was mine. My quality value came from a form field, so it was the string '0.8'. The engines treat that as if you passed nothing and use their default. For a JPEG in Chromium, 0.8 should be 446,131 bytes. The string got me 957,988 bytes, byte-identical to the 0.92 output, 2.15 times what I expected. Firefox came out 3.90 times. I also tried 1.5, -1, NaN and null. None of them get clamped to 0 or 1; they all fall back to the default. WebKit's default JPEG quality is something I couldn't pin down: it isn't 0.92, and scanning 0.700 to 0.800 in 0.005 steps found no byte-identical match.

The same JPEG export at quality 0.8 on three engines. Chromium 436 KB, Firefox 462 KB, WebKit 1,100 KB: the WebKit bar is 2.53 times Chromium's

Even a correct number doesn't mean the same thing everywhere. At 0.8, the WebKit build's JPEG was 2.53 times the size of Chromium's. Quality is a dial, and each engine prints its own scale on it, so comparing the number across browsers tells you very little.

What I check after every export now

Three checks went into my upload code. Read blob.type and compare it with what you asked for; the file name proves nothing. Run the quality value through Number() and confirm it sits between 0 and 1 before you pass it. If blob.size ends up larger than the original file, upload the original. And don't bother tuning quality for PNG: all three engines ignore it, and 0.1 and 1 produced identical files.

Top comments (0)