DEV Community

Lank_M
Lank_M

Posted on

216 DPI and 300 DPI gave me byte-identical output

Someone sent me a manual-style PDF last week: one page, 9000pt tall, and the small text came out mushy after converting it to an image. Their first move was to bump the setting from 216 DPI to 300. Same result — and I mean the same file, not "close enough". 1456×16380 pixels, 1.10 MB, 4.05 seconds, both times. I reproduced it with a page I generated myself and the reason turns out to sit at the very last step of the pipeline, which is also the part I'm actually qualified to talk about.

Quick disclosure on that. I do the client-side encode/decode work on this thing; the PDF engine isn't mine. So when I say something about how pages get parsed I'm guessing, and when I talk about canvases and encoders I can give you real numbers. Both test files I generated myself rather than quote somebody else's benchmark: a four-page A4 document with mixed text, a table and an image at 490 KB, and a single page at 800×9000pt for the tall case.

The normal file: the cost isn't storage

The four-page file first, WebP, all pages. 144 DPI gives 1192×1686 per page and 317 KB total in 2.0 seconds; 216 gives 1788×2529 and 494 KB in 3.0 seconds; 300 gives 2483×3512 and 647 KB in 6.1 seconds. Going 216 → 300 adds 93% more pixels, 31% more bytes, and doubles the time. I went in assuming storage would be the thing worth arguing about and it isn't — a document page is mostly flat colour and the encoder eats the extra pixels. What scales honestly is wall clock. The format run said the same thing from a different angle: at a fixed 216 DPI, WebP is 494 KB in 3.0s, AVIF is 552 KB in 10.3s, PNG is 1.28 MB in 1.0s, JPG is 1.36 MB in 1.0s. AVIF came out 12% larger than WebP and 3.4× slower, which is the opposite of what I expected — its intra prediction doesn't buy much on flat pages with crisp glyph edges, but you pay the encode time anyway. Lossless PNG beating JPEG by 6% is the same story from the other side: JPEG spends bitrate on ringing around text. Both of those hold for this sample only. A PDF full of photographs would almost certainly flip them and I didn't test it, so I'm not claiming it.

Two settings, one identical file

The tall page is where it gets interesting.

setting output size time
216 DPI, all pages 1456×16380 1.10 MB 4.06 s
300 DPI, all pages 1456×16380 1.10 MB 4.05 s
300 DPI, one long image 1456×16380 1.40 MB 4.05 s

At 216 DPI this page should be 2400×27000. It isn't. Work backwards from the width — 1456 / (800 / 72) — and you get 131 DPI, lower than the lowest preset in the dropdown. The tell is 16380: WebP's maximum side length is 16383, and the page got scaled down until it just fit, with three pixels to spare. This isn't a memory heuristic, the encoder simply refuses a longer edge. Aspect ratio came out 16380/1456 = 11.25, exactly 9000/800, so both axes shrank together — nothing cropped, nothing stretched. Row three I can't explain. Identical dimensions, identical content, but the long-image path produces 1.40 MB where all-pages produces 1.10 MB, 27% apart. I started to write "the long-image path probably runs a more conservative quality setting" and then deleted it, because that's just restating the observation with more words. Filing it as unexplained.

Why tiling isn't optional

Which brings up why tiling isn't optional. 2400×27000 is 64.8 megapixels; as RGBA that's a 259 MB canvas resident in memory, and mobile Safari's canvas area ceiling is well below that. So you render in horizontal slices, encode each, composite by original coordinates. Two things I got wrong the first time round. One, our early version rendered every tile and then composited, and peak memory came out worse than not tiling at all, because at composite time all N tiles and the destination canvas are alive simultaneously — tiling only helps if the tile dies as soon as it's drawn. Two, if each tile rounds its own height to device pixels the error accumulates and you get a one-pixel seam, a visible line or a doubled row of text; boundaries have to be computed in source coordinates so each slice's start is the previous slice's end, rounded once. Both of those were found by staring at seams, not by any clever instrumentation.

Practically: regular page sizes are fine at 216, go to 300 when you're printing or feeding OCR and budget the time rather than the storage. On tall pages don't raise the DPI, it won't do anything — measure the output width, compute width / (page_width_pt / 72), and that number is your real resolution. If you want more, the only lever is splitting the page. One thing I still haven't settled: once you hit the ceiling, emitting the tiles as separate images and letting the caller stitch them would preserve full resolution, but then "one page" stops meaning "one file" and the output shape changes. I keep going back and forth on which is less bad. If you've shipped something like this I'd genuinely like to know which way you went.

Numbers above were measured on https://imging.ai/pdf-to-image/ .

Top comments (0)