Skip to content

Keep a glyph's variation metrics stable when several composites use it - #393

Merged
blikblum merged 2 commits into
foliojs:masterfrom
youdie006:vf-composite-metrics
Oct 8, 2026
Merged

blikblum merged 2 commits into
foliojs:masterfrom
youdie006:vf-composite-metrics

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

In a glyf variable font without HVAR, a glyph's advance drifts each time a composite that uses it is decoded:

const font = fontkit.openSync('test/data/Mada/Mada-VF.ttf').getVariation({ wght: 100 })
font.layout('ìíîïı').positions.map(p => p.xAdvance)
// [229, 229, 229, 229, 148]; harfbuzz gives [229, 229, 229, 229, 229]

ı is a component of the other four. _getPhantomPoints builds the phantom points from this._metrics, and _getContours then overwrites this._metrics with the varied values. A component is decoded again for every composite, so its gvar delta (−27 here) is applied once per composite: 229, 202, 175, 148. This keeps a copy of the default metrics on the first call and builds the phantom points from it.

The new test in test/variations.js fails on master; building from the live metrics, aliasing instead of copying, or re-copying on every call each fail it. npm test: 547 passing, 1 failing (i18n setDefaultLanguage "ar", which also fails on master). #389 touches the same file but not _getPhantomPoints.

Written with AI assistance (Claude); I have reviewed the change.

_getPhantomPoints built the phantom points from this._metrics, which
_getContours replaces with the varied metrics. A glyph is decoded again
for every composite that uses it, so the gvar delta was applied once per
composite: in Mada-VF at wght 100, dotless i advanced 229, 202, 175, 148
after laying out its accented composites. Build the phantom points from
the default metrics.

@blikblum blikblum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the visual practical effect of the bug?

Comment thread src/glyph/TTFGlyph.js Outdated
let { advanceWidth, advanceHeight, leftBearing, topBearing } = this._metrics;
// _getContours replaces this._metrics with the varied metrics, and a glyph
// is decoded again for every composite using it, so keep the default ones.
if (this._defaultMetrics == null) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (this._defaultMetrics == null) {
if (!this._defaultMetrics) {

Comment thread src/glyph/TTFGlyph.js
}

let { advanceWidth, advanceHeight, leftBearing, topBearing } = this._metrics;
// _getContours replaces this._metrics with the varied metrics, and a glyph

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if _getContours could be made side effect free

@youdie006

youdie006 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Applied the suggestion, thanks.

The visible effect: in a glyf variable font without HVAR, at a non-default instance, a base letter laid out after two or more different accented letters built from it gets a narrower advance, so the following glyphs move left and crowd it. With Mada-VF.ttf at wght: 100 (1000 upm), font.layout(s).positions.map(p => p.xAdvance):

text master this PR harfbuzz
éèe 483, 483, 463 483, 483, 483 483, 483, 483
àáa 491, 491, 470 491, 491, 491 491, 491, 491
öóo 535, 535, 523 535, 535, 535 535, 535, 535
ìíîïı 229, 229, 229, 229, 148 229 ×5 229 ×5

It loses one gvar delta for every additional composite that decodes the base glyph, and it depends on the order glyphs are first touched. So in French text the e after é and è comes out about 4% (20 units) too narrow at that weight. I added the éèe case to the test.

On making _getContours side-effect free: I agree it would be cleaner. The variation metrics would then be computed and cached somewhere other than inside contour decoding. That touches how advanceWidth/_getMetrics read them, and #389 is reworking the same area, so I kept this to the minimal change. I can do the refactor as a follow-up if you prefer it that way.

@blikblum

blikblum commented Oct 8, 2026

Copy link
Copy Markdown
Member

Lets make minimal changes, one at time

@blikblum
blikblum merged commit d4952fd into foliojs:master Oct 8, 2026
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.

2 participants