From b8500ac97cd023aae26bc6c4673af8e21c6aa9b7 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 25 Sep 2019 17:21:50 +0100 Subject: [PATCH] instancer_test: add tests for restricting axis ranges (L3) --- Tests/varLib/instancer_test.py | 436 +++++++++++++++++++++++++++++++-- 1 file changed, 419 insertions(+), 17 deletions(-) diff --git a/Tests/varLib/instancer_test.py b/Tests/varLib/instancer_test.py index 468211569..84a1b9795 100644 --- a/Tests/varLib/instancer_test.py +++ b/Tests/varLib/instancer_test.py @@ -1,4 +1,5 @@ from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import floatToFixedToFloat from fontTools import ttLib from fontTools import designspaceLib from fontTools.feaLib.builder import addOpenTypeFeaturesFromString @@ -493,33 +494,40 @@ class TupleVarStoreAdapterTest(object): [TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-12, 8])], ] - def test_dropAxes(self): + def test_rebuildRegions(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)}, + {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, ] - axisOrder = ["wght", "wdth", "opsz"] - adapter = instancer._TupleVarStoreAdapter(regions, axisOrder, [], itemCounts=[]) + axisOrder = ["wght", "wdth"] + variations = [] + for region in regions: + variations.append(TupleVariation(region, [100])) + tupleVarData = [variations[:3], variations[3:]] + adapter = instancer._TupleVarStoreAdapter( + regions, axisOrder, tupleVarData, itemCounts=[1, 1] + ) - adapter.dropAxes({"wdth"}) + adapter.rebuildRegions() + + assert adapter.regions == regions + + del tupleVarData[0][2] + tupleVarData[1][0].axes = {"wght": (-1.0, -0.5, 0)} + tupleVarData[1][1].axes = {"wght": (0, 0.5, 1.0)} + + adapter.rebuildRegions() 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)}, + {"wght": (-1.0, -0.5, 0)}, + {"wght": (0, 0.5, 1.0)}, ] - adapter.dropAxes({"wght", "opsz"}) - - assert adapter.regions == [] - def test_roundtrip(self, fvarAxes): regions = [ {"wght": (-1.0, -1.0, 0)}, @@ -924,6 +932,208 @@ class InstantiateAvarTest(object): assert "avar" not in varfont + @staticmethod + def quantizeF2Dot14Floats(mapping): + return { + floatToFixedToFloat(k, 14): floatToFixedToFloat(v, 14) + for k, v in mapping.items() + } + + # the following values come from NotoSans-VF.ttf + DFLT_WGHT_MAPPING = { + -1.0: -1.0, + -0.6667: -0.7969, + -0.3333: -0.5, + 0: 0, + 0.2: 0.18, + 0.4: 0.38, + 0.6: 0.61, + 0.8: 0.79, + 1.0: 1.0, + } + + DFLT_WDTH_MAPPING = {-1.0: -1.0, -0.6667: -0.7, -0.3333: -0.36664, 0: 0, 1.0: 1.0} + + @pytest.fixture + def varfont(self): + fvarAxes = ("wght", (100, 400, 900)), ("wdth", (62.5, 100, 100)) + avarSegments = { + "wght": self.quantizeF2Dot14Floats(self.DFLT_WGHT_MAPPING), + "wdth": self.quantizeF2Dot14Floats(self.DFLT_WDTH_MAPPING), + } + varfont = ttLib.TTFont() + varfont["name"] = ttLib.newTable("name") + varLib._add_fvar(varfont, _makeDSAxesDict(fvarAxes), instances=()) + avar = varfont["avar"] = ttLib.newTable("avar") + avar.segments = avarSegments + return varfont + + @pytest.mark.parametrize( + "axisLimits, expectedSegments", + [ + pytest.param( + {"wght": (100, 900)}, + {"wght": DFLT_WGHT_MAPPING, "wdth": DFLT_WDTH_MAPPING}, + id="wght=100:900", + ), + pytest.param( + {"wght": (400, 900)}, + { + "wght": { + -1.0: -1.0, + 0: 0, + 0.2: 0.18, + 0.4: 0.38, + 0.6: 0.61, + 0.8: 0.79, + 1.0: 1.0, + }, + "wdth": DFLT_WDTH_MAPPING, + }, + id="wght=400:900", + ), + pytest.param( + {"wght": (100, 400)}, + { + "wght": { + -1.0: -1.0, + -0.6667: -0.7969, + -0.3333: -0.5, + 0: 0, + 1.0: 1.0, + }, + "wdth": DFLT_WDTH_MAPPING, + }, + id="wght=100:400", + ), + pytest.param( + {"wght": (400, 800)}, + { + "wght": { + -1.0: -1.0, + 0: 0, + 0.25: 0.22784, + 0.50006: 0.48103, + 0.75: 0.77214, + 1.0: 1.0, + }, + "wdth": DFLT_WDTH_MAPPING, + }, + id="wght=400:800", + ), + pytest.param( + {"wght": (400, 700)}, + { + "wght": { + -1.0: -1.0, + 0: 0, + 0.3334: 0.2951, + 0.66675: 0.623, + 1.0: 1.0, + }, + "wdth": DFLT_WDTH_MAPPING, + }, + id="wght=400:700", + ), + pytest.param( + {"wght": (400, 600)}, + { + "wght": {-1.0: -1.0, 0: 0, 0.5: 0.47363, 1.0: 1.0}, + "wdth": DFLT_WDTH_MAPPING, + }, + id="wght=400:600", + ), + pytest.param( + {"wdth": (62.5, 100)}, + { + "wght": DFLT_WGHT_MAPPING, + "wdth": { + -1.0: -1.0, + -0.6667: -0.7, + -0.3333: -0.36664, + 0: 0, + 1.0: 1.0, + }, + }, + id="wdth=62.5:100", + ), + pytest.param( + {"wdth": (70, 100)}, + { + "wght": DFLT_WGHT_MAPPING, + "wdth": { + -1.0: -1.0, + -0.8334: -0.85364, + -0.4166: -0.44714, + 0: 0, + 1.0: 1.0, + }, + }, + id="wdth=70:100", + ), + pytest.param( + {"wdth": (75, 100)}, + { + "wght": DFLT_WGHT_MAPPING, + "wdth": {-1.0: -1.0, -0.49994: -0.52374, 0: 0, 1.0: 1.0}, + }, + id="wdth=75:100", + ), + pytest.param( + {"wdth": (77, 100)}, + { + "wght": DFLT_WGHT_MAPPING, + "wdth": {-1.0: -1.0, -0.54346: -0.56696, 0: 0, 1.0: 1.0}, + }, + id="wdth=77:100", + ), + pytest.param( + {"wdth": (87.5, 100)}, + {"wght": DFLT_WGHT_MAPPING, "wdth": {-1.0: -1.0, 0: 0, 1.0: 1.0}}, + id="wdth=87.5:100", + ), + ], + ) + def test_limit_axes(self, varfont, axisLimits, expectedSegments): + instancer.instantiateAvar(varfont, axisLimits) + + newSegments = varfont["avar"].segments + expectedSegments = { + axisTag: self.quantizeF2Dot14Floats(mapping) + for axisTag, mapping in expectedSegments.items() + } + assert newSegments == expectedSegments + + @pytest.mark.parametrize( + "invalidSegmentMap", + [ + pytest.param({0.5: 0.5}, id="missing-required-maps-1"), + pytest.param({-1.0: -1.0, 1.0: 1.0}, id="missing-required-maps-2"), + pytest.param( + {-1.0: -1.0, 0: 0, 0.5: 0.5, 0.6: 0.4, 1.0: 1.0}, + id="retrograde-value-maps", + ), + ], + ) + def test_drop_invalid_segment_map(self, varfont, invalidSegmentMap, caplog): + varfont["avar"].segments["wght"] = invalidSegmentMap + + with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"): + instancer.instantiateAvar(varfont, {"wght": (100, 400)}) + + assert "Invalid avar" in caplog.text + assert "wght" not in varfont["avar"].segments + + def test_isValidAvarSegmentMap(self): + assert instancer._isValidAvarSegmentMap("FOOO", {}) + assert instancer._isValidAvarSegmentMap("FOOO", {-1.0: -1.0, 0: 0, 1.0: 1.0}) + assert instancer._isValidAvarSegmentMap( + "FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 1.0: 1.0} + ) + assert instancer._isValidAvarSegmentMap( + "FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 0.7: 0.5, 1.0: 1.0} + ) + class InstantiateFvarTest(object): @pytest.mark.parametrize( @@ -1321,12 +1531,204 @@ class InstantiateFeatureVariationsTest(object): assert rec1.ConditionSet.ConditionTable[0].Format == 2 +class LimitTupleVariationAxisRangesTest: + def check_limit_single_var_axis_range(self, var, axisTag, axisRange, expected): + result = instancer.limitTupleVariationAxisRange(var, axisTag, axisRange) + print(result) + + assert len(result) == len(expected) + for v1, v2 in zip(result, expected): + assert v1.coordinates == pytest.approx(v2.coordinates) + assert v1.axes.keys() == v2.axes.keys() + for k in v1.axes: + p, q = v1.axes[k], v2.axes[k] + assert p == pytest.approx(q) + + @pytest.mark.parametrize( + "var, axisTag, newMax, expected", + [ + ( + TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), + "wdth", + 0.5, + [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])], + ), + ( + TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), + "wght", + 0.5, + [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [50, 50])], + ), + ( + TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), + "wght", + 0.8, + [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])], + ), + ( + TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), + "wght", + 1.0, + [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])], + ), + (TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), "wght", 0.0, []), + (TupleVariation({"wght": (0.5, 1.0, 1.0)}, [100, 100]), "wght", 0.4, []), + ( + TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]), + "wght", + 0.5, + [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])], + ), + ( + TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]), + "wght", + 0.4, + [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])], + ), + ( + TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]), + "wght", + 0.6, + [TupleVariation({"wght": (0.0, 0.833334, 1.666667)}, [100, 100])], + ), + ( + TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]), + "wght", + 0.4, + [ + TupleVariation({"wght": (0.0, 0.5, 1.99994)}, [100, 100]), + TupleVariation({"wght": (0.5, 1.0, 1.0)}, [8.33333, 8.33333]), + ], + ), + ( + TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]), + "wght", + 0.5, + [TupleVariation({"wght": (0.0, 0.4, 1.99994)}, [100, 100])], + ), + ( + TupleVariation({"wght": (0.5, 0.5, 1.0)}, [100, 100]), + "wght", + 0.5, + [TupleVariation({"wght": (1.0, 1.0, 1.0)}, [100, 100])], + ), + ], + ) + def test_positive_var(self, var, axisTag, newMax, expected): + axisRange = instancer.NormalizedAxisRange(0, newMax) + self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected) + + @pytest.mark.parametrize( + "var, axisTag, newMin, expected", + [ + ( + TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), + "wdth", + -0.5, + [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])], + ), + ( + TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), + "wght", + -0.5, + [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [50, 50])], + ), + ( + TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), + "wght", + -0.8, + [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])], + ), + ( + TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), + "wght", + -1.0, + [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])], + ), + (TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), "wght", 0.0, []), + ( + TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [100, 100]), + "wght", + -0.4, + [], + ), + ( + TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]), + "wght", + -0.5, + [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])], + ), + ( + TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]), + "wght", + -0.4, + [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])], + ), + ( + TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]), + "wght", + -0.6, + [TupleVariation({"wght": (-1.666667, -0.833334, 0.0)}, [100, 100])], + ), + ( + TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]), + "wght", + -0.4, + [ + TupleVariation({"wght": (-2.0, -0.5, -0.0)}, [100, 100]), + TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [8.33333, 8.33333]), + ], + ), + ( + TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]), + "wght", + -0.5, + [TupleVariation({"wght": (-2.0, -0.4, 0.0)}, [100, 100])], + ), + ( + TupleVariation({"wght": (-1.0, -0.5, -0.5)}, [100, 100]), + "wght", + -0.5, + [TupleVariation({"wght": (-1.0, -1.0, -1.0)}, [100, 100])], + ), + ], + ) + def test_negative_var(self, var, axisTag, newMin, expected): + axisRange = instancer.NormalizedAxisRange(newMin, 0) + self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected) + + +@pytest.mark.parametrize( + "oldRange, newRange, expected", + [ + ((1.0, -1.0), (-1.0, 1.0), None), # invalid oldRange min > max + ((0.6, 1.0), (0, 0.5), None), + ((-1.0, -0.6), (-0.5, 0), None), + ((0.4, 1.0), (0, 0.5), (0.8, 1.0)), + ((-1.0, -0.4), (-0.5, 0), (-1.0, -0.8)), + ((0.4, 1.0), (0, 0.4), (1.0, 1.0)), + ((-1.0, -0.4), (-0.4, 0), (-1.0, -1.0)), + ((-0.5, 0.5), (-0.4, 0.4), (-1.0, 1.0)), + ((0, 1.0), (-1.0, 0), (0, 0)), # or None? + ((-1.0, 0), (0, 1.0), (0, 0)), # or None? + ], +) +def test_limitFeatureVariationConditionRange(oldRange, newRange, expected): + condition = featureVars.buildConditionTable(0, *oldRange) + + result = instancer._limitFeatureVariationConditionRange( + condition, instancer.NormalizedAxisRange(*newRange) + ) + + assert result == expected + + @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}), + (["slnt=11.4"], {"slnt": pytest.approx(11.399994)}), (["ABCD=drop"], {"ABCD": None}), ], ) @@ -1350,9 +1752,9 @@ def test_normalizeAxisLimits_tuple(varfont): def test_normalizeAxisLimits_no_avar(varfont): del varfont["avar"] - normalized = instancer.normalizeAxisLimits(varfont, {"wght": (500, 600)}) + normalized = instancer.normalizeAxisLimits(varfont, {"wght": (400, 500)}) - assert normalized["wght"] == pytest.approx((0.2, 0.4), 1e-4) + assert normalized["wght"] == pytest.approx((0, 0.2), 1e-4) def test_normalizeAxisLimits_missing_from_fvar(varfont):