| from __future__ import print_function, division, absolute_import |
| from fontTools.misc.py23 import * |
| from fontTools import ttLib |
| from fontTools import designspaceLib |
| from fontTools.feaLib.builder import addOpenTypeFeaturesFromString |
| from fontTools.ttLib.tables import _f_v_a_r, _g_l_y_f |
| from fontTools.ttLib.tables import otTables |
| from fontTools.ttLib.tables.TupleVariation import TupleVariation |
| from fontTools import varLib |
| from fontTools.varLib import instancer |
| from fontTools.varLib.mvar import MVAR_ENTRIES |
| from fontTools.varLib import builder |
| from fontTools.varLib import featureVars |
| from fontTools.varLib import models |
| import collections |
| from copy import deepcopy |
| import logging |
| import os |
| import re |
| import pytest |
| |
| |
| TESTDATA = os.path.join(os.path.dirname(__file__), "data") |
| |
| |
| @pytest.fixture |
| def varfont(): |
| f = ttLib.TTFont() |
| f.importXML(os.path.join(TESTDATA, "PartialInstancerTest-VF.ttx")) |
| return f |
| |
| |
| @pytest.fixture(params=[True, False], ids=["optimize", "no-optimize"]) |
| def optimize(request): |
| return request.param |
| |
| |
| @pytest.fixture |
| def fvarAxes(): |
| wght = _f_v_a_r.Axis() |
| wght.axisTag = Tag("wght") |
| wght.minValue = 100 |
| wght.defaultValue = 400 |
| wght.maxValue = 900 |
| wdth = _f_v_a_r.Axis() |
| wdth.axisTag = Tag("wdth") |
| wdth.minValue = 70 |
| wdth.defaultValue = 100 |
| wdth.maxValue = 100 |
| return [wght, wdth] |
| |
| |
| def _get_coordinates(varfont, glyphname): |
| # converts GlyphCoordinates to a list of (x, y) tuples, so that pytest's |
| # assert will give us a nicer diff |
| return list(varfont["glyf"].getCoordinatesAndControls(glyphname, varfont)[0]) |
| |
| |
| class InstantiateGvarTest(object): |
| @pytest.mark.parametrize("glyph_name", ["hyphen"]) |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| pytest.param( |
| {"wdth": -1.0}, |
| { |
| "hyphen": [ |
| (27, 229), |
| (27, 310), |
| (247, 310), |
| (247, 229), |
| (0, 0), |
| (274, 0), |
| (0, 536), |
| (0, 0), |
| ] |
| }, |
| id="wdth=-1.0", |
| ), |
| pytest.param( |
| {"wdth": -0.5}, |
| { |
| "hyphen": [ |
| (33.5, 229), |
| (33.5, 308.5), |
| (264.5, 308.5), |
| (264.5, 229), |
| (0, 0), |
| (298, 0), |
| (0, 536), |
| (0, 0), |
| ] |
| }, |
| id="wdth=-0.5", |
| ), |
| # an axis pinned at the default normalized location (0.0) means |
| # the default glyf outline stays the same |
| pytest.param( |
| {"wdth": 0.0}, |
| { |
| "hyphen": [ |
| (40, 229), |
| (40, 307), |
| (282, 307), |
| (282, 229), |
| (0, 0), |
| (322, 0), |
| (0, 536), |
| (0, 0), |
| ] |
| }, |
| id="wdth=0.0", |
| ), |
| ], |
| ) |
| def test_pin_and_drop_axis(self, varfont, glyph_name, location, expected, optimize): |
| instancer.instantiateGvar(varfont, location, optimize=optimize) |
| |
| assert _get_coordinates(varfont, glyph_name) == expected[glyph_name] |
| |
| # check that the pinned axis has been dropped from gvar |
| assert not any( |
| "wdth" in t.axes |
| for tuples in varfont["gvar"].variations.values() |
| for t in tuples |
| ) |
| |
| def test_full_instance(self, varfont, optimize): |
| instancer.instantiateGvar( |
| varfont, {"wght": 0.0, "wdth": -0.5}, optimize=optimize |
| ) |
| |
| assert _get_coordinates(varfont, "hyphen") == [ |
| (33.5, 229), |
| (33.5, 308.5), |
| (264.5, 308.5), |
| (264.5, 229), |
| (0, 0), |
| (298, 0), |
| (0, 536), |
| (0, 0), |
| ] |
| |
| assert "gvar" not in varfont |
| |
| def test_composite_glyph_not_in_gvar(self, varfont): |
| """ The 'minus' glyph is a composite glyph, which references 'hyphen' as a |
| component, but has no tuple variations in gvar table, so the component offset |
| and the phantom points do not change; however the sidebearings and bounding box |
| do change as a result of the parent glyph 'hyphen' changing. |
| """ |
| hmtx = varfont["hmtx"] |
| vmtx = varfont["vmtx"] |
| |
| hyphenCoords = _get_coordinates(varfont, "hyphen") |
| assert hyphenCoords == [ |
| (40, 229), |
| (40, 307), |
| (282, 307), |
| (282, 229), |
| (0, 0), |
| (322, 0), |
| (0, 536), |
| (0, 0), |
| ] |
| assert hmtx["hyphen"] == (322, 40) |
| assert vmtx["hyphen"] == (536, 229) |
| |
| minusCoords = _get_coordinates(varfont, "minus") |
| assert minusCoords == [(0, 0), (0, 0), (422, 0), (0, 536), (0, 0)] |
| assert hmtx["minus"] == (422, 40) |
| assert vmtx["minus"] == (536, 229) |
| |
| location = {"wght": -1.0, "wdth": -1.0} |
| |
| instancer.instantiateGvar(varfont, location) |
| |
| # check 'hyphen' coordinates changed |
| assert _get_coordinates(varfont, "hyphen") == [ |
| (26, 259), |
| (26, 286), |
| (237, 286), |
| (237, 259), |
| (0, 0), |
| (263, 0), |
| (0, 536), |
| (0, 0), |
| ] |
| # check 'minus' coordinates (i.e. component offset and phantom points) |
| # did _not_ change |
| assert _get_coordinates(varfont, "minus") == minusCoords |
| |
| assert hmtx["hyphen"] == (263, 26) |
| assert vmtx["hyphen"] == (536, 250) |
| |
| assert hmtx["minus"] == (422, 26) # 'minus' left sidebearing changed |
| assert vmtx["minus"] == (536, 250) # 'minus' top sidebearing too |
| |
| |
| class InstantiateCvarTest(object): |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| pytest.param({"wght": -1.0}, [500, -400, 150, 250], id="wght=-1.0"), |
| pytest.param({"wdth": -1.0}, [500, -400, 180, 200], id="wdth=-1.0"), |
| pytest.param({"wght": -0.5}, [500, -400, 165, 250], id="wght=-0.5"), |
| pytest.param({"wdth": -0.3}, [500, -400, 180, 235], id="wdth=-0.3"), |
| ], |
| ) |
| def test_pin_and_drop_axis(self, varfont, location, expected): |
| instancer.instantiateCvar(varfont, location) |
| |
| assert list(varfont["cvt "].values) == expected |
| |
| # check that the pinned axis has been dropped from cvar |
| pinned_axes = location.keys() |
| assert not any( |
| axis in t.axes for t in varfont["cvar"].variations for axis in pinned_axes |
| ) |
| |
| def test_full_instance(self, varfont): |
| instancer.instantiateCvar(varfont, {"wght": -0.5, "wdth": -0.5}) |
| |
| assert list(varfont["cvt "].values) == [500, -400, 165, 225] |
| |
| assert "cvar" not in varfont |
| |
| |
| class InstantiateMVARTest(object): |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| pytest.param( |
| {"wght": 1.0}, |
| {"strs": 100, "undo": -200, "unds": 150, "xhgt": 530}, |
| id="wght=1.0", |
| ), |
| pytest.param( |
| {"wght": 0.5}, |
| {"strs": 75, "undo": -150, "unds": 100, "xhgt": 515}, |
| id="wght=0.5", |
| ), |
| pytest.param( |
| {"wght": 0.0}, |
| {"strs": 50, "undo": -100, "unds": 50, "xhgt": 500}, |
| id="wght=0.0", |
| ), |
| pytest.param( |
| {"wdth": -1.0}, |
| {"strs": 20, "undo": -100, "unds": 50, "xhgt": 500}, |
| id="wdth=-1.0", |
| ), |
| pytest.param( |
| {"wdth": -0.5}, |
| {"strs": 35, "undo": -100, "unds": 50, "xhgt": 500}, |
| id="wdth=-0.5", |
| ), |
| pytest.param( |
| {"wdth": 0.0}, |
| {"strs": 50, "undo": -100, "unds": 50, "xhgt": 500}, |
| id="wdth=0.0", |
| ), |
| ], |
| ) |
| def test_pin_and_drop_axis(self, varfont, location, expected): |
| mvar = varfont["MVAR"].table |
| # initially we have two VarData: the first contains deltas associated with 3 |
| # regions: 1 with only wght, 1 with only wdth, and 1 with both wght and wdth |
| assert len(mvar.VarStore.VarData) == 2 |
| assert mvar.VarStore.VarRegionList.RegionCount == 3 |
| assert mvar.VarStore.VarData[0].VarRegionCount == 3 |
| assert all(len(item) == 3 for item in mvar.VarStore.VarData[0].Item) |
| # The second VarData has deltas associated only with 1 region (wght only). |
| assert mvar.VarStore.VarData[1].VarRegionCount == 1 |
| assert all(len(item) == 1 for item in mvar.VarStore.VarData[1].Item) |
| |
| instancer.instantiateMVAR(varfont, location) |
| |
| for mvar_tag, expected_value in expected.items(): |
| table_tag, item_name = MVAR_ENTRIES[mvar_tag] |
| assert getattr(varfont[table_tag], item_name) == expected_value |
| |
| # check that regions and accompanying deltas have been dropped |
| num_regions_left = len(mvar.VarStore.VarRegionList.Region) |
| assert num_regions_left < 3 |
| assert mvar.VarStore.VarRegionList.RegionCount == num_regions_left |
| assert mvar.VarStore.VarData[0].VarRegionCount == num_regions_left |
| # VarData subtables have been merged |
| assert len(mvar.VarStore.VarData) == 1 |
| |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| pytest.param( |
| {"wght": 1.0, "wdth": 0.0}, |
| {"strs": 100, "undo": -200, "unds": 150}, |
| id="wght=1.0,wdth=0.0", |
| ), |
| pytest.param( |
| {"wght": 0.0, "wdth": -1.0}, |
| {"strs": 20, "undo": -100, "unds": 50}, |
| id="wght=0.0,wdth=-1.0", |
| ), |
| pytest.param( |
| {"wght": 0.5, "wdth": -0.5}, |
| {"strs": 55, "undo": -145, "unds": 95}, |
| id="wght=0.5,wdth=-0.5", |
| ), |
| pytest.param( |
| {"wght": 1.0, "wdth": -1.0}, |
| {"strs": 50, "undo": -180, "unds": 130}, |
| id="wght=0.5,wdth=-0.5", |
| ), |
| ], |
| ) |
| def test_full_instance(self, varfont, location, expected): |
| instancer.instantiateMVAR(varfont, location) |
| |
| for mvar_tag, expected_value in expected.items(): |
| table_tag, item_name = MVAR_ENTRIES[mvar_tag] |
| assert getattr(varfont[table_tag], item_name) == expected_value |
| |
| assert "MVAR" not in varfont |
| |
| |
| class InstantiateHVARTest(object): |
| # the 'expectedDeltas' below refer to the VarData item deltas for the "hyphen" |
| # glyph in the PartialInstancerTest-VF.ttx test font, that are left after |
| # partial instancing |
| @pytest.mark.parametrize( |
| "location, expectedRegions, expectedDeltas", |
| [ |
| ({"wght": -1.0}, [{"wdth": (-1.0, -1.0, 0)}], [-59]), |
| ({"wght": 0}, [{"wdth": (-1.0, -1.0, 0)}], [-48]), |
| ({"wght": 1.0}, [{"wdth": (-1.0, -1.0, 0)}], [7]), |
| ( |
| {"wdth": -1.0}, |
| [ |
| {"wght": (-1.0, -1.0, 0.0)}, |
| {"wght": (0.0, 0.61, 1.0)}, |
| {"wght": (0.61, 1.0, 1.0)}, |
| ], |
| [-11, 31, 51], |
| ), |
| ({"wdth": 0}, [{"wght": (0.61, 1.0, 1.0)}], [-4]), |
| ], |
| ) |
| def test_partial_instance(self, varfont, location, expectedRegions, expectedDeltas): |
| instancer.instantiateHVAR(varfont, location) |
| |
| assert "HVAR" in varfont |
| hvar = varfont["HVAR"].table |
| varStore = hvar.VarStore |
| |
| regions = varStore.VarRegionList.Region |
| fvarAxes = [a for a in varfont["fvar"].axes if a.axisTag not in location] |
| assert [reg.get_support(fvarAxes) for reg in regions] == expectedRegions |
| |
| assert len(varStore.VarData) == 1 |
| assert varStore.VarData[0].ItemCount == 2 |
| |
| assert hvar.AdvWidthMap is not None |
| advWithMap = hvar.AdvWidthMap.mapping |
| |
| assert advWithMap[".notdef"] == advWithMap["space"] |
| varIdx = advWithMap[".notdef"] |
| # these glyphs have no metrics variations in the test font |
| assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == ( |
| [0] * varStore.VarData[0].VarRegionCount |
| ) |
| |
| varIdx = advWithMap["hyphen"] |
| assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == expectedDeltas |
| |
| def test_full_instance(self, varfont): |
| instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0}) |
| |
| assert "HVAR" not in varfont |
| |
| |
| class InstantiateItemVariationStoreTest(object): |
| def test_VarRegion_get_support(self): |
| axisOrder = ["wght", "wdth", "opsz"] |
| regionAxes = {"wdth": (-1.0, -1.0, 0.0), "wght": (0.0, 1.0, 1.0)} |
| region = builder.buildVarRegion(regionAxes, axisOrder) |
| |
| assert len(region.VarRegionAxis) == 3 |
| assert region.VarRegionAxis[2].PeakCoord == 0 |
| |
| fvarAxes = [SimpleNamespace(axisTag=axisTag) for axisTag in axisOrder] |
| |
| assert region.get_support(fvarAxes) == regionAxes |
| |
| @pytest.fixture |
| def varStore(self): |
| return builder.buildVarStore( |
| builder.buildVarRegionList( |
| [ |
| {"wght": (-1.0, -1.0, 0)}, |
| {"wght": (0, 0.5, 1.0)}, |
| {"wght": (0.5, 1.0, 1.0)}, |
| {"wdth": (-1.0, -1.0, 0)}, |
| {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, |
| {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, |
| {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, |
| ], |
| ["wght", "wdth"], |
| ), |
| [ |
| builder.buildVarData([0, 1, 2], [[100, 100, 100], [100, 100, 100]]), |
| builder.buildVarData( |
| [3, 4, 5, 6], [[100, 100, 100, 100], [100, 100, 100, 100]] |
| ), |
| ], |
| ) |
| |
| @pytest.mark.parametrize( |
| "location, expected_deltas, num_regions", |
| [ |
| ({"wght": 0}, [[0, 0], [0, 0]], 1), |
| ({"wght": 0.25}, [[50, 50], [0, 0]], 1), |
| ({"wdth": 0}, [[0, 0], [0, 0]], 3), |
| ({"wdth": -0.75}, [[0, 0], [75, 75]], 3), |
| ({"wght": 0, "wdth": 0}, [[0, 0], [0, 0]], 0), |
| ({"wght": 0.25, "wdth": 0}, [[50, 50], [0, 0]], 0), |
| ({"wght": 0, "wdth": -0.75}, [[0, 0], [75, 75]], 0), |
| ], |
| ) |
| def test_instantiate_default_deltas( |
| self, varStore, fvarAxes, location, expected_deltas, num_regions |
| ): |
| defaultDeltas = instancer.instantiateItemVariationStore( |
| varStore, fvarAxes, location |
| ) |
| |
| defaultDeltaArray = [] |
| for varidx, delta in sorted(defaultDeltas.items()): |
| major, minor = varidx >> 16, varidx & 0xFFFF |
| if major == len(defaultDeltaArray): |
| defaultDeltaArray.append([]) |
| assert len(defaultDeltaArray[major]) == minor |
| defaultDeltaArray[major].append(delta) |
| |
| assert defaultDeltaArray == expected_deltas |
| assert varStore.VarRegionList.RegionCount == num_regions |
| |
| |
| class TupleVarStoreAdapterTest(object): |
| def test_instantiate(self): |
| regions = [ |
| {"wght": (-1.0, -1.0, 0)}, |
| {"wght": (0.0, 1.0, 1.0)}, |
| {"wdth": (-1.0, -1.0, 0)}, |
| {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, |
| {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, |
| ] |
| axisOrder = ["wght", "wdth"] |
| tupleVarData = [ |
| [ |
| TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]), |
| TupleVariation({"wght": (0.0, 1.0, 1.0)}, [30, 90]), |
| TupleVariation( |
| {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100] |
| ), |
| TupleVariation( |
| {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120] |
| ), |
| ], |
| [ |
| TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]), |
| TupleVariation( |
| {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55] |
| ), |
| TupleVariation( |
| {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75] |
| ), |
| ], |
| ] |
| adapter = instancer._TupleVarStoreAdapter( |
| regions, axisOrder, tupleVarData, itemCounts=[2, 2] |
| ) |
| |
| defaultDeltaArray = adapter.instantiate({"wght": 0.5}) |
| |
| assert defaultDeltaArray == [[15, 45], [0, 0]] |
| assert adapter.regions == [{"wdth": (-1.0, -1.0, 0)}] |
| assert adapter.tupleVarData == [ |
| [TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-30, -60])], |
| [TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-12, 8])], |
| ] |
| |
| def test_dropAxes(self): |
| regions = [ |
| {"wght": (-1.0, -1.0, 0)}, |
| {"wght": (0.0, 1.0, 1.0)}, |
| {"wdth": (-1.0, -1.0, 0)}, |
| {"opsz": (0.0, 1.0, 1.0)}, |
| {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, |
| {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, |
| {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, |
| ] |
| axisOrder = ["wght", "wdth", "opsz"] |
| adapter = instancer._TupleVarStoreAdapter(regions, axisOrder, [], itemCounts=[]) |
| |
| adapter.dropAxes({"wdth"}) |
| |
| assert adapter.regions == [ |
| {"wght": (-1.0, -1.0, 0)}, |
| {"wght": (0.0, 1.0, 1.0)}, |
| {"opsz": (0.0, 1.0, 1.0)}, |
| {"wght": (0.0, 0.5, 1.0)}, |
| {"wght": (0.5, 1.0, 1.0)}, |
| ] |
| |
| adapter.dropAxes({"wght", "opsz"}) |
| |
| assert adapter.regions == [] |
| |
| def test_roundtrip(self, fvarAxes): |
| regions = [ |
| {"wght": (-1.0, -1.0, 0)}, |
| {"wght": (0, 0.5, 1.0)}, |
| {"wght": (0.5, 1.0, 1.0)}, |
| {"wdth": (-1.0, -1.0, 0)}, |
| {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, |
| {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, |
| {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, |
| ] |
| axisOrder = [axis.axisTag for axis in fvarAxes] |
| |
| itemVarStore = builder.buildVarStore( |
| builder.buildVarRegionList(regions, axisOrder), |
| [ |
| builder.buildVarData( |
| [0, 1, 2, 4, 5, 6], |
| [[10, -20, 30, -40, 50, -60], [70, -80, 90, -100, 110, -120]], |
| ), |
| builder.buildVarData( |
| [3, 4, 5, 6], [[5, -15, 25, -35], [45, -55, 65, -75]] |
| ), |
| ], |
| ) |
| |
| adapter = instancer._TupleVarStoreAdapter.fromItemVarStore( |
| itemVarStore, fvarAxes |
| ) |
| |
| assert adapter.tupleVarData == [ |
| [ |
| TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]), |
| TupleVariation({"wght": (0, 0.5, 1.0)}, [-20, -80]), |
| TupleVariation({"wght": (0.5, 1.0, 1.0)}, [30, 90]), |
| TupleVariation( |
| {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100] |
| ), |
| TupleVariation( |
| {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [50, 110] |
| ), |
| TupleVariation( |
| {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120] |
| ), |
| ], |
| [ |
| TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]), |
| TupleVariation( |
| {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55] |
| ), |
| TupleVariation( |
| {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [25, 65] |
| ), |
| TupleVariation( |
| {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75] |
| ), |
| ], |
| ] |
| assert adapter.itemCounts == [data.ItemCount for data in itemVarStore.VarData] |
| assert adapter.regions == regions |
| assert adapter.axisOrder == axisOrder |
| |
| itemVarStore2 = adapter.asItemVarStore() |
| |
| assert [ |
| reg.get_support(fvarAxes) for reg in itemVarStore2.VarRegionList.Region |
| ] == regions |
| |
| assert itemVarStore2.VarDataCount == 2 |
| assert itemVarStore2.VarData[0].VarRegionIndex == [0, 1, 2, 4, 5, 6] |
| assert itemVarStore2.VarData[0].Item == [ |
| [10, -20, 30, -40, 50, -60], |
| [70, -80, 90, -100, 110, -120], |
| ] |
| assert itemVarStore2.VarData[1].VarRegionIndex == [3, 4, 5, 6] |
| assert itemVarStore2.VarData[1].Item == [[5, -15, 25, -35], [45, -55, 65, -75]] |
| |
| |
| def makeTTFont(glyphOrder, features): |
| font = ttLib.TTFont() |
| font.setGlyphOrder(glyphOrder) |
| addOpenTypeFeaturesFromString(font, features) |
| font["name"] = ttLib.newTable("name") |
| return font |
| |
| |
| def _makeDSAxesDict(axes): |
| dsAxes = collections.OrderedDict() |
| for axisTag, axisValues in axes: |
| axis = designspaceLib.AxisDescriptor() |
| axis.name = axis.tag = axis.labelNames["en"] = axisTag |
| axis.minimum, axis.default, axis.maximum = axisValues |
| dsAxes[axis.tag] = axis |
| return dsAxes |
| |
| |
| def makeVariableFont(masters, baseIndex, axes, masterLocations): |
| vf = deepcopy(masters[baseIndex]) |
| dsAxes = _makeDSAxesDict(axes) |
| fvar = varLib._add_fvar(vf, dsAxes, instances=()) |
| axisTags = [axis.axisTag for axis in fvar.axes] |
| normalizedLocs = [models.normalizeLocation(m, dict(axes)) for m in masterLocations] |
| model = models.VariationModel(normalizedLocs, axisOrder=axisTags) |
| varLib._merge_OTL(vf, model, masters, axisTags) |
| return vf |
| |
| |
| def makeParametrizedVF(glyphOrder, features, values, increments): |
| # Create a test VF with given glyphs and parametrized OTL features. |
| # The VF is built from 9 masters (3 x 3 along wght and wdth), with |
| # locations hard-coded and base master at wght=400 and wdth=100. |
| # 'values' is a list of initial values that are interpolated in the |
| # 'features' string, and incremented for each subsequent master by the |
| # given 'increments' (list of 2-tuple) along the two axes. |
| assert values and len(values) == len(increments) |
| assert all(len(i) == 2 for i in increments) |
| masterLocations = [ |
| {"wght": 100, "wdth": 50}, |
| {"wght": 100, "wdth": 100}, |
| {"wght": 100, "wdth": 150}, |
| {"wght": 400, "wdth": 50}, |
| {"wght": 400, "wdth": 100}, # base master |
| {"wght": 400, "wdth": 150}, |
| {"wght": 700, "wdth": 50}, |
| {"wght": 700, "wdth": 100}, |
| {"wght": 700, "wdth": 150}, |
| ] |
| n = len(values) |
| values = list(values) |
| masters = [] |
| for _ in range(3): |
| for _ in range(3): |
| master = makeTTFont(glyphOrder, features=features % tuple(values)) |
| masters.append(master) |
| for i in range(n): |
| values[i] += increments[i][1] |
| for i in range(n): |
| values[i] += increments[i][0] |
| baseIndex = 4 |
| axes = [("wght", (100, 400, 700)), ("wdth", (50, 100, 150))] |
| vf = makeVariableFont(masters, baseIndex, axes, masterLocations) |
| return vf |
| |
| |
| @pytest.fixture |
| def varfontGDEF(): |
| glyphOrder = [".notdef", "f", "i", "f_i"] |
| features = ( |
| "feature liga { sub f i by f_i;} liga;" |
| "table GDEF { LigatureCaretByPos f_i %d; } GDEF;" |
| ) |
| values = [100] |
| increments = [(+30, +10)] |
| return makeParametrizedVF(glyphOrder, features, values, increments) |
| |
| |
| @pytest.fixture |
| def varfontGPOS(): |
| glyphOrder = [".notdef", "V", "A"] |
| features = "feature kern { pos V A %d; } kern;" |
| values = [-80] |
| increments = [(-10, -5)] |
| return makeParametrizedVF(glyphOrder, features, values, increments) |
| |
| |
| @pytest.fixture |
| def varfontGPOS2(): |
| glyphOrder = [".notdef", "V", "A", "acutecomb"] |
| features = ( |
| "markClass [acutecomb] <anchor 150 -10> @TOP_MARKS;" |
| "feature mark {" |
| " pos base A <anchor %d 450> mark @TOP_MARKS;" |
| "} mark;" |
| "feature kern {" |
| " pos V A %d;" |
| "} kern;" |
| ) |
| values = [200, -80] |
| increments = [(+30, +10), (-10, -5)] |
| return makeParametrizedVF(glyphOrder, features, values, increments) |
| |
| |
| class InstantiateOTLTest(object): |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| ({"wght": -1.0}, 110), # -60 |
| ({"wght": 0}, 170), |
| ({"wght": 0.5}, 200), # +30 |
| ({"wght": 1.0}, 230), # +60 |
| ({"wdth": -1.0}, 160), # -10 |
| ({"wdth": -0.3}, 167), # -3 |
| ({"wdth": 0}, 170), |
| ({"wdth": 1.0}, 180), # +10 |
| ], |
| ) |
| def test_pin_and_drop_axis_GDEF(self, varfontGDEF, location, expected): |
| vf = varfontGDEF |
| assert "GDEF" in vf |
| |
| instancer.instantiateOTL(vf, location) |
| |
| assert "GDEF" in vf |
| gdef = vf["GDEF"].table |
| assert gdef.Version == 0x00010003 |
| assert gdef.VarStore |
| assert gdef.LigCaretList |
| caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0] |
| assert caretValue.Format == 3 |
| assert hasattr(caretValue, "DeviceTable") |
| assert caretValue.DeviceTable.DeltaFormat == 0x8000 |
| assert caretValue.Coordinate == expected |
| |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| ({"wght": -1.0, "wdth": -1.0}, 100), # -60 - 10 |
| ({"wght": -1.0, "wdth": 0.0}, 110), # -60 |
| ({"wght": -1.0, "wdth": 1.0}, 120), # -60 + 10 |
| ({"wght": 0.0, "wdth": -1.0}, 160), # -10 |
| ({"wght": 0.0, "wdth": 0.0}, 170), |
| ({"wght": 0.0, "wdth": 1.0}, 180), # +10 |
| ({"wght": 1.0, "wdth": -1.0}, 220), # +60 - 10 |
| ({"wght": 1.0, "wdth": 0.0}, 230), # +60 |
| ({"wght": 1.0, "wdth": 1.0}, 240), # +60 + 10 |
| ], |
| ) |
| def test_full_instance_GDEF(self, varfontGDEF, location, expected): |
| vf = varfontGDEF |
| assert "GDEF" in vf |
| |
| instancer.instantiateOTL(vf, location) |
| |
| assert "GDEF" in vf |
| gdef = vf["GDEF"].table |
| assert gdef.Version == 0x00010000 |
| assert not hasattr(gdef, "VarStore") |
| assert gdef.LigCaretList |
| caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0] |
| assert caretValue.Format == 1 |
| assert not hasattr(caretValue, "DeviceTable") |
| assert caretValue.Coordinate == expected |
| |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| ({"wght": -1.0}, -85), # +25 |
| ({"wght": 0}, -110), |
| ({"wght": 1.0}, -135), # -25 |
| ({"wdth": -1.0}, -105), # +5 |
| ({"wdth": 0}, -110), |
| ({"wdth": 1.0}, -115), # -5 |
| ], |
| ) |
| def test_pin_and_drop_axis_GPOS_kern(self, varfontGPOS, location, expected): |
| vf = varfontGPOS |
| assert "GDEF" in vf |
| assert "GPOS" in vf |
| |
| instancer.instantiateOTL(vf, location) |
| |
| gdef = vf["GDEF"].table |
| gpos = vf["GPOS"].table |
| assert gdef.Version == 0x00010003 |
| assert gdef.VarStore |
| |
| assert gpos.LookupList.Lookup[0].LookupType == 2 # PairPos |
| pairPos = gpos.LookupList.Lookup[0].SubTable[0] |
| valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 |
| assert valueRec1.XAdvDevice |
| assert valueRec1.XAdvDevice.DeltaFormat == 0x8000 |
| assert valueRec1.XAdvance == expected |
| |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| ({"wght": -1.0, "wdth": -1.0}, -80), # +25 + 5 |
| ({"wght": -1.0, "wdth": 0.0}, -85), # +25 |
| ({"wght": -1.0, "wdth": 1.0}, -90), # +25 - 5 |
| ({"wght": 0.0, "wdth": -1.0}, -105), # +5 |
| ({"wght": 0.0, "wdth": 0.0}, -110), |
| ({"wght": 0.0, "wdth": 1.0}, -115), # -5 |
| ({"wght": 1.0, "wdth": -1.0}, -130), # -25 + 5 |
| ({"wght": 1.0, "wdth": 0.0}, -135), # -25 |
| ({"wght": 1.0, "wdth": 1.0}, -140), # -25 - 5 |
| ], |
| ) |
| def test_full_instance_GPOS_kern(self, varfontGPOS, location, expected): |
| vf = varfontGPOS |
| assert "GDEF" in vf |
| assert "GPOS" in vf |
| |
| instancer.instantiateOTL(vf, location) |
| |
| assert "GDEF" not in vf |
| gpos = vf["GPOS"].table |
| |
| assert gpos.LookupList.Lookup[0].LookupType == 2 # PairPos |
| pairPos = gpos.LookupList.Lookup[0].SubTable[0] |
| valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 |
| assert not hasattr(valueRec1, "XAdvDevice") |
| assert valueRec1.XAdvance == expected |
| |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| ({"wght": -1.0}, (210, -85)), # -60, +25 |
| ({"wght": 0}, (270, -110)), |
| ({"wght": 0.5}, (300, -122)), # +30, -12 |
| ({"wght": 1.0}, (330, -135)), # +60, -25 |
| ({"wdth": -1.0}, (260, -105)), # -10, +5 |
| ({"wdth": -0.3}, (267, -108)), # -3, +2 |
| ({"wdth": 0}, (270, -110)), |
| ({"wdth": 1.0}, (280, -115)), # +10, -5 |
| ], |
| ) |
| def test_pin_and_drop_axis_GPOS_mark_and_kern( |
| self, varfontGPOS2, location, expected |
| ): |
| vf = varfontGPOS2 |
| assert "GDEF" in vf |
| assert "GPOS" in vf |
| |
| instancer.instantiateOTL(vf, location) |
| |
| v1, v2 = expected |
| gdef = vf["GDEF"].table |
| gpos = vf["GPOS"].table |
| assert gdef.Version == 0x00010003 |
| assert gdef.VarStore |
| assert gdef.GlyphClassDef |
| |
| assert gpos.LookupList.Lookup[0].LookupType == 4 # MarkBasePos |
| markBasePos = gpos.LookupList.Lookup[0].SubTable[0] |
| baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0] |
| assert baseAnchor.Format == 3 |
| assert baseAnchor.XDeviceTable |
| assert baseAnchor.XDeviceTable.DeltaFormat == 0x8000 |
| assert not baseAnchor.YDeviceTable |
| assert baseAnchor.XCoordinate == v1 |
| assert baseAnchor.YCoordinate == 450 |
| |
| assert gpos.LookupList.Lookup[1].LookupType == 2 # PairPos |
| pairPos = gpos.LookupList.Lookup[1].SubTable[0] |
| valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 |
| assert valueRec1.XAdvDevice |
| assert valueRec1.XAdvDevice.DeltaFormat == 0x8000 |
| assert valueRec1.XAdvance == v2 |
| |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| ({"wght": -1.0, "wdth": -1.0}, (200, -80)), # -60 - 10, +25 + 5 |
| ({"wght": -1.0, "wdth": 0.0}, (210, -85)), # -60, +25 |
| ({"wght": -1.0, "wdth": 1.0}, (220, -90)), # -60 + 10, +25 - 5 |
| ({"wght": 0.0, "wdth": -1.0}, (260, -105)), # -10, +5 |
| ({"wght": 0.0, "wdth": 0.0}, (270, -110)), |
| ({"wght": 0.0, "wdth": 1.0}, (280, -115)), # +10, -5 |
| ({"wght": 1.0, "wdth": -1.0}, (320, -130)), # +60 - 10, -25 + 5 |
| ({"wght": 1.0, "wdth": 0.0}, (330, -135)), # +60, -25 |
| ({"wght": 1.0, "wdth": 1.0}, (340, -140)), # +60 + 10, -25 - 5 |
| ], |
| ) |
| def test_full_instance_GPOS_mark_and_kern(self, varfontGPOS2, location, expected): |
| vf = varfontGPOS2 |
| assert "GDEF" in vf |
| assert "GPOS" in vf |
| |
| instancer.instantiateOTL(vf, location) |
| |
| v1, v2 = expected |
| gdef = vf["GDEF"].table |
| gpos = vf["GPOS"].table |
| assert gdef.Version == 0x00010000 |
| assert not hasattr(gdef, "VarStore") |
| assert gdef.GlyphClassDef |
| |
| assert gpos.LookupList.Lookup[0].LookupType == 4 # MarkBasePos |
| markBasePos = gpos.LookupList.Lookup[0].SubTable[0] |
| baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0] |
| assert baseAnchor.Format == 1 |
| assert not hasattr(baseAnchor, "XDeviceTable") |
| assert not hasattr(baseAnchor, "YDeviceTable") |
| assert baseAnchor.XCoordinate == v1 |
| assert baseAnchor.YCoordinate == 450 |
| |
| assert gpos.LookupList.Lookup[1].LookupType == 2 # PairPos |
| pairPos = gpos.LookupList.Lookup[1].SubTable[0] |
| valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 |
| assert not hasattr(valueRec1, "XAdvDevice") |
| assert valueRec1.XAdvance == v2 |
| |
| |
| class InstantiateAvarTest(object): |
| @pytest.mark.parametrize("location", [{"wght": 0.0}, {"wdth": 0.0}]) |
| def test_pin_and_drop_axis(self, varfont, location): |
| instancer.instantiateAvar(varfont, location) |
| |
| assert set(varfont["avar"].segments).isdisjoint(location) |
| |
| def test_full_instance(self, varfont): |
| instancer.instantiateAvar(varfont, {"wght": 0.0, "wdth": 0.0}) |
| |
| assert "avar" not in varfont |
| |
| |
| class InstantiateFvarTest(object): |
| @pytest.mark.parametrize( |
| "location, instancesLeft", |
| [ |
| ( |
| {"wght": 400.0}, |
| ["Regular", "SemiCondensed", "Condensed", "ExtraCondensed"], |
| ), |
| ( |
| {"wght": 100.0}, |
| ["Thin", "SemiCondensed Thin", "Condensed Thin", "ExtraCondensed Thin"], |
| ), |
| ( |
| {"wdth": 100.0}, |
| [ |
| "Thin", |
| "ExtraLight", |
| "Light", |
| "Regular", |
| "Medium", |
| "SemiBold", |
| "Bold", |
| "ExtraBold", |
| "Black", |
| ], |
| ), |
| # no named instance at pinned location |
| ({"wdth": 90.0}, []), |
| ], |
| ) |
| def test_pin_and_drop_axis(self, varfont, location, instancesLeft): |
| instancer.instantiateFvar(varfont, location) |
| |
| fvar = varfont["fvar"] |
| assert {a.axisTag for a in fvar.axes}.isdisjoint(location) |
| |
| for instance in fvar.instances: |
| assert set(instance.coordinates).isdisjoint(location) |
| |
| name = varfont["name"] |
| assert [ |
| name.getDebugName(instance.subfamilyNameID) for instance in fvar.instances |
| ] == instancesLeft |
| |
| def test_full_instance(self, varfont): |
| instancer.instantiateFvar(varfont, {"wght": 0.0, "wdth": 0.0}) |
| |
| assert "fvar" not in varfont |
| |
| |
| class InstantiateSTATTest(object): |
| @pytest.mark.parametrize( |
| "location, expected", |
| [ |
| ({"wght": 400}, ["Condensed", "Upright"]), |
| ({"wdth": 100}, ["Thin", "Regular", "Black", "Upright"]), |
| ], |
| ) |
| def test_pin_and_drop_axis(self, varfont, location, expected): |
| instancer.instantiateSTAT(varfont, location) |
| |
| stat = varfont["STAT"].table |
| designAxes = {a.AxisTag for a in stat.DesignAxisRecord.Axis} |
| |
| assert designAxes == {"wght", "wdth", "ital"}.difference(location) |
| |
| name = varfont["name"] |
| valueNames = [] |
| for axisValueTable in stat.AxisValueArray.AxisValue: |
| valueName = name.getDebugName(axisValueTable.ValueNameID) |
| valueNames.append(valueName) |
| |
| assert valueNames == expected |
| |
| def test_skip_empty_table(self, varfont): |
| stat = otTables.STAT() |
| stat.Version = 0x00010001 |
| stat.populateDefaults() |
| assert not stat.DesignAxisRecord |
| assert not stat.AxisValueArray |
| varfont["STAT"].table = stat |
| |
| instancer.instantiateSTAT(varfont, {"wght": 100}) |
| |
| assert not varfont["STAT"].table.DesignAxisRecord |
| |
| def test_drop_table(self, varfont): |
| stat = otTables.STAT() |
| stat.Version = 0x00010001 |
| stat.populateDefaults() |
| stat.DesignAxisRecord = otTables.AxisRecordArray() |
| axis = otTables.AxisRecord() |
| axis.AxisTag = "wght" |
| axis.AxisNameID = 0 |
| axis.AxisOrdering = 0 |
| stat.DesignAxisRecord.Axis = [axis] |
| varfont["STAT"].table = stat |
| |
| instancer.instantiateSTAT(varfont, {"wght": 100}) |
| |
| assert "STAT" not in varfont |
| |
| |
| def test_pruningUnusedNames(varfont): |
| varNameIDs = instancer.getVariationNameIDs(varfont) |
| |
| assert varNameIDs == set(range(256, 296 + 1)) |
| |
| fvar = varfont["fvar"] |
| stat = varfont["STAT"].table |
| |
| with instancer.pruningUnusedNames(varfont): |
| del fvar.axes[0] # Weight (nameID=256) |
| del fvar.instances[0] # Thin (nameID=258) |
| del stat.DesignAxisRecord.Axis[0] # Weight (nameID=256) |
| del stat.AxisValueArray.AxisValue[0] # Thin (nameID=258) |
| |
| assert not any(n for n in varfont["name"].names if n.nameID in {256, 258}) |
| |
| with instancer.pruningUnusedNames(varfont): |
| del varfont["fvar"] |
| del varfont["STAT"] |
| |
| assert not any(n for n in varfont["name"].names if n.nameID in varNameIDs) |
| assert "ltag" not in varfont |
| |
| |
| def test_setMacOverlapFlags(): |
| flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND |
| flagOverlapSimple = _g_l_y_f.flagOverlapSimple |
| |
| glyf = ttLib.newTable("glyf") |
| glyf.glyphOrder = ["a", "b", "c"] |
| a = _g_l_y_f.Glyph() |
| a.numberOfContours = 1 |
| a.flags = [0] |
| b = _g_l_y_f.Glyph() |
| b.numberOfContours = -1 |
| comp = _g_l_y_f.GlyphComponent() |
| comp.flags = 0 |
| b.components = [comp] |
| c = _g_l_y_f.Glyph() |
| c.numberOfContours = 0 |
| glyf.glyphs = {"a": a, "b": b, "c": c} |
| |
| instancer.setMacOverlapFlags(glyf) |
| |
| assert a.flags[0] & flagOverlapSimple != 0 |
| assert b.components[0].flags & flagOverlapCompound != 0 |
| |
| |
| def _strip_ttLibVersion(string): |
| return re.sub(' ttLibVersion=".*"', "", string) |
| |
| |
| @pytest.fixture |
| def varfont2(): |
| f = ttLib.TTFont(recalcTimestamp=False) |
| f.importXML(os.path.join(TESTDATA, "PartialInstancerTest2-VF.ttx")) |
| return f |
| |
| |
| def _dump_ttx(ttFont): |
| # compile to temporary bytes stream, reload and dump to XML |
| tmp = BytesIO() |
| ttFont.save(tmp) |
| tmp.seek(0) |
| ttFont2 = ttLib.TTFont(tmp, recalcBBoxes=False, recalcTimestamp=False) |
| s = StringIO() |
| ttFont2.saveXML(s, newlinestr="\n") |
| return _strip_ttLibVersion(s.getvalue()) |
| |
| |
| def _get_expected_instance_ttx(wght, wdth): |
| with open( |
| os.path.join( |
| TESTDATA, |
| "test_results", |
| "PartialInstancerTest2-VF-instance-{0},{1}.ttx".format(wght, wdth), |
| ), |
| "r", |
| encoding="utf-8", |
| ) as fp: |
| return _strip_ttLibVersion(fp.read()) |
| |
| |
| class InstantiateVariableFontTest(object): |
| @pytest.mark.parametrize( |
| "wght, wdth", |
| [(100, 100), (400, 100), (900, 100), (100, 62.5), (400, 62.5), (900, 62.5)], |
| ) |
| def test_multiple_instancing(self, varfont2, wght, wdth): |
| partial = instancer.instantiateVariableFont(varfont2, {"wght": wght}) |
| instance = instancer.instantiateVariableFont(partial, {"wdth": wdth}) |
| |
| expected = _get_expected_instance_ttx(wght, wdth) |
| |
| assert _dump_ttx(instance) == expected |
| |
| def test_default_instance(self, varfont2): |
| instance = instancer.instantiateVariableFont( |
| varfont2, {"wght": None, "wdth": None} |
| ) |
| |
| expected = _get_expected_instance_ttx(400, 100) |
| |
| assert _dump_ttx(instance) == expected |
| |
| |
| def _conditionSetAsDict(conditionSet, axisOrder): |
| result = {} |
| for cond in conditionSet.ConditionTable: |
| assert cond.Format == 1 |
| axisTag = axisOrder[cond.AxisIndex] |
| result[axisTag] = (cond.FilterRangeMinValue, cond.FilterRangeMaxValue) |
| return result |
| |
| |
| def _getSubstitutions(gsub, lookupIndices): |
| subs = {} |
| for index, lookup in enumerate(gsub.LookupList.Lookup): |
| if index in lookupIndices: |
| for subtable in lookup.SubTable: |
| subs.update(subtable.mapping) |
| return subs |
| |
| |
| def makeFeatureVarsFont(conditionalSubstitutions): |
| axes = set() |
| glyphs = set() |
| for region, substitutions in conditionalSubstitutions: |
| for box in region: |
| axes.update(box.keys()) |
| glyphs.update(*substitutions.items()) |
| |
| varfont = ttLib.TTFont() |
| varfont.setGlyphOrder(sorted(glyphs)) |
| |
| fvar = varfont["fvar"] = ttLib.newTable("fvar") |
| fvar.axes = [] |
| for axisTag in sorted(axes): |
| axis = _f_v_a_r.Axis() |
| axis.axisTag = Tag(axisTag) |
| fvar.axes.append(axis) |
| |
| featureVars.addFeatureVariations(varfont, conditionalSubstitutions) |
| |
| return varfont |
| |
| |
| class InstantiateFeatureVariationsTest(object): |
| @pytest.mark.parametrize( |
| "location, appliedSubs, expectedRecords", |
| [ |
| ({"wght": 0}, {}, [({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"})]), |
| ( |
| {"wght": -1.0}, |
| {}, |
| [ |
| ({"cntr": (0, 0.25)}, {"uni0061": "uni0041"}), |
| ({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"}), |
| ], |
| ), |
| ( |
| {"wght": 1.0}, |
| {"uni0024": "uni0024.nostroke"}, |
| [ |
| ( |
| {"cntr": (0.75, 1.0)}, |
| {"uni0024": "uni0024.nostroke", "uni0041": "uni0061"}, |
| ) |
| ], |
| ), |
| ( |
| {"cntr": 0}, |
| {}, |
| [ |
| ({"wght": (-1.0, -0.45654)}, {"uni0061": "uni0041"}), |
| ({"wght": (0.20886, 1.0)}, {"uni0024": "uni0024.nostroke"}), |
| ], |
| ), |
| ( |
| {"cntr": 1.0}, |
| {"uni0041": "uni0061"}, |
| [ |
| ( |
| {"wght": (0.20886, 1.0)}, |
| {"uni0024": "uni0024.nostroke", "uni0041": "uni0061"}, |
| ) |
| ], |
| ), |
| ], |
| ) |
| def test_partial_instance(self, location, appliedSubs, expectedRecords): |
| font = makeFeatureVarsFont( |
| [ |
| ([{"wght": (0.20886, 1.0)}], {"uni0024": "uni0024.nostroke"}), |
| ([{"cntr": (0.75, 1.0)}], {"uni0041": "uni0061"}), |
| ( |
| [{"wght": (-1.0, -0.45654), "cntr": (0, 0.25)}], |
| {"uni0061": "uni0041"}, |
| ), |
| ] |
| ) |
| |
| instancer.instantiateFeatureVariations(font, location) |
| |
| gsub = font["GSUB"].table |
| featureVariations = gsub.FeatureVariations |
| |
| assert featureVariations.FeatureVariationCount == len(expectedRecords) |
| |
| axisOrder = [a.axisTag for a in font["fvar"].axes if a.axisTag not in location] |
| for i, (expectedConditionSet, expectedSubs) in enumerate(expectedRecords): |
| rec = featureVariations.FeatureVariationRecord[i] |
| conditionSet = _conditionSetAsDict(rec.ConditionSet, axisOrder) |
| |
| assert conditionSet == expectedConditionSet |
| |
| subsRecord = rec.FeatureTableSubstitution.SubstitutionRecord[0] |
| lookupIndices = subsRecord.Feature.LookupListIndex |
| substitutions = _getSubstitutions(gsub, lookupIndices) |
| |
| assert substitutions == expectedSubs |
| |
| appliedLookupIndices = gsub.FeatureList.FeatureRecord[0].Feature.LookupListIndex |
| |
| assert _getSubstitutions(gsub, appliedLookupIndices) == appliedSubs |
| |
| @pytest.mark.parametrize( |
| "location, appliedSubs", |
| [ |
| ({"wght": 0, "cntr": 0}, None), |
| ({"wght": -1.0, "cntr": 0}, {"uni0061": "uni0041"}), |
| ({"wght": 1.0, "cntr": 0}, {"uni0024": "uni0024.nostroke"}), |
| ({"wght": 0.0, "cntr": 1.0}, {"uni0041": "uni0061"}), |
| ( |
| {"wght": 1.0, "cntr": 1.0}, |
| {"uni0041": "uni0061", "uni0024": "uni0024.nostroke"}, |
| ), |
| ({"wght": -1.0, "cntr": 0.3}, None), |
| ], |
| ) |
| def test_full_instance(self, location, appliedSubs): |
| font = makeFeatureVarsFont( |
| [ |
| ([{"wght": (0.20886, 1.0)}], {"uni0024": "uni0024.nostroke"}), |
| ([{"cntr": (0.75, 1.0)}], {"uni0041": "uni0061"}), |
| ( |
| [{"wght": (-1.0, -0.45654), "cntr": (0, 0.25)}], |
| {"uni0061": "uni0041"}, |
| ), |
| ] |
| ) |
| |
| instancer.instantiateFeatureVariations(font, location) |
| |
| gsub = font["GSUB"].table |
| assert not hasattr(gsub, "FeatureVariations") |
| |
| if appliedSubs: |
| lookupIndices = gsub.FeatureList.FeatureRecord[0].Feature.LookupListIndex |
| assert _getSubstitutions(gsub, lookupIndices) == appliedSubs |
| else: |
| assert not gsub.FeatureList.FeatureRecord |
| |
| def test_unsupported_condition_format(self, caplog): |
| font = makeFeatureVarsFont( |
| [ |
| ( |
| [{"wdth": (-1.0, -0.5), "wght": (0.5, 1.0)}], |
| {"dollar": "dollar.nostroke"}, |
| ) |
| ] |
| ) |
| featureVariations = font["GSUB"].table.FeatureVariations |
| rec1 = featureVariations.FeatureVariationRecord[0] |
| assert len(rec1.ConditionSet.ConditionTable) == 2 |
| rec1.ConditionSet.ConditionTable[0].Format = 2 |
| |
| with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"): |
| instancer.instantiateFeatureVariations(font, {"wdth": 0}) |
| |
| assert ( |
| "Condition table 0 of FeatureVariationRecord 0 " |
| "has unsupported format (2); ignored" |
| ) in caplog.text |
| |
| # check that record with unsupported condition format (but whose other |
| # conditions do not reference pinned axes) is kept as is |
| featureVariations = font["GSUB"].table.FeatureVariations |
| assert featureVariations.FeatureVariationRecord[0] is rec1 |
| assert len(rec1.ConditionSet.ConditionTable) == 2 |
| assert rec1.ConditionSet.ConditionTable[0].Format == 2 |
| |
| |
| @pytest.mark.parametrize( |
| "limits, expected", |
| [ |
| (["wght=400", "wdth=100"], {"wght": 400, "wdth": 100}), |
| (["wght=400:900"], {"wght": (400, 900)}), |
| (["slnt=11.4"], {"slnt": 11.4}), |
| (["ABCD=drop"], {"ABCD": None}), |
| ], |
| ) |
| def test_parseLimits(limits, expected): |
| assert instancer.parseLimits(limits) == expected |
| |
| |
| @pytest.mark.parametrize( |
| "limits", [["abcde=123", "=0", "wght=:", "wght=1:", "wght=abcd", "wght=x:y"]] |
| ) |
| def test_parseLimits_invalid(limits): |
| with pytest.raises(ValueError, match="invalid location format"): |
| instancer.parseLimits(limits) |
| |
| |
| def test_normalizeAxisLimits_tuple(varfont): |
| normalized = instancer.normalizeAxisLimits(varfont, {"wght": (100, 400)}) |
| assert normalized == {"wght": (-1.0, 0)} |
| |
| |
| def test_normalizeAxisLimits_no_avar(varfont): |
| del varfont["avar"] |
| |
| normalized = instancer.normalizeAxisLimits(varfont, {"wght": (500, 600)}) |
| |
| assert normalized["wght"] == pytest.approx((0.2, 0.4), 1e-4) |
| |
| |
| def test_normalizeAxisLimits_missing_from_fvar(varfont): |
| with pytest.raises(ValueError, match="not present in fvar"): |
| instancer.normalizeAxisLimits(varfont, {"ZZZZ": 1000}) |
| |
| |
| def test_sanityCheckVariableTables(varfont): |
| font = ttLib.TTFont() |
| with pytest.raises(ValueError, match="Missing required table fvar"): |
| instancer.sanityCheckVariableTables(font) |
| |
| del varfont["glyf"] |
| |
| with pytest.raises(ValueError, match="Can't have gvar without glyf"): |
| instancer.sanityCheckVariableTables(varfont) |
| |
| |
| def test_main(varfont, tmpdir): |
| fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf") |
| varfont.save(fontfile) |
| args = [fontfile, "wght=400"] |
| |
| # exits without errors |
| assert instancer.main(args) is None |
| |
| |
| def test_main_exit_nonexistent_file(capsys): |
| with pytest.raises(SystemExit): |
| instancer.main([""]) |
| captured = capsys.readouterr() |
| |
| assert "No such file ''" in captured.err |
| |
| |
| def test_main_exit_invalid_location(varfont, tmpdir, capsys): |
| fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf") |
| varfont.save(fontfile) |
| |
| with pytest.raises(SystemExit): |
| instancer.main([fontfile, "wght:100"]) |
| captured = capsys.readouterr() |
| |
| assert "invalid location format" in captured.err |
| |
| |
| def test_main_exit_multiple_limits(varfont, tmpdir, capsys): |
| fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf") |
| varfont.save(fontfile) |
| |
| with pytest.raises(SystemExit): |
| instancer.main([fontfile, "wght=400", "wght=90"]) |
| captured = capsys.readouterr() |
| |
| assert "Specified multiple limits for the same axis" in captured.err |