DEV Community

NullPointerZen
NullPointerZen

Posted on

Why the same A4 page at 300 DPI is 2480 or 2481 pixels wide depending on the tool

Ask two PDF converters for the same A4 page at 300 DPI and one hands back a PNG 2481 pixels wide while the other says 2480. Nothing is broken, and a calculator is enough to see why. I ran one A4 page (595.276 × 841.89 pt, generated by me) through four things on a single Apple M4 machine, once each: poppler's pdftoppm 26.08.0, PyMuPDF 1.26.5, pdf.js 4.4.168 in a desktop Chromium 149, and ImgIng, a PDF to image tool in the desktop browser at https://imging.ai/ with its three presets of 144, 216 and 300 DPI. In the runs I watched, ImgIng sent no request that carried the file.

Four tools, three answers

A4 at 300 DPI Width × height
pdftoppm 2481 × 3508
PyMuPDF 2481 × 3508
pdf.js, canvas sized with Math.floor 2480 × 3507
ImgIng, 300 DPI preset 2480 × 3508

The pdf.js row is partly my doing. pdf.js hands you a viewport with fractional sizes, and the floor comes from my own line that creates the canvas. I did not try round or ceil there.

The fraction decides

Pixels are points × DPI ÷ 72, and an A4 page is not a whole number of points. At 300 DPI the width is 595.276 × 300 ÷ 72 = 2480.317 and the height is 3507.875. Every tool has to turn those into integers, and there are three common ways: drop the fraction, round to the nearest, or go up.

The width fraction is .317, so dropping it and rounding both give 2480 and only going up gives 2481. The height fraction is .875, so rounding and going up both give 3508 and only dropping it gives 3507. That is the whole table above. pdftoppm and PyMuPDF behave like ceil on every size I ran, the browser tool behaves like round to nearest at all three presets (1191 × 1684, 1786 × 2526, 2480 × 3508), and my pdf.js canvas is the floor. That last part is read off its outputs, I cannot see how it is built.

When the gap shows up

A4 Exact Round Ceil
72 DPI 595.276 × 841.890 595 × 842 596 × 842
144 DPI 1190.552 × 1683.780 1191 × 1684 1191 × 1684
300 DPI 2480.317 × 3507.875 2480 × 3508 2481 × 3508

The two camps disagree only when a fraction falls below .5, so the one-pixel gap appears at 72 and 300 DPI but not at 144. In my run pdftoppm and the browser tool both gave 1191 × 1684 there. At 216 DPI the fractions are .83 and .67, so by arithmetic they should agree again, though I only ran the browser tool at that setting (1786 × 2526). Pages with whole-point sizes never split: Letter at 300 DPI was 2550 × 3300 in every tool I ran it through (pdftoppm, PyMuPDF, pdf.js and ImgIng), and a 504 × 720 pt brochure page, from a 4-page Wikimedia Commons sample (Wiki Education Foundation, CC BY-SA 3.0), came out as 2100 × 3000 in Smallpdf, PDF24 (after I set 300) and the browser tool alike.

Asking for a width instead

If a layout needs one exact width, skip DPI and ask for the width. This is the full output of two runs with pdftoppm:

$ pdftoppm -r 300 -png -singlefile A4.pdf A4-300
$ file A4-300.png
A4-300.png: PNG image data, 2481 x 3508, 8-bit/color RGB, non-interlaced
$ pdftoppm -scale-to-x 1000 -scale-to-y -1 -png -singlefile A4.pdf A4-w1000
$ file A4-w1000.png
A4-w1000.png: PNG image data, 1000 x 1415, 8-bit/color RGB, non-interlaced
Enter fullscreen mode Exit fullscreen mode

The width is exactly what I asked for. The height took the rounding instead: 1000 × 841.89 ÷ 595.276 is about 1414.3, and the file is 1415 tall.

What I check before blaming a tool

Read the page size first, then multiply and look at the fraction. If a fraction is under .5, two tools that round differently will differ by one pixel there, and that is the convention, not a bug. If the page is a whole number of points, you will not see it at all.

Two limits on all this. I only looked at A4 in depth (A3 followed the same pattern in PyMuPDF), and Smallpdf, PDF24 and iLovePDF were only fed the whole-point brochure page, so I cannot say which way they round on A4. Whether any of this matters depends on what comes after the converter, and I did not test that.

Top comments (0)