← Back to all posts

getClientRects Measures Text in Subpixels, and the Subpixels Name the Renderer

getClientRects and canvas measureText return the sub-pixel width of rendered text. At a fractional font size that width comes out different on FreeType than on DirectWrite or CoreText, because the three scalers round the glyph advance on different grids. One measurement at font-size:24.5555px separates a Linux text stack from a Windows or macOS one, and a custom Chromium build on Linux carries the Linux value no matter what OS it claims.

Summarize this article with

getClientRects1 and canvas measureText2 are font-detection tools first. A fingerprinter uses them to check which fonts and emoji are installed: set a font-family, measure a string, and if the width differs from the width in a known fallback the font is present. A common anti-bot form of this measures an emoji against Arial and then the same emoji against a font that ships only on one platform, Segoe UI Emoji on Windows, Apple Color Emoji on macOS, Noto Color Emoji on Android. Which of those the browser can actually render, and the exact width it renders at, says both which emoji font is installed and which platform it belongs to. That primary use is about the machine’s font set.

The width those methods return carries a second signal at the same time: it depends on which font scaler3 drew the glyphs. A font scaler rasterizes a font at a given pixel size and produces the horizontal advances layout uses to place the glyphs. Chrome ships a different one per platform: FreeType4 on Linux and Android, DirectWrite5 on Windows, Core Text6 on macOS. Each rounds an advance on its own grid. At a fractional font size the three disagree in the low bits, and the disagreement is stable, deterministic, and one DOM read away.

Real fingerprinting scripts collect this. browserleaks.com/rects7 lays out a few styled strings at font-size: 24.5555px, reads the getClientRects box of each, and hashes the numbers, and the same rect measurements feed the width vectors that libraries like FingerprintJS mix into their overall hash. It is not read on its own as an OS oracle. It is one component among canvas, WebGL, fonts, and the rest, and it moves the aggregate hash and cross-checks the OS the other signals claim. That is enough reason not to leave the wrong value sitting in it.

The probe

No transform, no styling tricks. Measure one string.

function textWidth(px) {
  const c = document.createElement('canvas').getContext('2d');
  c.font = px + 'px "Times New Roman"';
  return c.measureText('Fingerprinting? https://browserleaks.com/rects').width;
}
textWidth(24.5555);

Genuine Chrome 150, one string, three real machines:

textWidth(24.5555)WindowsmacOSLinux
result458.16650390625458.16650390625458.108642578125

Windows and macOS agree to the bit. Linux is off by about 0.058 px. That is the whole tell: a fractional-size text measurement that lands on the Windows/macOS value or the Linux value, and nothing in between.

The same split shows up in getClientRects. Lay out that text in a <span> and read getBoundingClientRect().width: Windows and macOS return 458.171875, Linux returns 458.109375. Any element, any DirectWrite-or-CoreText versus FreeType difference, surfaces as a fractional pixel in the box.

Why the low bits differ

A glyph’s advance, the horizontal step from one glyph’s origin to the next, starts as an integer in the font’s own design units. Times New Roman stores 2048 units per em; the letter n is 1024 of them, exactly half an em. To render at a pixel size, the scaler multiplies: advance_px = design_units * size / unitsPerEm. At size = 24, n is 1024 * 24 / 2048 = 12.0 px on every OS. Integer sizes are boring. All three scalers return the same number, bit for bit.

Fractional sizes are where the design shows. No scaler uses the raw float size. Each first snaps the size to its own internal grid, and the grids differ.

FreeType snaps the pixels-per-em to a 1/64 grid, and it floors:

eff = floor(24.5555 * 64) / 64 = floor(1571.552) / 64 = 1571 / 64 = 24.546875

So Linux computes n as 1024 * 24.546875 / 2048 = 12.2734375.

DirectWrite and CoreText snap the em to a 1/100-px staircase instead, with the level boundary sitting exactly at each hundredth of a pixel. A dense sweep from 24.00 to 25.00 px shows the effective size stepping every 0.01 px, the step landing precisely at 24.01, 24.02, 24.03, ... (sharp to better than 1/4000 px). At 24.5555 the level is 24.55, realized on the 16.16 fixed-point grid as 24.549988:

n = 1024 * 24.549988 / 2048 = 12.274994

24.546875 versus 24.549988. The Linux em is smaller, so every glyph is a hair narrower, and the difference accumulates across the string into the 0.0625 px gap the probe reads.

There is a second layer under the em grid. DirectWrite and CoreText do not apply one uniform em-scale to every glyph. They hint each glyph on its own grid, so at a fractional size the letters H, i, and m come out about 1e-5 px off from what a single scale factor predicts, while n and o land on it. FreeType hints differently again. The upshot is that the advance a page reads back is a per-glyph, per-size value fixed by the scaler and its rounding, not a formula anyone can reproduce by scaling design units.

Windows and macOS are the same number

The surprising part is that DirectWrite and CoreText return byte-identical advances. Measured on a genuine Windows 11 box and a genuine Apple-Silicon Mac, both Chrome 150, the same font at the same size gives the same float, integer and fractional alike, down to n at 24.5555 reading 12.274993896484375 on each. Both implement the standard TrueType hinted-advance quantization, so this signal’s axis is Linux versus not-Linux. It does not separate Windows from macOS; the other axes, canvas rasterization and WebGL among them, do that. What the rect float contributes is one clean bit: the renderer is FreeType, or it is not.

Why the bot version fails

A scraper built on a custom Chromium always runs on Linux servers, so it always links FreeType. Change the User-Agent to Windows, spoof navigator.platform, rewrite the client hints, and the text still measures at the FreeType value. A profile that says Windows and returns 458.109375 from textWidth(24.5555) is contradicting itself, and the contradiction is a single float that no header rewrite touches.

It runs the other way too. A stack that hardcodes an integer-rounded advance, an older trick for hiding text metrics, returns 459 for that measurement. No genuine browser on any OS returns a whole number there. Real text is sub-pixel. An integer is as loud as the wrong fraction.

The leak sits in layout, under the font scaler, three layers below where a spoofing stack usually reaches. It is deterministic, it needs one measureText call, and it does not depend on the GPU, the display, or headless mode. No one blocks on this float by itself; it is one term in a fingerprint hash and a cross-check against the OS the other signals claim. Line up the claimed OS with the actual scaler and the numbers agree. Leave them crossed and a fractional pixel disagrees with everything else the profile says.

Scrapium is Scrapfly’s scraping browser, built to stay indistinguishable from real traffic. Our engineering blog goes deep on the rest of the stack.


  1. getClientRects / getBoundingClientRect: DOM methods returning the on-screen rectangles of an element as floats, including sub-pixel fractions from text layout. Reference: MDN↩︎

  2. Canvas measureText: returns a TextMetrics whose width is the advance width of the string at the current font, as a float. Reference: MDN↩︎

  3. Font scaler: the component that rasterizes a font at a given pixel size, producing glyph bitmaps and the horizontal advances layout uses to place them. Its hinting and rounding decide the sub-pixel advance a page can read back. Chrome uses a different scaler per OS, listed below. ↩︎

  4. FreeType: the open-source font engine Chrome uses on Linux and Android. It scales advances at a pixels-per-em value floored to a 1/64 grid. Reference: FreeType↩︎

  5. DirectWrite: Microsoft’s text layout and glyph-rendering API, the scaler Chrome uses on Windows. It reads glyph metrics through IDWriteFontFace and quantizes the effective em to a 1/100-px staircase. Reference: DirectWrite↩︎

  6. Core Text: Apple’s text layout and font-access framework, the scaler Chrome uses on macOS (and iOS). It returns advances byte-identical to DirectWrite for the same font and size. Reference: Core Text↩︎

  7. BrowserLeaks ClientRects: a public fingerprinting probe that lays out styled strings and hashes their getClientRects boxes. Reference: browserleaks.com/rects↩︎