Text Measurement & Font Metrics

Building a Web Browser Engine: Layout & Rendering

Chapter 6 · Text Measurement & Font Metrics

Chapter 5 built a real line-breaking algorithm and honestly flagged its own weak point: every character, at every font-size, measured as a flat 8px. This chapter replaces that placeholder with something a real browser would actually trust — and finds that the placeholder wasn't just imprecise, it was wrong in a way that would visibly break real pages.

A Real (Simplified) Per-Glyph Width Table, in Em Units

CHAR_WIDTHS_EM = { # narrow characters 'i': 0.28, 'l': 0.28, 'j': 0.28, '.': 0.28, ',': 0.28, # normal-width lowercase 'a': 0.50, 'e': 0.44, 'o': 0.50, ... # wide lowercase 'm': 0.78, 'w': 0.72, # uppercase -- generally wider than their own lowercase counterpart 'A': 0.67, 'W': 0.94, ... } DEFAULT_CHAR_WIDTH_EM = 0.50 # fallback for anything not in the table def char_width_px(ch, font_size_px): em = CHAR_WIDTHS_EM.get(ch, DEFAULT_CHAR_WIDTH_EM) return em * font_size_px def measure_word(word, font_size_px): return sum(char_width_px(ch, font_size_px) for ch in word)

Defining widths as a fraction of the font's own size — "em" units, matching how real font metrics actually work — is exactly what makes correct font-size scaling fall out for free: multiply every character's own em-width by whatever font_size_px actually is, and the whole word scales proportionally.

Bug 1: Chapter 5's Placeholder Had No Way to Know Font-Size Ever Mattered

Verified directly — and this is a real, already-published fact about this course's own UA stylesheet
measure_word_naive('Welcome') — Chapter 5's own flat model — has no font_size_px parameter at all. It returns 56, unconditionally, whether that word is rendered as tiny fine print or a giant headline. The real model gives 'Welcome' a width of 61.12px at the default 16px body-text size, and exactly double, 122.24px, at 32px. That doubling isn't an arbitrary test case — Course 1's own published UA_STYLESHEET already contains Rule(['h1'], [Declaration('font-size', '2em'), ...]). A real <h1>'s own text is measurably almost twice as wide, per word, as the identical text set as plain body copy — and the old placeholder had no way to represent that at all.

Bug 2: Even at One Fixed Font-Size, a Flat Model Ignores Real Glyph Shape

Verified directly — two equal-length strings, dramatically different real widths
measure_word_naive('iiiii') and measure_word_naive('mmmmm') both return 40 — five characters, flat 8px each, no distinction at all. The real model, at the same 16px: 'iiiii' comes out to 22.4px, 'mmmmm' to 62.4px — nearly three times wider, matching how dramatically different these two letters actually look in any real font.

The Real, Motivated Overflow: An H1's Own Text

A real <h1> containing "Welcome home now", available width 150px, actual font-size 32px (per the real UA rule above).

Verified directly — the old model packs all three words onto one line; the real width overflows by 116px
Fed Chapter 5's own line-breaking algorithm, using the OLD flat measurement: all three words end up on one single line, because their flat 8-per-character counts sum to 128, comfortably under the 150px budget. But measured correctly, at the real 32px this text is actually rendered at, that one line's true rendered width is 266.24px — a genuine 116px overflow the old model had no way to see, because it never had font-size as an input in the first place.
Verified directly — the real model wraps correctly, and both resulting lines genuinely fit
Line-broken again with the real, font-size-aware model: [['Welcome'], ['home', 'now']]. 'Welcome' alone already takes 122.24px at this size, so it correctly gets its own line before the 150px budget is exceeded. Both resulting lines' own real rendered widths — 122.24px and 135.04px — are independently confirmed within the 150px budget.

Where This Connects

This chapter's findingWhat it connects to
Widths defined in em units, scaling with font-sizeCourse 1 Chapter 7's own inheritance model — font-size is one of the properties Chapter 7 established as inheritable, meaning a real font-size genuinely does propagate down through nested inline elements like <b>, exactly the value this chapter's own measure_word needs
Chapter 5's own line-breaking loop reused completely unchangedChapter 5's own explicit forward reference — "the exact same break_into_lines() algorithm this chapter built stays unchanged; only measure_word()'s own implementation gets replaced" — confirmed exactly true here, only the measurement function itself changed
A real 116px overflow on genuinely real UA-stylesheet contentChapter 2's own overflow bug and Chapter 3's own overflow bug — a recurring theme across this entire layout course: a plausible simplification quietly produces boxes or lines that visually overflow their own intended space, caught only once real numbers are actually run through it

Hands-On Exercises

Exercise 1

Call char_width_px('#', 16.0) — a character with no explicit entry in CHAR_WIDTHS_EM. Confirm it falls back to DEFAULT_CHAR_WIDTH_EM rather than raising an error, and compute the exact pixel value it should return at 16px.

📄 View solution
Exercise 2

Compare char_width_px('a', 16.0) against char_width_px('A', 16.0). Confirm the uppercase letter measures wider than its own lowercase counterpart at the identical font-size, and explain why this matches real typography rather than being an arbitrary choice in the width table.

📄 View solution
Exercise 3

Compute measure_word('Welcome', 24.0) and compare it against measure_word('Welcome', 16.0). Confirm the relationship between the two is exactly what the em-based scaling model predicts, using a font-size that isn't a clean doubling of the 16px reference, and explain why this confirms the scaling is genuinely linear rather than a special case that only happens to work for 2x.

📄 View solution

Chapter 6 Quick Reference

  • Real per-glyph widths, in em units: each character's own width is a fraction of the font's own size — multiplying by font_size_px gives correct scaling for free
  • Real bug found and fixed: Chapter 5's own flat model had no font-size input at all — verified measuring a real <h1>'s own 32px text identically to 16px body text, causing a genuine 116px line overflow once run through real line-breaking
  • A second, independent bug: even at a single fixed font-size, a flat per-character model can't distinguish a narrow glyph ('i') from a wide one ('m') — fixed by the real per-character table
  • Verified: the scaling relationship is genuinely linear — doubling font-size exactly doubles measured width, and this holds at any font-size, not just a clean 2x
  • Chapter 5's own line-breaking algorithm needed zero changes — only the measurement function feeding it was replaced
  • Next chapter: Painting — turning a finished layout tree into an ordered list of paint commands, the real bridge to actual pixels