This repository has no description
bez docs text-metrics.md
16 kB

Do the three engines agree on text geometry? #

Partly, and the part they disagree on is sharply localized. Every other corpus this project checks the engine against is text-free by construction (crates/harness/src/cases.rs's module doc) and the held-out WPT filter (oracle/suites/normal-flow-pages.mjs) rejects any page with a text node, so until now nothing had measured whether Chromium, Firefox and WebKit place glyphs closely enough for the geometry vote (docs/geometry-verification.md) to run at all. This is that measurement. It builds no layout code.

The short answer. Line breaking and element geometry are votable; the text fragment rect is not. All three engines broke every line at the same place in both fonts, and the element boxes of a text-bearing page agree at 1/60 px 99–100% of the time. But the rect a text node's own line fragment occupies disagrees systematically: Firefox makes it 1 px taller than the other two almost always, and in the proportional font the three engines never once agreed on a line's width. Loosening the tolerance does not rescue it — the differences are whole-pixel-scale, not sub-pixel.

flowchart TD
  probe["textProbe:<br/>pinned font, 3 engines"] --> lines{"same number<br/>of line fragments?"}
  lines -- "yes, 100% of the time<br/>in both fonts" --> boxes{"element boxes<br/>within 1/60 px?"}
  boxes -- "yes, 99-100%" --> ok["element geometry of a<br/>text-bearing page: votable"]
  boxes --> frag{"text fragment rect<br/>within 1/60 px?"}
  frag -- "x: yes in Ahem, 81.7% in Lato<br/>width: 96.6% in Ahem, 0% in Lato" --> horiz["horizontal: font-dependent"]
  frag -- "y and height: 4-9%" --> vert["vertical: no, in either font"]

What was measured #

  • Probe: docs/dump-format.md → "Text probe". One request renders a document with a pinned font embedded as a data: URL and returns, per engine, the ordinary element dump plus every text node's Range.getClientRects() — one rect per line fragment. An engine whose page fell back to a system font fails the request rather than being measured, so no figure here is contaminated by font substitution.
  • Cases: crates/harness/src/text_cases.rs, ten categories (TextCategory::ALL), each run in both fonts with byte-identical markup. The run below is enumerated() plus 60 seeded-random cases at seed 0x7e47, so 70 cases per font.
  • Quantities: each element's bounding box and per-fragment rects, voted by crates/harness/src/compare.rs vote(); each text node's fragment count and per-fragment x, y, width and height, voted by crates/harness/src/trace.rs vote_text(). Both use Consensus at dump::TOLERANCE_PX = 1/60 px.
  • Binary: crates/harness/src/bin/text_metrics.rs. Raw responses are kept under corpus/text-metrics/<font>/<case>.json (gitignored) so a rerun on other engine versions can be diffed against this one.

A text node that produced no line box in any engine — the contents of every <style>, including the injected @font-face rule — is reported by the probe but dropped from these tallies rather than counted as agreement about geometry that does not exist.

Engines #

From Playwright, launched with the oracle's default profile on Linux:

Engine Version
Chromium 153.0.8010.12
Firefox 155.0
WebKit 26.6

Every number below is valid only for these builds. The vote's tolerance, 1/60 px, is the coarsest grid any of them rounds to (Firefox's app units); Chromium and WebKit use 1/64.

Fonts #

Both are files in the WPT checkout oracle/suites/suites.json pins (commit 26f417a8c0), whose sparse list already fetches /fonts/.

Key File Version License sha256
ahem fonts/Ahem.ttf 1.50 Public domain, with a Creative Commons fallback for jurisdictions that do not recognize it (stated in the font's own name table; http://dev.w3.org/CSS/fonts/ahem/COPYING) b719ecb31c5b21fc573c03f6421c74ac63c271a5a3ff841e34f9705fb94b8448
lato fonts/Lato-Medium.ttf 2.015 SIL Open Font License 1.1, © 2011–2015 tyPoland Lukasz Dziedzic, reserved font name "Lato" (stated in the font's own name table) be8bbf7105500e8fc1f9429307fb396905a0e6cff63fe1eb751bb319d0f1b0db

Ahem's every glyph is a 1 em square and it is the font WPT's own text-bearing layout tests use, so it is the case most likely to agree. Lato is proportional, with real advances, kerning pairs and hinting; it was chosen over the other proportional faces in that directory because it is a TrueType file rather than a WOFF, so the ordinary hinting path is exercised, and because it states its own license, which is where the license above was read from rather than assumed.

Ahem #

3581 measurements over 70 cases. 91.6% unanimous, 8.0% 2-1, 0.4% split.

Category Voted Unanimous 2-1 Split Max delta px
one word, integer font size 322 95.7% 4.3% 0.0% 1.000000
one word, fractional font size 230 95.2% 4.3% 0.4% 7.500458
wrapping at a fixed width 534 85.4% 14.6% 0.0% 1.000000
line-height in px 540 88.9% 8.3% 2.8% 1.000000
line-height as a number 270 88.9% 11.1% 0.0% 1.000000
white-space: pre 276 97.1% 2.9% 0.0% 1.000000
<span> inside a line 315 85.7% 14.3% 0.0% 1.000000
letter-spacing 414 95.7% 4.3% 0.0% 1.000000
kerning pair 238 97.5% 2.5% 0.0% 1.000000
text-align: center 442 92.8% 7.2% 0.0% 1.000000
all 3581 91.6% 8.0% 0.4% 7.500458

The per-category view is misleading on its own: the disagreement is not spread across categories, it is one quantity appearing in all of them. Grouped by what was measured rather than by which case measured it:

Quantity Voted Unanimous 2-1 Split Max delta px Outlier
element x 495 100.0% 0.0% 0.0% 0.000748 —
element y 495 99.0% 1.0% 0.0% 0.500000 firefox ×5
element width 495 100.0% 0.0% 0.0% 0.000641 —
element height 495 99.0% 1.0% 0.0% 1.000000 firefox ×5
element fragment count 495 100.0% 0.0% 0.0% 0.000000 —
element fragment width 215 100.0% 0.0% 0.0% 0.000641 —
element fragment height 215 97.7% 2.3% 0.0% 1.000000 firefox ×5
text fragment count 80 100.0% 0.0% 0.0% 0.000000 —
text fragment x 149 100.0% 0.0% 0.0% 0.001389 —
text fragment y 149 4.0% 85.9% 10.1% 0.875000 firefox ×124, chromium ×4
text fragment width 149 96.6% 2.7% 0.7% 7.500458 chromium ×4
text fragment height 149 6.7% 93.3% 0.0% 1.000000 firefox ×139

Firefox reports the text fragment one pixel taller, and half a pixel higher, than the other two. One word at 16px, the simplest case in the corpus:

Engine fragment y fragment height fragment width
Chromium 0.0 16.0 240.0
Firefox −0.5 17.0 240.0
WebKit 0.0 16.0 240.00045776367188

The widths agree to 0.00046 px, well inside the tolerance. The height does not, and it is the same 1 px in essentially every case — which is why text fragment height is unanimous only 6.7% of the time while every horizontal quantity is unanimous 96.6% or better.

The 7.5 px outlier is Chromium rounding glyph advances to whole pixels at a fractional font size. "Hamburgefonstiv" (15 glyphs) at font-size: 13.5px:

Engine fragment width per glyph
Chromium 210.0 14.0
Firefox 202.5 13.5
WebKit 202.49954223632812 13.499969…

Firefox and WebKit keep the fractional em advance; Chromium rounds it up to the next whole pixel and the error multiplies by the glyph count. This is the only place in Ahem where a horizontal quantity leaves the tolerance, and it needs a font size that is not a whole pixel — the same precondition docs/firefox-app-units.md found for the box-geometry divergence, with a different engine as the outlier.

Lato #

3401 measurements over 70 cases. 90.1% unanimous, 7.7% 2-1, 2.2% split.

Category Voted Unanimous 2-1 Split Max delta px
one word, integer font size 322 93.5% 6.2% 0.3% 1.000000
one word, fractional font size 230 94.3% 1.7% 3.9% 4.949997
wrapping at a fixed width 450 88.0% 10.0% 2.0% 2.185547
line-height in px 500 88.0% 8.0% 4.0% 1.000000
line-height as a number 250 88.0% 10.0% 2.0% 1.000000
white-space: pre 276 94.9% 2.9% 2.2% 1.000000
<span> inside a line 315 78.7% 18.1% 3.2% 1.812607
letter-spacing 414 93.5% 6.5% 0.0% 1.000000
kerning pair 230 93.5% 4.3% 2.2% 3.793808
text-align: center 414 91.3% 6.5% 2.2% 1.000000
all 3401 90.1% 7.7% 2.2% 4.949997
Quantity Voted Unanimous 2-1 Split Max delta px Outlier
element x 495 99.0% 1.0% 0.0% 1.812607 chromium ×5
element y 495 99.4% 0.6% 0.0% 0.500000 webkit ×3
element width 495 99.0% 0.6% 0.4% 0.400009 chromium ×3
element height 495 99.4% 0.6% 0.0% 1.000000 firefox ×3
element fragment count 495 100.0% 0.0% 0.0% 0.000000 —
element fragment width 215 97.7% 1.4% 0.9% 0.400009 chromium ×3
element fragment height 215 98.6% 1.4% 0.0% 1.000000 firefox ×3
text fragment count 80 100.0% 0.0% 0.0% 0.000000 —
text fragment x 104 81.7% 14.4% 3.8% 1.812607 chromium ×15
text fragment y 104 8.7% 77.9% 13.5% 0.750000 webkit ×77, firefox ×4
text fragment width 104 0.0% 50.0% 50.0% 4.949997 chromium ×52
text fragment height 104 8.7% 91.3% 0.0% 1.000000 firefox ×95

The three engines never once agreed on a line's width in Lato. Not one of the 104 measurements was unanimous; half were 2-1 with Chromium named, and the other half were three-way splits with no reference value at all. The kerning case at 16px shows all three mechanisms at once:

Engine fragment width fragment y fragment height
Chromium 157.0 −2.0 19.0
Firefox 159.51666259765625 −2.0 20.0
WebKit 160.52565002441406 −1.5 19.0

Chromium's whole-pixel rounding of the run width, seen in Ahem only at fractional font sizes, applies to every line in a proportional font, because a proportional advance is essentially never a whole pixel. That alone would leave a 2-1 vote with Chromium as the standing outlier. But Firefox and WebKit are 1.01 px apart from each other as well — they do not apply this font's kerning identically — so the measurement is a three-way split half the time.

<span> inside a line is the worst category (78.7% unanimous) for the same reason: the span's position depends on the width of the text before it, so a width difference becomes a position difference for everything after it on the line.

Loosening the tolerance does not help #

A measurement is unanimous at tolerance T exactly when the spread between the extreme engines is at most T, so one pass over the stored spreads answers the question for every T at once:

Tolerance Ahem Lato
1/60 px (the vote) 89.95% 88.08%
0.125 px 89.95% 88.39%
0.25 px 89.95% 89.84%
0.5 px 94.28% 93.98%
1 px 99.87% 98.62%
2 px 99.87% 99.43%
4 px 99.87% 99.86%
8 px 100.00% 100.00%

Going from 1/60 px to 0.25 px — fifteen times looser — buys nothing in Ahem and 1.8 points in Lato, because the disagreements are not sub-pixel. Reaching 99% needs a 1 px tolerance, sixty times the vote's, at which point the vote can no longer distinguish a correct box from one that is a whole pixel wrong; that is not a tolerance, it is giving up on geometry verification. Full agreement needs 8 px in both fonts.

Verdict #

Ahem: yes at 1/60 px, with Firefox a standing named outlier on the fragment's vertical extent. The vote's own bar is not unanimity — compare::vote accepts a 2-1 majority and takes the pair's value as the reference, skipping only a full split. By that bar 99.6% of Ahem measurements carry a trustworthy reference. A text-bearing generated function can be scored against a three-engine vote in Ahem, provided two things are understood: the reference for text fragment y and text fragment height will come from a Chromium–WebKit pair with Firefox outvoted about 90% of the time, which is a much weaker guarantee than the unanimity element geometry enjoys; and fractional font sizes must be avoided or Chromium becomes an outlier on width too.

Lato: no. Half of all text fragment width measurements are three-way splits, so half of the horizontal geometry of a line has no reference value at any tolerance the vote could adopt. The other half yields only a Firefox–WebKit pair, and those two are themselves over a pixel apart on kerned text. A generated function whose output is a line's width, a glyph's advance, or the position of anything after text on a line cannot be scored against this vote in a proportional font. Element geometry of the same pages remains votable at 99%+, so a function that consumes text only as an opaque inline box still can be.

What both fonts agree on, and it matters: line breaking. text fragment count and element fragment count were unanimous in 100% of measurements in both fonts, across the wrapping, white-space: pre, letter-spacing and text-align: center categories — the engines never once disagreed about how many lines a text node broke into or where the breaks fell. Where a line starts and how tall it is are contested; whether it starts is not.

What this blocks #

Against docs/platform-areas.md → "Text and internationalization" → "Text rendering (font loading and fallback, shaping, line breaking and hyphenation, glyph rasterization)":

  • Blocked in a proportional font: shaping and glyph advances. The measured quantity — the advance width of a run of Latin text — has no three-engine reference value half the time, so a generated shaping or advance-measurement function cannot be scored against this vote. This is the number, not a softening of it: 0 of 104 measurements unanimous.
  • Blocked in both fonts: the vertical extent of a line box as Range.getClientRects() reports it. Firefox differs by a whole pixel from Chromium and WebKit in over 90% of measurements, in Ahem as much as in Lato.
  • Not blocked: line breaking. It is the one part of that row this vote can carry today, in both fonts, at 1/60 px.
  • Not blocked: the element geometry of pages that contain text. A text-bearing document does not degrade the box-geometry vote — 99–100% unanimous, maximum difference 0.0007 px on element width in Ahem. The text-free corpora were a simplification, not a necessity, as far as element boxes are concerned.

An oracle that could score shaping would have to be something other than a three-engine geometry vote — the engines do not agree, so there is nothing for a vote to find.

Reproducing #

cargo run --release -p harness --bin text_metrics -- --random 60 --seed 0x7e47

It needs the pinned WPT checkout; node oracle/suites/fetch.mjs wpt fetches it if the fonts are missing, and the probe says so by name rather than measuring something else. node oracle/selftest.mjs --engines=chromium,firefox,webkit checks the probe end to end, including that the pinned Ahem really loaded — two of its glyphs at 100px are exactly 200px wide, which no substituted font would produce.