DEV Community

AsyncMonk
AsyncMonk

Posted on

canvas.toBlob silently crops WebP past 16,383 px, and nothing tells you


My little image tool site does its resizing and format conversion in the browser. The reason is boring: files that never reach my server never show up on my bandwidth bill. What I had never tested was what happens when someone drops a really large image into it. So I generated synthetic test images with a script (gradients, shapes and noise, no photos) and ran them through three engine builds: the Chromium 149 open-source build (not Chrome itself), a WebKit 26.5 build that is not Safari, and a Firefox 151 build that is not the release. None of this is real Safari, iOS or a release Chrome.

Before touching my own code I wanted to know whether keeping big files on the client is realistic at all. I used imging (https://imging.ai/), operating the Chinese version of its interface, on large synthetic JPEGs (the biggest common case was 10000×10000) in all three engines, and counted the requests in the network log: across 12 cases there were zero non-GET requests. So a 100-megapixel file can stay on the user's machine. Then I went back to my own code.

What happens when you export a canvas wider than 16,383 px to WebP?

In Chromium 149, calling toBlob with 'image/webp' on a 20000×64 canvas hands back a perfectly valid WebP. There is no error, no console warning, and the MIME type is right. The file is simply 16383×64. To tell cropping from scaling I painted a horizontal gradient with a 64 px blue bar on the right edge. In the output the blue bar is gone, and the last column has exactly the color of column 16,383 of the source gradient. The right 3,617 px were cut off. At 65535×64 the output is again 16,383 px wide, which means 75% of the image is gone. A 64×20000 canvas loses its bottom instead.

A 65535 by 2400 canvas in Chromium 149 and the WebP it exported, which covers only the left quarter

This is the small repro page I wrote, running a 65535×2400 canvas in Chromium 149. The top strip is the canvas: green to orange, ending in the blue bar. The strip under it is the exported WebP shown at the same scale. It stops about a quarter of the way in, and the red box is the part that never made it into the file. The line at the bottom reads export.webp · 16,383 × 2,400 · 81 KB. Nothing on the page says anything went wrong.

The other two engines fail in different ways. The Firefox 151 build calls back with null for the same exports, which is at least something you can catch. The WebKit 26.5 build cannot encode WebP at all and returns a full-size PNG instead, so the type is wrong but the pixels are all there. JPEG has its own ceiling as well: Chromium 149 turns a 65535×64 canvas into a 65500×64 JPEG and drops the rightmost 35 px, again without a word. The Firefox build returns null there too.

The last 300 columns of a 65535 by 64 source next to the same columns of the JPEG Chromium 149 exported, where the blue bar stops early

Here the source and the exported file sit side by side, both zoomed 6× on the last 300 columns. The source ends in a full blue bar. The JPEG, 65500×64, keeps only part of it, and the red box is the 35 px that are simply not there.

How I check canvas exports now

The change in my uploader is not clever. After every export I read the width and height of the resulting image and compare them with the canvas I drew. If they differ, if the blob is null, or if blob.type is not what I asked for, the job stops and the user gets a message instead of a quietly truncated file. For WebP output I also scale anything with an edge above 16,383 px down before drawing, because a check that fires after encoding has already spent the user's memory on a result I will throw away.

A few things I still can't answer. I only ran these open-source engine builds, so I don't know if release Chrome, Chrome on Android or real Safari behave the same. I also didn't dig into the encoder source to see where 16,383 comes from; I only know where it bites. If your app exports WebP from a canvas, draw a 20000×64 test canvas with a colored strip on the right edge, export it, and check the width of what comes back.

Top comments (0)