# text_metrics

> Every TextMetrics field (width, bounding boxes, baselines) of fixed strings in four generic font families, measured twice, as a digest. Not in either id.

Source: https://whiskerjs.com/signals/text-metrics/ (last updated 2026-10-27)

**text_metrics at a glance (from the library code, signalsVersion 14)**

| Field | Value |
| --- | --- |
| Component name | `text_metrics` |
| In the visitorId | no (reported, kept out of both ids) |
| structuredId group | none |
| Hashing | always a digest (first 128 bits of SHA-256) |
| Excluded under | Brave (brave_farbling) |
| Inner timeout | 1000 ms |
| Added in | [signalsVersion 11](https://whiskerjs.com/whats-new/) |
| Collector revision | [1](https://whiskerjs.com/docs/api/#component) (reported as `revision` with every result) |

> **In plain words:**
>
> How text is drawn depends on the fonts installed, their versions and the text engine of the system. A canvas can tell a page exactly how wide a piece of text is and how far it reaches above and below the line. WhiskerJS measures a few fixed sentences in the browser’s standard font families and turns all the numbers into one short code.

## What it reads

`CanvasRenderingContext2D.measureText()` on a canvas of its own, for three fixed strings:

1. `Cwm fjordbank glyphs vext quiz` (Latin letters)
2. `AVAWAy fi ffl Tj 0123456789` (kerning pairs, ligatures, digits)
3. a line of Cyrillic, Hebrew, Arabic, Devanagari, Chinese, Korean and Thai

each in `16px sans-serif`, `16px serif`, `16px monospace` and `16px system-ui`. From each measurement it takes every `TextMetrics` field: `width`, `actualBoundingBoxLeft`, `actualBoundingBoxRight`, `actualBoundingBoxAscent`, `actualBoundingBoxDescent`, `fontBoundingBoxAscent`, `fontBoundingBoxDescent`, `emHeightAscent`, `emHeightDescent`, `hangingBaseline`, `alphabeticBaseline` and `ideographicBaseline`. A field the browser does not offer is left empty.

## How the technique works

Each number is rounded to three decimals; the twelve fields of each measurement form one line and the twelve lines are hashed together. The whole measurement runs twice: if the two passes differ, the component is `noisy` / `read_varies`. Nothing is drawn and nothing is added to the page. Generic families cannot name a page’s `@font-face` rule, so measuring never starts a font download.

## Why it helps

Advance widths alone (as in `fonts_canvas`) mostly say which fonts are installed. Bounding boxes and baselines also follow font versions, hinting and the text engine, and the multi-script line shows which fallback fonts the system picks for each script. It is a richer view of the text stack than a width.

## Stability

Expected to be stable until fonts or the operating system change. In the hosted CI runs of PR #29 each browser gave the same value in every collection and on both runners. It is reported outside both ids until measured on real devices; it is a candidate for the `fonts` group. Added in library PR #29 (2026-10-27) at signalsVersion 11: it is outside both ids, so adding it changed no id and needed no version bump.

## Browser support and what blocks it

- `ok` wherever a 2D canvas exists: Chromium, Chrome, Firefox and WebKit in CI.
- Firefox with Fingerprinting Protection or resistFingerprinting gave a different value from plain Firefox in CI (both restrict which system fonts pages see), the same in every collection: a privacy setting changes it, as expected.
- [Brave](https://whiskerjs.com/browsers/brave/): `excluded` / `brave_farbling`, because Brave shows each site a per-session subset of the system fonts.
- `unavailable` / `no_context` without a 2D canvas.

## Privacy

Text measurements reveal installed fonts indirectly, like the other font signals. The value leaves the library only as a digest. No prompt, no request, no font download.

## How WhiskerJS handles noise and failures

| Status | ruleId | When |
| --- | --- | --- |
| `ok` |   | a digest |
| `noisy` | `read_varies` | the two passes gave different numbers |
| `unavailable` | `no_document`, `no_api`, `no_context` | no document, canvas or 2D context |
| `unavailable` | `no_value` | the metrics held no numbers |
| `unavailable` | `no_digest` | no Web Crypto |
| `blocked` | `threw`, `security_error` | `measureText` threw |
| `excluded` | `brave_farbling`, `caller_excluded` | exclusion table or your `exclude` list |
| `timed_out` | `timeout`, `aborted` | budget or abort |

## Example value

```json
"text_metrics": { "status": "ok", "value": "c41e8a7d02f9b3651e7c0a4d8b92f6e3", "hashed": true, "durationMs": 2.4, "revision": 1 }
```

## Questions and answers

### Can text_metrics download a font?

No. It measures only in the generic families sans-serif, serif, monospace and system-ui, which resolve to fonts installed on the device and can never name a web font of the page.

### What if a browser adds noise to measureText?

Every string is measured twice. If the two reads differ the component reports noisy with ruleId read_varies instead of a value that changes on every read.

## Related pages

- [fonts_canvas](https://whiskerjs.com/signals/fonts-canvas/): Installed fonts by canvas width, in the id.
- [font_preferences](https://whiskerjs.com/signals/font-preferences/): Default font widths, in the id.
- [emoji_metrics](https://whiskerjs.com/signals/emoji-metrics/): The same measurement for emoji.
