Skip to content

Measure intrinsic text width the way the line breaker does - #184

Merged
danmolitor merged 3 commits into
mainfrom
fix/intrinsic-width-shaped
Oct 6, 2026
Merged

danmolitor merged 3 commits into
mainfrom
fix/intrinsic-width-shaped

Conversation

@danmolitor

@danmolitor danmolitor commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Addresses #181. Thanks @santhiprakash for the detailed report and the measurements.

Intrinsic width summed each character's unshaped advance, while both line breakers measure shaped text. When GPOS kerning widened a word, a content-sized box came out a fraction of a point narrower than its line, and the breaker split the word. In IBM Plex Serif 10pt, "Mumbai" drew as "Mumba" / "i", "Mumbai," as "Mumbai" / ",", and "Bengaluru" as "Bengalur" / "u". This happened for plain text as well as nested runs: with a single font family, plain text also takes the shaped path.

Intrinsic width now uses each breaker's own measurement: measure_width for plain text (as min-content already did) and the new measure_runs_width for runs. One helper builds the run characters for both layout and measurement, so they always see the same characters.

This also fixes a second measurement bug. Runs summed every run's width, so text with a hard break (an HTML <span>…</span><br><span>…</span> cell) measured as all its lines end to end. It now measures as its widest line, which is CSS max-content.

Tests (both fail on main)

  • content_sized_text_holds_its_shaped_line: the three strings above in a 200pt space-between row, as plain text and as a nested run, in IBM Plex Serif (added as an OFL test fixture). On main: ["Mumba", "i"].
  • styled_text_with_a_hard_break_is_as_wide_as_its_wider_line: on main, two lines measure 240.15pt when the wider one is 167.61pt.

What moves

  • HTML invoice fixture: the right-aligned address block (lines split by <br>) is now 110.1pt wide instead of 208.7pt. The drawn PDF is byte-identical; the structural baseline is updated.
  • Template-compat corpus: the two Crater invoices' item tables redistribute their columns (each file changes by 1 to 4 bytes). The item cells are <span>/<br>/<span>, so they no longer claim both lines' width. Checked against Chrome with a controlled copy of that table, using explicit units: the Quantity column starts 7pt from Chrome's position instead of 43pt (Price: 3.5pt instead of 21pt). The Crater template on its own drifts the other way, because of differences in that template that the old over-measurement partly offset (unitless font sizes, a narrower table). Those are unrelated to this change. The corpus baseline is refreshed at release time.
  • Engine (672), visual, html and package tests pass; the byte wall is identical; veraPDF passes PDF/UA-1 and every PDF/A level. Gallery manifest regenerated (hashes only).

Performance

Intrinsic width now shapes text instead of summing advances. That's slower for registered fonts, and it adds up where layout measures the same cells repeatedly, as automatic table layout does. Native forme-html, median of 5 runs:

Workload main this PR
300-row table, registered font (IBM Plex Serif) 231 ms 268 ms (+16%)
300 flex rows, registered font 185 ms 170 ms (no change)
All 15 template-compat corpus documents 211 ms 226 ms (+7%)

Not noticeable at these sizes, so no cache is added. If large tables in registered fonts ever matter, a per-node intrinsic-width cache is the fix.

danmolitor and others added 3 commits October 6, 2026 09:08
Intrinsic width summed each character's unshaped advance, while both
line breakers measure shaped text. When GPOS kerning widened a word,
a content-sized box came out a fraction of a point narrower than the
line it had to hold, and the breaker split the word: "Mumbai" in IBM
Plex Serif 10pt drew as "Mumba" / "i" in a flex row, as plain text and
as a nested run alike.

Plain text is now measured with measure_width (the plain breaker's
measurement, as min-content already uses) and runs with the new
measure_runs_width (the runs breaker's). The StyledChar list is built by
one helper for layout and measurement, so both see the same chars.

The test lays out the reported strings in IBM Plex Serif, added as an
OFL fixture; it fails before this change on all three.

Addresses #181.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Styled runs used to sum every run's width, so text with a hard break
measured as all its lines laid end to end. Measuring through the
StyledChar list split at '\n' takes the widest line instead, as CSS
max-content does. The new test fails before the fix (240.15pt for two
lines whose wider one is 167.61pt).

The HTML invoice fixture's address block (lines split by <br>) is now
110.1pt wide instead of 208.7pt; its drawn PDF is byte-identical, so the
structural baseline is accepted. Gallery manifest regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…haped

# Conflicts:
#	docs/images/templates/manifest.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant