Questions about rendering a PDF page to a bitmap are usually about which DPI to pick. This post is about a different one: at which step of the pipeline the size gets checked, because a check placed after the render call cannot catch this failure. The numbers below come from one machine (Apple M4, desktop Chromium 149, pdf.js 4.4.168, poppler 26.08.0, PyMuPDF 1.26.5), run once each on pages I generated myself. Browser limits differ, so none of this transfers to other browsers or devices without testing.
The page size is only known after the document is open
A request says "300 DPI", and that is all the pipeline knows at the door. The pixel size depends on the page's own size in points, which you only get after the file is parsed and the page is fetched. A multi-page file can mix sizes, and a rotated page swaps width and height. So a check at the request boundary can only cap the DPI value, which says nothing about the bitmap you will actually allocate.
The only place where the real size and the allocation sit next to each other is the gap between getting the viewport and creating the canvas. That is where I put the check. The formula is pixels = points × DPI ÷ 72, and pdf.js expresses it as scale = DPI ÷ 72, so 144 DPI is scale 2 and 300 DPI is scale 4.1666667.
The same page gives different pixel counts
A budget is written in integers, and the tools I tried do not agree on the integers. I used an A4 page of 595.276 × 841.89 pt. The PyMuPDF line comes from its own output; the excerpt below is the full A4 section of that run.
--- A4: page.rect = 595.2760009765625 x 841.8900146484375 pt
dpi=72 pixmap=596x842 exact=595.276x841.890 round=595x842 ceil=596x842 floor=595x841
dpi=144 pixmap=1191x1684 exact=1190.552x1683.780 round=1191x1684 ceil=1191x1684 floor=1190x1683
dpi=300 pixmap=2481x3508 exact=2480.317x3507.875 round=2480x3508 ceil=2481x3508 floor=2480x3507
| A4 at 300 DPI | Width × height |
|---|---|
| poppler pdftoppm | 2481 × 3508 |
| PyMuPDF pixmap | 2481 × 3508 |
| pdf.js, canvas sized with floor | 2480 × 3507 |
| ImgIng, 300 DPI preset | 2480 × 3508 |
The pdf.js row depends on my own Math.floor when creating the canvas. pdf.js does not force it, and I did not try round or ceil there. A page that is an integer number of points, such as Letter at 612 × 792, gives 2550 × 3300 in every tool. The one-pixel gaps show up only on fractional page sizes like A4 and A3.
For a budget this means one thing. If the check uses the exact or floored size and the allocation uses the ceiled one, a page near the limit passes the check and fails at allocation. I compute the budget on the same rounded integers that the canvas will get, and I round up, which is the conservative side. I did not build a page that straddles the limit to prove this, so treat it as reasoning from the sizes above.
Why the check cannot wait for the render call
You might expect the render call to complain when the canvas is too big. It does not. On this Chromium, a 16384 × 16384 canvas (area 268,435,456) filled and read back as red. A 16385 × 16384 canvas read back transparent, and no error was thrown. A 20000 × 20000 canvas did the same.
Then I rendered a 14400 × 14400 pt page with pdf.js. At scale 1.1 the canvas was 15840 × 15840 and rendering worked. At scale 1.3333 the canvas would be 19199 × 19199, area 368,601,601. The render().promise resolved successfully in about 1 ms and the canvas read back fully transparent. Nothing was drawn and nothing failed.
So a catch block or a status check after rendering sees a success. That leaves only two honest places: before the canvas exists, using the integer size and the area limit, or after rendering, by reading a pixel back. The first works as the gate; the readback is the fallback for pages that got through. In the 20000 × 13421 case (area 268,420,000) the canvas worked, so the limit I hit is on area, and checking width and height separately against one fixed edge length would have missed it. A per-side limit exists as well, somewhere between 32768 and 65536 on a 1-pixel-high canvas, and I did not narrow it down.
What I could not settle
I do not know where the limit sits in other browsers, and I did not test mobile ones. I also have not decided what to do when a page fails the budget: scaling down until it fits keeps the aspect ratio, but then the DPI the caller asked for is no longer the DPI delivered, and the pipeline has to say so in its response.
The PDF engine itself is not the part I work on, so I cross-checked sizes from outside. For the desktop-browser side I used the PDF to image page of ImgIng https://imging.ai/ , which offers 144, 216 and 300 DPI presets. Across 18 runs I captured, its network traffic showed only GET requests, and its A4 sizes at the three presets were 1191 × 1684, 1786 × 2526 and 2480 × 3508. The 216 DPI row is the one I could not compare, because I did not run it through poppler or PyMuPDF.
Top comments (0)