measureText is treated as a function that returns a width. It returns an object with twelve properties, and the width is the one that everybody uses because it was the only one for years.
The other eleven matter now, because the standard answer to the font-metric problem has become metric-compatible substitution. If a persona claims Windows on a machine that has no Windows fonts, you register substitutes designed to match: Selawik for Segoe UI, Carlito for Calibri, Caladea for Cambria. All three were built for document layout, where the requirement is that a paragraph occupies the same space and breaks in the same places as it would with the original.
Read that requirement again. Metric-compatible means the advance widths match. It does not say anything about the shape of the glyphs, the extent of their ink, where the font declares its ascent and descent, or where it puts its baselines. Those are all in the same return value.
What the object actually contains
Measured on Chrome 151 on macOS, 72px of the string Hamburgefonstiv 123, four real families and one family nobody has installed:
| Helvetica Neue | Arial | Georgia | Menlo | unknown family | |
|---|---|---|---|---|---|
width |
690.192 | 684.387 | 686.918 | 823.605 | 708.552 |
actualBoundingBoxLeft |
−5.616 | −5.766 | −3.445 | −4.816 | −5.184 |
actualBoundingBoxRight |
686.88 | 681.117 | 683.262 | 818.191 | 704.952 |
actualBoundingBoxAscent |
51.984 | 52.418 | 54.457 | 54.703 | 52.416 |
actualBoundingBoxDescent |
15.048 | 15.152 | 15.609 | 15.469 | 15.264 |
fontBoundingBoxAscent |
69 | 65 | 66 | 67 | 76 |
fontBoundingBoxDescent |
15 | 15 | 16 | 17 | 24 |
hangingBaseline |
55.2 | 52 | 52.8 | 53.6 | 60.8 |
alphabeticBaseline |
0 | 0 | 0 | 0 | 0 |
ideographicBaseline |
−15 | −15 | −16 | −17 | −10.08 |
emHeightAscent and emHeightDescent are in the specification and return null in this build, which is itself a stable fact about the engine.
Two groups are worth separating.
actualBoundingBox* describe the ink: the extent of the pixels this particular string would mark, relative to the text origin. They depend on the glyphs and on which characters are in the string, so they vary with content.
fontBoundingBox*, the baselines and the em-height fields describe the font: values read out of the font’s own tables, independent of the string being measured. They do not change if you measure a different word. They are metadata about the file.
That distinction is the whole point of this post. Advance widths and font metadata are two different things, and metric compatibility is a promise about the first.
Why a substitute cannot match the second group
Selawik matches Segoe UI’s advance widths because it was drawn and hinted to do so. It is a different typeface. Its glyphs have different ink, so actualBoundingBoxAscent for a string with tall letters is not going to land on the same number. And its font tables carry its own ascent, descent and line gap, so fontBoundingBoxAscent, fontBoundingBoxDescent, hangingBaseline and ideographicBaseline come from Selawik’s file rather than Segoe UI’s.
The unknown-family column above shows how far apart these can be even when widths are in the same neighbourhood. The fallback font’s width, 708.552, sits within three percent of Helvetica Neue’s 690.192, close enough that a width-only check with a sloppy tolerance would not separate them. Its fontBoundingBoxAscent is 76 against Helvetica Neue’s 69, and its descent is 24 against 15. That is a ten and sixty percent difference respectively, in numbers that do not depend on which string you measured.
So the substitution trick, which genuinely does defeat the width collapse, leaves a second-order signal behind: the widths are now right and the font metadata is a different font’s. Detecting it requires a reference table - you need to know what Segoe UI’s fontBoundingBoxAscent is at 72px - which is a real cost, and it is a cost a serious detector pays once and amortises across everyone.
Where the ratios help
Reference tables have a version problem: fonts get updated and the numbers move. Ratios are more durable than absolute values, and several of them are close to invariant per typeface:
fontBoundingBoxAscent / fontBoundingBoxDescentdescribes how the font apportions its vertical space, and it is scale-free. Helvetica Neue reads 4.6 above, Arial 4.33, Georgia 4.125, Menlo 3.94, and the fallback 3.17.actualBoundingBoxAscent / widthfor a fixed string is a shape signature that survives font-size changes.hangingBaseline / fontBoundingBoxAscentcatches substitutes that got the overall box right and the baseline placement wrong.
A check built on ratios can flag “these numbers are internally inconsistent with any single font” without maintaining a per-font table. It is a weaker signal than an exact match against a reference, and it does not go stale.
The rest of the surface, briefly
measureText is not the only place font metadata escapes.
Layout produces the same information through geometry: an inline element’s height at a known line-height: normal is derived from the font’s ascent, descent and line gap, so offsetHeight on a span carries fontBoundingBox in a different unit. A font mask that patches the canvas path and not the layout path disagrees with itself, which is the older and better-known version of this check.
The document.fonts interface exposes loaded font faces and their descriptors, and FontFaceSet.check() answers a different question again - whether a family is usable, including page-declared web fonts. Three paths, three chances to disagree.
And beneath all of it there is rendering. Two fonts with identical advance widths still put different pixels on a canvas, so a hash of rasterised text separates them without any metrics at all. Which is the older canvas fingerprint, and the reason a font strategy has to be evaluated against the whole stack rather than against the one number everyone measures.
What this means for both sides
For a detector: read the whole object. It is one call, it already returns everything, and the fields nobody reads are exactly the fields nobody spoofs. Prefer the string-independent ones for identity, since they describe the font rather than your probe text.
For anyone building a persona: metric-compatible substitution is a genuine improvement over shipping names with no files behind them, and it is not a complete answer. The honest ranking is that shipping the real fonts is best where licensing allows it - which for an Android persona it entirely does, since Roboto and the Noto families are openly licensed - substitution is a solid second where it does not, and a name in an allow-list with no file behind it is not a strategy at all.