The goal: a scanned document that measures 10 × 10 cm on paper should measure 10 × 10 cm on the reader’s screen, laid against a ruler. The browser cannot do this by itself, because it does not know how big the screen is. This note describes how to get there: measure the screen once, prepare the image at high resolution, and size it with a small calculation.
Why the browser cannot do it alone
CSS has physical units (cm, mm, in, pt), but they are not physical. The CSS
specification fixes 1in = 96px, so width: 10cm always means 378 CSS pixels, whatever the
screen. On a screen whose real density happens to be close to 96 CSS pixels per inch it comes out
roughly right; on a 14-inch 1920 × 1080 laptop at 100% system scaling (about 157 CSS pixels per
inch) the same 10cm measures about 6.1 cm. No browser API reports the physical size of the
screen, so the density has to be measured.
Two densities, and which one is for what
Two numbers describe the screen, and they answer different questions:
- CSS ppi: how many CSS pixels fall on one real inch of glass. Everything written in CSS
(
width,height,font-size,style.width) is in CSS pixels, so this number decides the size. - Effective DPI: how many device pixels the browser draws per real inch. It is always
CSS ppi × devicePixelRatio. This number decides the sharpness: how many pixels the image file needs so that nothing is stretched.
The native resolution of the panel is not needed for either. If the system renders at a higher resolution and then downscales (fractional or “render big” scaling), the compositor takes care of it; the page only has to fill the surface the browser draws.
Step 1: measure the screen
Use Monitor DPI: drag the on-screen ruler until it matches a real ruler, or lay a bank card on the orange marks (ISO/IEC 7810 ID-1, 85.60 × 53.98 mm, the same size everywhere, and nearly everyone has one). The tool reports both the CSS ppi and the effective DPI.
Pixel grid is a cross-check: it draws squares whose side is N device pixels.
The square that measures exactly one inch (2.54 cm) names the effective DPI. One inch is short,
though: 5% of it is 1.3 mm, which a ruler held against the glass easily misreads. A square as long
as the card’s long side (85.60 mm = 3.370 in) is a better check: if N = 3.370 × effective DPI,
the card fits it exactly, and a 5% error shows as 4 mm.
Example, a 23.8-inch 1920 × 1080 monitor rendered at devicePixelRatio 2:
| Value | |
|---|---|
| Square that matched the card’s long side in Pixel grid | 628 device px |
| Effective DPI | 628 / 3.370 ≈ 186 |
devicePixelRatio | 2 |
| CSS ppi | 186 / 2 = 93 |
Alternative: ask for the screen diagonal
Most people know the size of their screen (“a 24-inch monitor”, “a 15.6-inch laptop”), and that one number is enough, with no ruler and no card. The surface the browser draws covers the whole glass, whatever the panel’s native resolution, so its diagonal in device pixels divided by the diagonal in inches is the effective DPI:
rw, rh = screen.width × devicePixelRatio, screen.height × devicePixelRatio
effective DPI = √(rw² + rh²) / diagonal in inches
For the example screen, its 23.8-inch diagonal gives √(3840² + 2160²) / 23.8 ≈ 4406 / 23.8 ≈ 185,
less than 1% from the measurement.
The error in the result is the error in the diagonal, one for one, and it comes from three places:
- The advertised size is rounded. Monitors are sold by size class: a “24-inch” panel is often 23.8 inches and a “22-inch” one 21.5. Typing the class instead of the real size makes the DPI 0.8% and 2.3% too low respectively, so a 10 cm document shows as 9.92 cm and 9.77 cm. Laptop sizes (14, 15.6) are usually stated exactly. The real diagonal is in the monitor’s data sheet.
- A wrong answer is not caught. With a card the reader sees that the frame matches; a misremembered diagonal (24 for a 27-inch screen) gives a document 12.5% too large, 11.25 cm for 10, with nothing on the page to show it.
- The image must fill the glass. If the system resolution has a different aspect ratio from the panel and the monitor shows black bars, or the picture is centred instead of stretched, the drawn surface is smaller than the glass, the formula assumes the larger one, and the document comes out too small.
So the diagonal is a good default, comparable to the card when the real size is known, and the card or ruler remains the check. Offer both, and state which one the current size came from.
Step 2: prepare the image at high resolution
The image file needs at least size in inches × effective DPI pixels along each side. The
effective DPI differs from screen to screen (DPR 1, 1.25, 2, 3…), while the file is one, so scan
generously: at 300 dpi a 10 cm side becomes 10 / 2.54 × 300 ≈ 1181 pixels, which covers
nearly every screen. The browser downscales to whatever each screen needs; a larger file only
costs bandwidth.
Step 3: compute the size
The rule in one line:
CSS pixels = centimetres / 2.54 × CSS ppi
= centimetres / 2.54 × effective DPI / devicePixelRatio
For the example screen, 10 cm → 10 / 2.54 × 93 ≈ 366 CSS pixels, which the browser draws
as 366 × 2 = 732 device pixels: 10 cm on the glass.
Store the effective DPI, not the CSS ppi. Browser zoom (Ctrl +) changes devicePixelRatio
and with it the CSS ppi, but the device pixels per inch stay the same: they belong to the screen,
not to the page. So the effective DPI is the number that survives a zoom, and the CSS size is
derived from it every time:
// once, at calibration time
const devPxPerCm = cssPxPerCm * devicePixelRatio; // fixed for this screen
localStorage.setItem("devPxPerCm", devPxPerCm);
// on every render, and again on every zoom change
function fit(img, widthCm, heightCm) {
const k = Number(localStorage.getItem("devPxPerCm")) / devicePixelRatio; // CSS px per cm now
img.style.width = widthCm * k + "px";
img.style.height = heightCm * k + "px";
}
Re-run fit() on the resize event, or on a matchMedia("(resolution: …dppx)") listener that
re-arms itself for the new value, since a zoom fires one of the two.
A page on the same origin as maistrelis.com can reuse the calibration Monitor DPI has already
stored, so the reader does not measure twice: the localStorage key monitorDpiCal holds
pxPerCmH / pxPerCmV (CSS pixels per centimetre, horizontal and vertical) together with the
dpr they were measured at, so pxPerCmH × dpr is the device pixels per centimetre.
The trap: multiplying by the DPR twice
The effective DPI is for the file, the CSS ppi is for the CSS. Mixing them doubles the
size. With the example screen, width: 732px (the device-pixel count) is drawn as 1464 device
pixels: 20 cm.
The same happens silently with an image that gets no width at all: the browser shows one file pixel as one CSS pixel, so a 732-pixel file comes out at 20 cm. Either give the width explicitly, or tell the browser the file’s density:
<img src="doc-732.png" style="width: 366px"> <!-- explicit CSS size -->
<img srcset="doc-732.png 2x"> <!-- shown at 732 / 2 = 366 CSS px -->
For a <canvas> (e.g. a PDF page rendered with pdf.js) the two numbers sit side by side:
canvas.style.width = cssW + "px"; // size: CSS ppi
canvas.width = Math.round(cssW * devicePixelRatio); // sharpness: effective DPI
PDF viewers
A PDF measures in points, 1pt = 1/72 in. At “100%” a PDF viewer assumes 96 CSS ppi, so
1pt = 96/72 CSS pixels. True size needs CSS ppi / 72 CSS pixels per point instead, i.e. a zoom
of CSS ppi / 96. On the example screen that is 93 / 96 ≈ 0.97: the “100%” is about 3% too
large, 7 mm on the width of an A4 page and 10 mm on its height. On the 14-inch laptop above it is 157 / 96 ≈ 1.64.
Limits
- The measurement belongs to one screen. A second monitor, or a change of the system’s display scaling, needs a new measurement.
- The accuracy is that of the measurement, in practice ±1–2%.
- Without a measurement the best available guess is the CSS
cmunit (96 ppi). Say so on the page (“approximate size”) rather than present a guess as a true size.
Why this is not built in
The calibration step looks like a workaround for something missing, but it is the result of two deliberate decisions, and the only way the platform leaves open.
CSS gave up physical units. In CSS 1 and 2 the absolute units meant real lengths. Browsers had
no reliable way to know the size of the screen, so the results differed from one browser to the
next; Internet Explorer and WebKit had already fixed 1in = 96px, and around 2010 the revision
that became CSS 2.1 made that the rule. Separate “true” units (truemm, truept) were proposed
on the CSS mailing list at the time. The objection that prevailed was that such units help no one
while the ratio of device pixels to real lengths is not reliably known, and that mixing logical
and true units would break pages. Firefox shipped an experimental true millimetre, mozmm, in
2010, mainly to size touch targets; it was removed in Firefox 59 (2018) because no other browser
showed interest in implementing it.
Privacy closed the door for good. The operating system usually knows the physical size of the
screen, because the monitor reports it in its EDID data (on Linux, xrandr prints it, e.g.
520mm x 290mm). Browsers do not pass it on to pages: screen properties are a well-known
fingerprinting vector, and the trend is towards exposing less, not more. CSSOM now allows a
browser to report the viewport as the screen, and Tor Browser hides the physical screen values
altogether. Requests to expose the EDID size to web developers have been made, without result.
Even when present, the EDID values are not always right: televisions, projectors and some adapters
report wrong or zero sizes.
What remains is the one input no API can fake or leak: the reader holding a real object against the screen. That is why sites that need true size (online eye tests, ring sizers, measuring glasses) ask for a bank card, as this note does.
Sources
- Robert O’Callahan, CSS absolute length units (January 2010) and CSS units changes landed (August 2010): the
1in = 96pxchange andmozmm. - www-style mailing list: Jonathan Kew proposing
truemm/truept, and Peter Linss objecting (January 2010). - WICG Discourse: Physical unit redux.
- Mozilla bug 1416564: removal of
mozmm(Firefox 59), and Emilio Cobos Álvarez on why (2019). - public-css-archive: a request to expose EDID data (2019).
- Screen information and privacy: WebKit bug 161227, Mozilla bug 1298116, Tor Browser issue 13636.

Αφήστε ένα σχόλιο
Τα σχόλια ελέγχονται πριν δημοσιευθούν και εμφανίζονται με την επόμενη ενημέρωση του ιστότοπου.