ink extent and advance width grow by the same step
Check id text-ink-extent-vs-advance
What an ordinary browser yields
Every step agrees: adding one copy of a glyph moves the total width and the right ink edge by the same amount, to within 1e-4 px.
What a detector infers
TextMetrics reports two numbers produced by different parts of the engine. `width` is the sum of the glyph advances the shaper accumulated. `actualBoundingBoxRight` is the rightmost inked pixel, derived from the glyph outlines. For a run of one repeated glyph the two are locked together: appending one more copy adds exactly one advance to the total, and it also shifts the final glyph — the one whose outline sets the right ink edge — right by that same advance. So both must take the SAME step, and subtracting consecutive measurements leaves a difference that has to be zero. No reference value appears anywhere in that. It is two fields of one object, made by two different subsystems, held against each other, which is why it needs no corpus and no knowledge of what any correct width would be. It exists alongside the four other text rows because a farble that perturbs each glyph's advance by a small fixed step is invisible to every one of them, and that was verified rather than assumed: the dyadic-grid row stays quiet because a 1/512 step lands back ON the grid, additivity stays quiet because a per-glyph step is consistent so N copies still measure N times one, the uniform-scale row stays quiet because no single factor explains a per-glyph offset, and the cross-realm worker row stays quiet as soon as the farble is applied inside workers too, as any real implementation does. The tolerance was measured rather than chosen, because on this check the tolerance IS the check. Over 1200 samples across three page loads each, Chrome 150 and WebKit returned a residue of exactly zero on every sample; Firefox 151 held only to float precision, ceiling 5.59e-6 and nonzero median 1.4e-6, because Gecko computes these in lower precision. An exact comparison would therefore fail every genuine Firefox — the same calibration trap that forces the dyadic-grid row to be Blink-only. The smallest farble worth catching is one 1/512 step, 1.95e-3. The threshold sits at 1e-4, the geometric midpoint of that gap: 17.9 times above the worst residue any unmodified browser produced, and 19.5 times below the smallest meaningful perturbation. That is also why the probe caps its font sizes at 20px — the residue is float error proportional to the advance, so an absolute tolerance is only defensible while the probe controls the magnitudes it measures. Holding within tolerance on Gecko and WebKit as well as Blink is what lets this run with no engine gate, which matters because the product it catches is a Gecko build.
How to resolve it
Nothing to fix if this passes, and it passes on every unmodified browser measured here. If it fails, one of the two numbers was adjusted on the way out while the other still describes what the engine actually laid out. The boundary is the useful part, so it is worth stating plainly what this row does NOT claim. A uniform multiplicative scale of the whole TextMetrics object passes it, because both quantities scale by the same factor and the shared step survives — that shape belongs to the uniform-scale row next door. A perturbation applied genuinely inside the shaper, where the ink extent is then laid out from the perturbed advances, also passes, because the two numbers stay honestly consistent with each other. That second case is the instructive one for anyone building text-metric protection: it is the correct way to do this, and the product measured below did not do it, despite its own documentation describing the modification as running through the shaper. Perturbing a metric is easy; keeping every other subsystem's view of that metric consistent with it is the hard part, and a page can test that consistency without knowing what any correct value would be.
Read in the wild by
Camoufox (measured)
Camoufox's font protection perturbs the advance and the ink extent by DIFFERENT amounts, so its own two metrics stop agreeing with each other. Its documentation describes that layer as a per-userContext seed applying deterministic spacing modifications through the whole measure path, naming gfxFont and gfxHarfBuzzShaper — the shaper. Had the change been applied there and only there, the ink extent would have been laid out from the perturbed advances and stayed coherent, and this row would see nothing. Three confounds were eliminated before recording it. The seed was driven directly rather than left at its default, and the residue tracks the config key — which no browser-version difference can do. At seed 0 the residue collapses to the stock-Gecko float-noise ceiling, matching Playwright's Firefox 151 to every measured digit, so the effect is the farble rather than a property of the Firefox 152 base. And the tempting stronger reading — that only the advance is touched, leaving the ink extent to leak the true value — was tested with the persona held fixed and is FALSE: both quantities move with the seed. The perturbation is a constant absolute pixel offset rather than a fraction of the em, the same residue appearing at 12px, 16px and 20px across Arial, monospace and serif.
Camoufox 0.5.4 (Firefox 152 base) on Windows, canvas measureText over 3 sizes x 3 families x 4 glyphs x 6 run lengths. Default launches: 270 of 270 glyph-run steps disagreed at a residue of exactly 0.1 px, reproduced across three launches. Through the audit page itself: Implausible, 180 of 180 steps, worst 8.33e-2 px — and Coherent at worst 5.59e-6 with `fonts:spacing_seed` 0, so the row tracks the farble rather than the product. Driving MaskConfig `fonts:spacing_seed` directly — seed 0: ceiling 5.594889e-06 (stock Firefox 151 over 1200 samples: 5.5949e-06); seed 1: 0.0500; seed 12345: 0.0667; seed 4000000000: 0.0167. Holding the persona at os=windows and varying only the seed, both the advance step and the ink step move (12px Arial "m": ink step 10 / 10.0999 / 10.1333 at seeds 0 / 1 / 12345). Stock Chrome 150 and Playwright WebKit: residue exactly 0 on 1200 of 1200 samples each; Brave 150, Chromium headless, CloakBrowser and Clearcote all read Coherent through the page.
camoufox.comEvery attribution traces to a published artifact. See the sources and their limits.
Nearby checks in Render & GPU
- WebGL and WebGPU agree on whether this device clears uniformly
webgl-webgpu-readback-agreeOne clear, two GPU APIs, one adapter. WebGL paints a flat colour and reads it back with readPixels; WebGPU renders the same clear into a… - canvas measureText widths on the 1/512 sub-pixel grid
measuretext-gridChrome's 2D text metrics are quantised: advance widths are computed in fixed-point and land on a dyadic 1/512-pixel grid. - text metrics are not a uniform multiple of the engine's own grid
text-metrics-uniform-scaleThis is the sub-pixel grid check asked one question deeper. That check reports THAT widths have left Blink's 1/512-pixel grid; this one asks… - N copies of a glyph measure exactly N times one copy
text-advance-additivityThe advance width of N copies of a single glyph is exactly N times the advance of one copy. - main-thread metrics === Worker metrics
worker-vs-mainThe same five strings are measured at 12px Arial twice: once on a main-thread 2D canvas, and once inside a dedicated Worker on an… - getBoundingClientRect === Range client rects
bcr-vs-rangeTwo independent APIs are pointed at the same laid-out text: an absolutely-positioned div containing "WWWWWiiiii" is measured with… - clientWidth and offsetWidth equal the box the element itself reports
layout-box-arithmeticclientWidth and offsetWidth are not opinions the browser offers — CSSOM defines them as arithmetic over values the same element hands you… - a box placed at the viewport origin measures its origin as zero
client-rect-origin-vs-positionZero is scale-invariant, and that single property is the whole check.
Clearcote is a browser built for fingerprint coherence
It is a Chromium fork, maintained by the same people who wrote this reference. It ships as a compiled browser rather than as a stealth script injected into someone else's — which is a description of how it is built, and is not an argument about how it behaves on this check.
This audit takes no position on how Clearcote scores on Render & GPU checks, on this one, or anywhere else. It has no baseline corpus of other people's fingerprints to rank you against and no vendor scoreboard — nearly every check is self-referential, asking one browser the same question through two independent APIs and reporting whether both answers can be true at once. It runs identically on any browser, including ours. Run it on yours and read the result yourself.
See the other checks in Render & GPU — the family text-ink-extent-vs-advance belongs to.