| from collections import OrderedDict |
| from fontTools.designspaceLib import AxisDescriptor |
| from fontTools.ttLib import TTFont, newTable |
| from fontTools import varLib |
| from fontTools.varLib.featureVars import ( |
| addFeatureVariations, |
| overlayFeatureVariations, |
| overlayBox, |
| ) |
| import pytest |
| |
| |
| def makeVariableFont(glyphOrder, axes): |
| font = TTFont() |
| font.setGlyphOrder(glyphOrder) |
| font["name"] = newTable("name") |
| ds_axes = OrderedDict() |
| for axisTag, (minimum, default, maximum) in axes.items(): |
| axis = AxisDescriptor() |
| axis.name = axis.tag = axis.labelNames["en"] = axisTag |
| axis.minimum, axis.default, axis.maximum = minimum, default, maximum |
| ds_axes[axisTag] = axis |
| varLib._add_fvar(font, ds_axes, instances=()) |
| return font |
| |
| |
| @pytest.fixture |
| def varfont(): |
| return makeVariableFont( |
| [".notdef", "space", "A", "B", "A.alt", "B.alt"], |
| {"wght": (100, 400, 900)}, |
| ) |
| |
| |
| def test_addFeatureVariations(varfont): |
| assert "GSUB" not in varfont |
| |
| addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})]) |
| |
| assert "GSUB" in varfont |
| gsub = varfont["GSUB"].table |
| |
| assert len(gsub.ScriptList.ScriptRecord) == 1 |
| assert gsub.ScriptList.ScriptRecord[0].ScriptTag == "DFLT" |
| |
| assert len(gsub.FeatureList.FeatureRecord) == 1 |
| assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rvrn" |
| |
| assert len(gsub.LookupList.Lookup) == 1 |
| assert gsub.LookupList.Lookup[0].LookupType == 1 |
| assert len(gsub.LookupList.Lookup[0].SubTable) == 1 |
| assert gsub.LookupList.Lookup[0].SubTable[0].mapping == {"A": "A.alt"} |
| |
| assert gsub.FeatureVariations is not None |
| assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1 |
| fvr = gsub.FeatureVariations.FeatureVariationRecord[0] |
| assert len(fvr.ConditionSet.ConditionTable) == 1 |
| cst = fvr.ConditionSet.ConditionTable[0] |
| assert cst.AxisIndex == 0 |
| assert cst.FilterRangeMinValue == 0.5 |
| assert cst.FilterRangeMaxValue == 1.0 |
| assert len(fvr.FeatureTableSubstitution.SubstitutionRecord) == 1 |
| ftsr = fvr.FeatureTableSubstitution.SubstitutionRecord[0] |
| assert ftsr.FeatureIndex == 0 |
| assert ftsr.Feature.LookupListIndex == [0] |
| |
| |
| def _substitution_features(gsub, rec_index): |
| fea_tags = [feature.FeatureTag for feature in gsub.FeatureList.FeatureRecord] |
| fea_indices = [ |
| gsub.FeatureVariations.FeatureVariationRecord[rec_index] |
| .FeatureTableSubstitution.SubstitutionRecord[i] |
| .FeatureIndex |
| for i in range( |
| len( |
| gsub.FeatureVariations.FeatureVariationRecord[ |
| rec_index |
| ].FeatureTableSubstitution.SubstitutionRecord |
| ) |
| ) |
| ] |
| return [(i, fea_tags[i]) for i in fea_indices] |
| |
| |
| def test_addFeatureVariations_existing_variable_feature(varfont): |
| assert "GSUB" not in varfont |
| |
| addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})]) |
| |
| gsub = varfont["GSUB"].table |
| assert len(gsub.FeatureList.FeatureRecord) == 1 |
| assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rvrn" |
| assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1 |
| assert _substitution_features(gsub, rec_index=0) == [(0, "rvrn")] |
| |
| # can't add feature variations for an existing feature tag that already has some, |
| # in this case the default 'rvrn' |
| with pytest.raises( |
| varLib.VarLibError, |
| match=r"FeatureVariations already exist for feature tag\(s\): {'rvrn'}", |
| ): |
| addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})]) |
| |
| |
| def test_addFeatureVariations_new_feature(varfont): |
| assert "GSUB" not in varfont |
| |
| addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})]) |
| |
| gsub = varfont["GSUB"].table |
| assert len(gsub.FeatureList.FeatureRecord) == 1 |
| assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rvrn" |
| assert len(gsub.LookupList.Lookup) == 1 |
| assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1 |
| assert _substitution_features(gsub, rec_index=0) == [(0, "rvrn")] |
| |
| # we can add feature variations for a feature tag that does not have |
| # any feature variations yet |
| addFeatureVariations( |
| varfont, [([{"wght": (-1.0, 0.0)}], {"B": "B.alt"})], featureTag="rclt" |
| ) |
| |
| assert len(gsub.FeatureList.FeatureRecord) == 2 |
| # Note 'rclt' is now first (index=0) in the feature list sorted by tag, and |
| # 'rvrn' is second (index=1) |
| assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rclt" |
| assert gsub.FeatureList.FeatureRecord[1].FeatureTag == "rvrn" |
| assert len(gsub.LookupList.Lookup) == 2 |
| assert len(gsub.FeatureVariations.FeatureVariationRecord) == 2 |
| # The new 'rclt' feature variation record is appended to the end; |
| # the feature index for 'rvrn' feature table substitution record is now 1 |
| assert _substitution_features(gsub, rec_index=0) == [(1, "rvrn")] |
| assert _substitution_features(gsub, rec_index=1) == [(0, "rclt")] |
| |
| |
| def test_addFeatureVariations_existing_condition(varfont): |
| assert "GSUB" not in varfont |
| |
| # Add a feature variation for 'ccmp' feature tag with a condition |
| addFeatureVariations( |
| varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})], featureTag="ccmp" |
| ) |
| |
| gsub = varfont["GSUB"].table |
| |
| # Should now have one feature record, one lookup, and one feature variation record |
| assert len(gsub.FeatureList.FeatureRecord) == 1 |
| assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "ccmp" |
| assert len(gsub.LookupList.Lookup) == 1 |
| assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1 |
| assert _substitution_features(gsub, rec_index=0) == [(0, "ccmp")] |
| |
| # Add a feature variation for 'rlig' feature tag with the same condition |
| addFeatureVariations( |
| varfont, [([{"wght": (0.5, 1.0)}], {"B": "B.alt"})], featureTag="rlig" |
| ) |
| |
| # Should now have two feature records, two lookups, and one feature variation |
| # record, since the condition is the same for both feature variations |
| assert len(gsub.FeatureList.FeatureRecord) == 2 |
| assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "ccmp" |
| assert gsub.FeatureList.FeatureRecord[1].FeatureTag == "rlig" |
| assert len(gsub.LookupList.Lookup) == 2 |
| assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1 |
| assert _substitution_features(gsub, rec_index=0) == [(0, "ccmp"), (1, "rlig")] |
| |
| |
| def _test_linear(n): |
| conds = [] |
| for i in range(n): |
| end = i / n |
| start = end - 1.0 |
| region = [{"X": (start, end)}] |
| subst = {"g%.2g" % start: "g%.2g" % end} |
| conds.append((region, subst)) |
| overlaps = overlayFeatureVariations(conds) |
| assert len(overlaps) == 2 * n - 1, overlaps |
| return conds, overlaps |
| |
| |
| def test_linear(): |
| _test_linear(10) |
| |
| |
| def _test_quadratic(n): |
| conds = [] |
| for i in range(1, n + 1): |
| region = [{"X": (0, i / n), "Y": (0, (n + 1 - i) / n)}] |
| subst = {str(i): str(n + 1 - i)} |
| conds.append((region, subst)) |
| overlaps = overlayFeatureVariations(conds) |
| assert len(overlaps) == n * (n + 1) // 2, overlaps |
| return conds, overlaps |
| |
| |
| def test_quadratic(): |
| _test_quadratic(10) |
| |
| |
| def _merge_substitutions(substitutions): |
| merged = {} |
| for subst in substitutions: |
| merged.update(subst) |
| return merged |
| |
| |
| def _match_condition(location, overlaps): |
| for box, substitutions in overlaps: |
| for tag, coord in location.items(): |
| start, end = box[tag] |
| if start <= coord <= end: |
| return _merge_substitutions(substitutions) |
| return {} # no match |
| |
| |
| def test_overlaps_1(): |
| # https://github.com/fonttools/fonttools/issues/1400 |
| conds = [ |
| ([{"abcd": (4, 9)}], {0: 0}), |
| ([{"abcd": (5, 10)}], {1: 1}), |
| ([{"abcd": (0, 8)}], {2: 2}), |
| ([{"abcd": (3, 7)}], {3: 3}), |
| ] |
| overlaps = overlayFeatureVariations(conds) |
| subst = _match_condition({"abcd": 0}, overlaps) |
| assert subst == {2: 2} |
| subst = _match_condition({"abcd": 1}, overlaps) |
| assert subst == {2: 2} |
| subst = _match_condition({"abcd": 3}, overlaps) |
| assert subst == {2: 2, 3: 3} |
| subst = _match_condition({"abcd": 4}, overlaps) |
| assert subst == {0: 0, 2: 2, 3: 3} |
| subst = _match_condition({"abcd": 5}, overlaps) |
| assert subst == {0: 0, 1: 1, 2: 2, 3: 3} |
| subst = _match_condition({"abcd": 7}, overlaps) |
| assert subst == {0: 0, 1: 1, 2: 2, 3: 3} |
| subst = _match_condition({"abcd": 8}, overlaps) |
| assert subst == {0: 0, 1: 1, 2: 2} |
| subst = _match_condition({"abcd": 9}, overlaps) |
| assert subst == {0: 0, 1: 1} |
| subst = _match_condition({"abcd": 10}, overlaps) |
| assert subst == {1: 1} |
| |
| |
| def test_overlaps_2(): |
| # https://github.com/fonttools/fonttools/issues/1400 |
| conds = [ |
| ([{"abcd": (1, 9)}], {0: 0}), |
| ([{"abcd": (8, 10)}], {1: 1}), |
| ([{"abcd": (3, 4)}], {2: 2}), |
| ([{"abcd": (1, 10)}], {3: 3}), |
| ] |
| overlaps = overlayFeatureVariations(conds) |
| subst = _match_condition({"abcd": 0}, overlaps) |
| assert subst == {} |
| subst = _match_condition({"abcd": 1}, overlaps) |
| assert subst == {0: 0, 3: 3} |
| subst = _match_condition({"abcd": 2}, overlaps) |
| assert subst == {0: 0, 3: 3} |
| subst = _match_condition({"abcd": 3}, overlaps) |
| assert subst == {0: 0, 2: 2, 3: 3} |
| subst = _match_condition({"abcd": 5}, overlaps) |
| assert subst == {0: 0, 3: 3} |
| subst = _match_condition({"abcd": 10}, overlaps) |
| assert subst == {1: 1, 3: 3} |
| |
| |
| def test_overlayBox(): |
| # https://github.com/fonttools/fonttools/issues/3003 |
| top = {"opsz": (0.75, 1.0), "wght": (0.5, 1.0)} |
| bot = {"wght": (0.25, 1.0)} |
| intersection, remainder = overlayBox(top, bot) |
| assert intersection == {"opsz": (0.75, 1.0), "wght": (0.5, 1.0)} |
| assert remainder == {"wght": (0.25, 1.0)} |
| |
| |
| def run(test, n, quiet): |
| print() |
| print("%s:" % test.__name__) |
| input, output = test(n) |
| if quiet: |
| print(len(output)) |
| else: |
| print() |
| print("Input:") |
| pprint(input) |
| print() |
| print("Output:") |
| pprint(output) |
| print() |
| |
| |
| if __name__ == "__main__": |
| import sys |
| from pprint import pprint |
| |
| quiet = False |
| n = 3 |
| if len(sys.argv) > 1 and sys.argv[1] == "-q": |
| quiet = True |
| del sys.argv[1] |
| if len(sys.argv) > 1: |
| n = int(sys.argv[1]) |
| |
| run(_test_linear, n=n, quiet=quiet) |
| run(_test_quadratic, n=n, quiet=quiet) |