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 adata:URL and returns, per engine, the ordinary element dump plus every text node'sRange.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 isenumerated()plus 60 seeded-random cases at seed0x7e47, so 70 cases per font. - Quantities: each element's bounding box and per-fragment rects, voted by
crates/harness/src/compare.rsvote(); each text node's fragment count and per-fragment x, y, width and height, voted bycrates/harness/src/trace.rsvote_text(). Both useConsensusatdump::TOLERANCE_PX= 1/60 px. - Binary:
crates/harness/src/bin/text_metrics.rs. Raw responses are kept undercorpus/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.