We have a request at work to export reports as one long image, and before writing anything I wanted to see how existing tools handle very tall output. So I made an 8-page A4 handout and stitched it into a single long image. The tool I used was ImgIng (https://imging.ai/), a browser-based image and PDF toolkit, running in Chromium 149 on an Apple M4 Mac. I watched the network panel the whole time and the PDF never left the machine, which matters for anything from work. I exported at 216 DPI, then again at 300 DPI.
The two WebP files were byte-for-byte identical. Both were 1446×16376.
At 216 DPI an 8-page A4 sheet should be 1785×20208. Divide the actual width by the page width (8.264 in) and you get about 175 DPI. Picking 300 changed nothing.
The height was capped just under 16383
More pages made it obvious. 20 pages came out at 578×16380, about 70 DPI. 60 pages came out at 192×16380, about 23 DPI. The tool scaled the whole image down proportionally to keep the height under the cap, and the UI only showed the final dimensions. No "downscaled" warning.
16383 is the WebP limit. The WebP header stores width and height in 14 bits each. Pillow 11.3 writes an 8×16383 WebP fine and refuses 8×16384 with "Image size exceeds WebP limit of 16383 pixels". What surprised me is that JPG and PNG exports in the same long-image mode also came out at 1446×16376. My guess is the tool sizes long images against the WebP limit for every format. I haven't read its code, so that's inferred from the output alone.
Your own canvas export can fail silently
If you build the long image yourself on a canvas and call toBlob, it gets worse, because each engine fails differently. I asked for oversized exports at quality 0.8:
-
1786×20208 as WebP: Chromium 149 returned a 1786×16383 WebP without complaint. Firefox 151 returned
null. WebKit 26.5 handed back a PNG instead. -
1786×65535 as JPEG: Chromium returned 1786×65500. Firefox returned
null. WebKit returned the full 65535. - PNG was fine everywhere at 1786×20208.
The Chromium results are crops, not rescales (an earlier test with gradient bars confirmed it, and the sizes match). A note on JPEG: the spec allows 65535 per side, but Pillow and Chromium both stop at 65500 (WebKit went all the way). Pillow errors at 65501 with "Maximum supported image dimension is 65500 pixels". So "JPEG maxes out at 65500" is wrong as a statement about the format. It's an implementation limit.
The canvas itself has limits too. Chromium and Firefox allow 65535 on one side. Chromium and WebKit also cap total area at roughly 2^28 pixels (WebKit topped out at 150299 tall when 1786 wide). In practice the WebP 16383 limit bites first.
What I'm planning to do instead
Splitting beats lowering DPI. Here's the trade I measured on a 20-page handout:
- Four sheets of 5 pages each: 1785×12630 apiece, full 216 DPI, about 4.35 MB total. A 7pt footer line is 840×39 pixels.
- One sheet: 578×16380, about 70 DPI, 986 KB. The same footer line is 272×13 pixels.
A4 at 216 DPI is 2526 px per page, so a WebP sheet holds 6 pages at most. After export I'm adding a check for all three silent behaviors. This isn't in our codebase yet:
async function exportSheet(cv, type) {
const out = await new Promise(done => cv.toBlob(done, type, 0.8));
if (out === null) throw new Error('canvas too large for this format'); // Firefox
if (out.type !== type) throw new Error(`got ${out.type} instead`); // WebKit
const img = await createImageBitmap(out);
if (img.height !== cv.height) throw new Error(`cropped to ${img.height}px`); // Chromium
return out;
}
All of this was measured on one M4 machine with those three engine builds. I haven't tested real Chrome, Safari or phones. Before this ships I'll rerun it against reports with our real page counts and check the split threshold holds.
Top comments (0)