diff --git a/src/opentype/OTProcessor.js b/src/opentype/OTProcessor.js index 80d1ce69..a6e25e8c 100644 --- a/src/opentype/OTProcessor.js +++ b/src/opentype/OTProcessor.js @@ -17,9 +17,19 @@ export default class OTProcessor { this.features = {}; this.lookups = {}; - // Setup variation substitutions - this.variationsIndex = font._variationProcessor - ? this.findVariationsIndex(font._variationProcessor.normalizedCoords) + // Setup variation substitutions. FeatureVariations apply at the font's + // current location, defaulting to the normalized origin (all-zero coords) + // when no variation is set — matching HarfBuzz, which always evaluates them. + // fontkit only builds a _variationProcessor once coords are applied (or for + // CFF2), so a plain variable font would otherwise skip FeatureVariations + // (e.g. default-location bracket-layer substitutions) entirely. + let variationCoords = font._variationProcessor + ? font._variationProcessor.normalizedCoords + : font.fvar + ? new Array(font.fvar.axis.length).fill(0) + : null; + this.variationsIndex = variationCoords + ? this.findVariationsIndex(variationCoords) : -1; // initialize to default script + language diff --git a/test/data/fonttest/TestFeatureVariations.py b/test/data/fonttest/TestFeatureVariations.py new file mode 100644 index 00000000..87434f4a --- /dev/null +++ b/test/data/fonttest/TestFeatureVariations.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# Generates TestFeatureVariations.ttf: a minimal TTF variable font whose `rvrn` +# FeatureVariations substitutes A -> A.alt under a condition set that covers the +# default location (normalized wght in [0, 1], which includes the default 0). +# Used to test that fontkit evaluates FeatureVariations at the default location. +# Build: python3 TestFeatureVariations.py TestFeatureVariations.ttf +import sys +from fontTools.fontBuilder import FontBuilder +from fontTools.varLib.featureVars import addFeatureVariations +from fontTools.pens.ttGlyphPen import TTGlyphPen + + +def box(x0, y0, x1, y1): + pen = TTGlyphPen(None) + pen.moveTo((x0, y0)); pen.lineTo((x0, y1)) + pen.lineTo((x1, y1)); pen.lineTo((x1, y0)); pen.closePath() + return pen.glyph() + + +glyphs = ['.notdef', 'A', 'A.alt'] +fb = FontBuilder(1000, isTTF=True) +fb.setupGlyphOrder(glyphs) +fb.setupCharacterMap({0x41: 'A'}) +fb.setupGlyf({'.notdef': box(0, 0, 0, 0), 'A': box(100, 0, 500, 700), + 'A.alt': box(100, 0, 400, 700)}) +fb.setupHorizontalMetrics({g: (600, 100) for g in glyphs}) +fb.setupHorizontalHeader(ascent=800, descent=-200) +fb.setupNameTable({'familyName': 'TestFeatureVariations', 'styleName': 'Regular'}) +fb.setupOS2(); fb.setupPost() +fb.setupFvar(axes=[('wght', 0, 400, 1000, 'Weight')], instances=[]) +fb.setupGvar({}) +addFeatureVariations(fb.font, [([{'wght': (0, 1)}], {'A': 'A.alt'})], featureTag='rvrn') +fb.save(sys.argv[1]) diff --git a/test/data/fonttest/TestFeatureVariations.ttf b/test/data/fonttest/TestFeatureVariations.ttf new file mode 100644 index 00000000..338670da Binary files /dev/null and b/test/data/fonttest/TestFeatureVariations.ttf differ diff --git a/test/shaping.js b/test/shaping.js index dc005ea0..0ea749ac 100644 --- a/test/shaping.js +++ b/test/shaping.js @@ -582,4 +582,15 @@ describe('shaping', function () { test('SHBALI-2/12', 'NotoSans/NotoSansBalinese-Regular.ttf', "ᬓ᭄ᭅᬸ", '23+2275|162+0|60@0,-1000+0'); }); }); + + describe('FeatureVariations', function () { + it('applies FeatureVariations at the default variation location', function () { + // TestFeatureVariations.ttf's rvrn feature substitutes A -> A.alt under a + // condition set covering the default location. HarfBuzz applies it; so + // must fontkit, even with no explicit variation instance selected. + let font = fontkit.openSync(new URL('data/fonttest/TestFeatureVariations.ttf', import.meta.url)); + let { glyphs } = font.layout('A'); + assert.deepEqual(glyphs.map(g => g.name), ['A.alt']); + }); + }); });