Skip to content

Apply the metrics and the collection font a fontdata entry sets to the font it registers (#321) - #322

Merged
jakejackson1 merged 1 commit into
gravitypdffrom
fix/321-fontdata-metric-overrides
Sep 22, 2026
Merged

jakejackson1 merged 1 commit into
gravitypdffrom
fix/321-fontdata-metric-overrides

Conversation

@jakejackson1

@jakejackson1 jakejackson1 commented Sep 22, 2026

Copy link
Copy Markdown
Member

Two fontdata settings have been read wrongly since v7.1.8, both broken by the same commit: the Ascent, Descent and Leading overrides have no effect, and TTCfontID always selects the first font of a TrueType Collection. This fixes both.

Cause: the metric overrides

  • Before 93515f7: AddFont() loaded the font cache by require-ing a PHP file that defined $desc. The override block changed $desc, and the font was registered with 'desc' => $desc.
  • 93515f7 ("Convert tmp PHP Font files to JSON", first released in v7.1.8) loads the cached metrics into $font and registers 'desc' => $font['desc']. The override block was left writing to $desc, which nothing reads any more.

Fix: the metric overrides

The three assignments now write to $font['desc'] instead of $desc.

  • The isset(...) && $this->fontdata[$family][...] test is unchanged from before v7.1.8, so an override of 0 is still ignored. For Leading, 0 is what most fonts declare anyway. For Ascent, _getNormalLineheight() and _setLineYpos() treat 0 as "no metrics" and use the fallback, so 0 could never have been a useful override.
  • $font is this document's copy of what was read from .mtx.json, and nothing writes the cache after this point. The cache keeps the font's own metrics, and another document sharing the tempDir gets them.

What the overrides affect

The override now reaches the registered font's desc, which is read in two places:

  • Layout. _getNormalLineheight() builds the normal line height from Ascent - Descent + Leading. _setLineYpos() uses Ascent and Descent for the box top and bottom, which place the baseline, and adds Leading again as a line gap.
  • The embedded FontDescriptor. FontWriter writes every key of desc into it, so /Ascent, /Descent and /Leading carry the overrides.

Both match how the overrides behaved before v7.1.8.

Measured

This is the example from #321, with the overrides set through the fontdata config so they are in place before the default font is added. DejaVu Sans at 11pt, in <p>Line one<br>Line two<br>Line three</p>:

desc Ascent / Descent / Leading First baseline (pt) Distance between baselines (pt) FontDescriptor
No overrides 928 / -236 / 0 785.431 14.596 /Ascent 928 /Descent -236 /Leading 0
Overrides, before this PR 928 / -236 / 0 785.431 14.596 /Ascent 928 /Descent -236 /Leading 0
Overrides, with this PR 1500 / -900 / 400 773.480 39.512 /Ascent 1500 /Descent -900 /Leading 400

Before this PR, the document with overrides was byte-identical to the one without. With it, 39.512 = (1.14 × 2.8 + 0.4) × 11: adjustFontDescLineheight times the overridden metrics, plus the line gap. A document that sets no overrides renders as before, and the snapshot suite passes unchanged.

(The issue's own snippet sets $mpdf->fontdata[...] after construction, when the default font is already registered, so it shows no change even with this fix. A font that is first used after the assignment does take the overrides.)

The TTCfontID fix

93515f7 also rewrote the TTCfontID lookup as

$TTCfontID = isset($this->fontdata[$family]['TTCfontID'][$stylekey]) ? isset($this->fontdata[$family]['TTCfontID'][$stylekey]) : 0;

so every configured font of a collection became true. TTFontFile::selectFont() indexes its offsets with it, and true reads as 1, so the first font was registered and embedded whichever one was asked for. Before v7.1.8 it was $TTCfontID = $this->fontdata[$family]['TTCfontID'][$stylekey];. It is the configured value again.

That fix alone would leave caches wrong: .mtx.json is keyed by font key only, and nothing checked the face it was generated from. A cache written before this PR for a TTCfontID of 2 or more holds the first font's metrics, and the same happens when someone edits TTCfontID. fontMetrics() now regenerates when the cached TTCfontID differs from the configured one. The comparison is between integers, because the old caches hold true, and true == 2. A plain font caches 0 and is configured as 0, so nothing else is regenerated.

Measured with a collection of two fixture fonts, TTCfontID set to ['R' => 2]:

fonts[...]['TTCfontID'] fonts[...]['name'] Embedded FontName
Before true NotoSans-Regular MPDFAA+NotoSans-Regular
After 2 Carlito-Regular MPDFAA+Carlito-Regular

Tests

tests/Mpdf/FontdataMetricOverrideTest.php, with each test using its own tempDir:

  • Each override on its own, and all three together, end up on $mpdf->fonts['dejavusans']['desc']. Metrics that are not overridden keep the font's values.
  • The distance between the baselines of three rendered lines matches the formula above, both with and without the overrides.
  • The FontDescriptor carries the overridden values.
  • After a document with the overrides, dejavusans.mtx.json still holds 928 / -236 / 0, and a second document without overrides gets those values.

Six of the seven cases fail on gravitypdf without the fix. The cache case passes either way, because it guards against the fix leaking into the cache.

tests/Mpdf/TtcFontIdTest.php has no binary fixture, because the repo has no .ttc. Its set-up packs two fixture fonts from tests/data/ttf/ into a collection, rewriting each table offset to where the font now starts.

  • For each of the two fonts, the registered TTCfontID and name, and the embedded FontName, match the font configured.
  • Face 1 then face 2 in the same tempDir gives Carlito for the second, so a cache for another font of the collection is not reused.

All three cases fail on gravitypdf. With only the one-line fix, the shared-cache case still fails.

Notes

  • Leave no trace of a backup font the substitution scan tries but does not use (#315) #318 merged while this was in progress, so this branch is rebased onto it and the override lines now sit in registerFont(). The mpdf/mpdf mirror makes the same change in AddFont(). It needs the TTCfontID fix and its cache check too, since development has the same code.
  • The mpdf/mpdf PR needs a changelog entry covering both fixes. Documents that set the overrides will render differently: their line heights, their baselines and their FontDescriptor all change. Documents that use any font of a collection but the first will switch to the font they asked for.

Fixes #321

🤖 Generated with Claude Code

@jakejackson1 jakejackson1 added bug Something isn't working create-upstream-pr labels Sep 22, 2026
…e font it registers (#321)

Since 93515f7 the Ascent, Descent and Leading overrides were written to a local $desc
that nothing read, while the font was registered with the metrics from the cache. They
now go into $font['desc'], as they went into $desc before v7.1.8, so line heights, the
baseline and the embedded FontDescriptor follow them again. The cache is not rewritten,
so another document reading it gets the font's own metrics.

The same commit set TTCfontID to the result of isset(), so every configured font of a
TrueType Collection was read as its first. It is the configured index again, and metrics
cached for another font of the collection, including the ones cached while every font
was read as the first, are regenerated rather than reused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jakejackson1
jakejackson1 force-pushed the fix/321-fontdata-metric-overrides branch from 07d587a to 6f321eb Compare September 22, 2026 07:20
@jakejackson1 jakejackson1 changed the title Apply the Ascent, Descent and Leading a fontdata entry sets to the font it registers (#321) Apply the metrics and the collection font a fontdata entry sets to the font it registers (#321) Sep 22, 2026
@jakejackson1
jakejackson1 merged commit 9d585a1 into gravitypdf Sep 22, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working create-upstream-pr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The Ascent, Descent and Leading overrides in fontdata have been ignored since v7.1.8

1 participant