xref: /aosp_15_r20/external/fonttools/Tests/varLib/featureVars_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from collections import OrderedDict
2from fontTools.designspaceLib import AxisDescriptor
3from fontTools.ttLib import TTFont, newTable
4from fontTools import varLib
5from fontTools.varLib.featureVars import (
6    addFeatureVariations,
7    overlayFeatureVariations,
8    overlayBox,
9)
10import pytest
11
12
13def makeVariableFont(glyphOrder, axes):
14    font = TTFont()
15    font.setGlyphOrder(glyphOrder)
16    font["name"] = newTable("name")
17    ds_axes = OrderedDict()
18    for axisTag, (minimum, default, maximum) in axes.items():
19        axis = AxisDescriptor()
20        axis.name = axis.tag = axis.labelNames["en"] = axisTag
21        axis.minimum, axis.default, axis.maximum = minimum, default, maximum
22        ds_axes[axisTag] = axis
23    varLib._add_fvar(font, ds_axes, instances=())
24    return font
25
26
27@pytest.fixture
28def varfont():
29    return makeVariableFont(
30        [".notdef", "space", "A", "B", "A.alt", "B.alt"],
31        {"wght": (100, 400, 900)},
32    )
33
34
35def test_addFeatureVariations(varfont):
36    assert "GSUB" not in varfont
37
38    addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})])
39
40    assert "GSUB" in varfont
41    gsub = varfont["GSUB"].table
42
43    assert len(gsub.ScriptList.ScriptRecord) == 1
44    assert gsub.ScriptList.ScriptRecord[0].ScriptTag == "DFLT"
45
46    assert len(gsub.FeatureList.FeatureRecord) == 1
47    assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rvrn"
48
49    assert len(gsub.LookupList.Lookup) == 1
50    assert gsub.LookupList.Lookup[0].LookupType == 1
51    assert len(gsub.LookupList.Lookup[0].SubTable) == 1
52    assert gsub.LookupList.Lookup[0].SubTable[0].mapping == {"A": "A.alt"}
53
54    assert gsub.FeatureVariations is not None
55    assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
56    fvr = gsub.FeatureVariations.FeatureVariationRecord[0]
57    assert len(fvr.ConditionSet.ConditionTable) == 1
58    cst = fvr.ConditionSet.ConditionTable[0]
59    assert cst.AxisIndex == 0
60    assert cst.FilterRangeMinValue == 0.5
61    assert cst.FilterRangeMaxValue == 1.0
62    assert len(fvr.FeatureTableSubstitution.SubstitutionRecord) == 1
63    ftsr = fvr.FeatureTableSubstitution.SubstitutionRecord[0]
64    assert ftsr.FeatureIndex == 0
65    assert ftsr.Feature.LookupListIndex == [0]
66
67
68def _substitution_features(gsub, rec_index):
69    fea_tags = [feature.FeatureTag for feature in gsub.FeatureList.FeatureRecord]
70    fea_indices = [
71        gsub.FeatureVariations.FeatureVariationRecord[rec_index]
72        .FeatureTableSubstitution.SubstitutionRecord[i]
73        .FeatureIndex
74        for i in range(
75            len(
76                gsub.FeatureVariations.FeatureVariationRecord[
77                    rec_index
78                ].FeatureTableSubstitution.SubstitutionRecord
79            )
80        )
81    ]
82    return [(i, fea_tags[i]) for i in fea_indices]
83
84
85def test_addFeatureVariations_existing_variable_feature(varfont):
86    assert "GSUB" not in varfont
87
88    addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})])
89
90    gsub = varfont["GSUB"].table
91    assert len(gsub.FeatureList.FeatureRecord) == 1
92    assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rvrn"
93    assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
94    assert _substitution_features(gsub, rec_index=0) == [(0, "rvrn")]
95
96    # can't add feature variations for an existing feature tag that already has some,
97    # in this case the default 'rvrn'
98    with pytest.raises(
99        varLib.VarLibError,
100        match=r"FeatureVariations already exist for feature tag\(s\): {'rvrn'}",
101    ):
102        addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})])
103
104
105def test_addFeatureVariations_new_feature(varfont):
106    assert "GSUB" not in varfont
107
108    addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})])
109
110    gsub = varfont["GSUB"].table
111    assert len(gsub.FeatureList.FeatureRecord) == 1
112    assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rvrn"
113    assert len(gsub.LookupList.Lookup) == 1
114    assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
115    assert _substitution_features(gsub, rec_index=0) == [(0, "rvrn")]
116
117    # we can add feature variations for a feature tag that does not have
118    # any feature variations yet
119    addFeatureVariations(
120        varfont, [([{"wght": (-1.0, 0.0)}], {"B": "B.alt"})], featureTag="rclt"
121    )
122
123    assert len(gsub.FeatureList.FeatureRecord) == 2
124    # Note 'rclt' is now first (index=0) in the feature list sorted by tag, and
125    # 'rvrn' is second (index=1)
126    assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rclt"
127    assert gsub.FeatureList.FeatureRecord[1].FeatureTag == "rvrn"
128    assert len(gsub.LookupList.Lookup) == 2
129    assert len(gsub.FeatureVariations.FeatureVariationRecord) == 2
130    # The new 'rclt' feature variation record is appended to the end;
131    # the feature index for 'rvrn' feature table substitution record is now 1
132    assert _substitution_features(gsub, rec_index=0) == [(1, "rvrn")]
133    assert _substitution_features(gsub, rec_index=1) == [(0, "rclt")]
134
135
136def test_addFeatureVariations_existing_condition(varfont):
137    assert "GSUB" not in varfont
138
139    # Add a feature variation for 'ccmp' feature tag with a condition
140    addFeatureVariations(
141        varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})], featureTag="ccmp"
142    )
143
144    gsub = varfont["GSUB"].table
145
146    # Should now have one feature record, one lookup, and one feature variation record
147    assert len(gsub.FeatureList.FeatureRecord) == 1
148    assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "ccmp"
149    assert len(gsub.LookupList.Lookup) == 1
150    assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
151    assert _substitution_features(gsub, rec_index=0) == [(0, "ccmp")]
152
153    # Add a feature variation for 'rlig' feature tag with the same condition
154    addFeatureVariations(
155        varfont, [([{"wght": (0.5, 1.0)}], {"B": "B.alt"})], featureTag="rlig"
156    )
157
158    # Should now have two feature records, two lookups, and one feature variation
159    # record, since the condition is the same for both feature variations
160    assert len(gsub.FeatureList.FeatureRecord) == 2
161    assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "ccmp"
162    assert gsub.FeatureList.FeatureRecord[1].FeatureTag == "rlig"
163    assert len(gsub.LookupList.Lookup) == 2
164    assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
165    assert _substitution_features(gsub, rec_index=0) == [(0, "ccmp"), (1, "rlig")]
166
167
168def _test_linear(n):
169    conds = []
170    for i in range(n):
171        end = i / n
172        start = end - 1.0
173        region = [{"X": (start, end)}]
174        subst = {"g%.2g" % start: "g%.2g" % end}
175        conds.append((region, subst))
176    overlaps = overlayFeatureVariations(conds)
177    assert len(overlaps) == 2 * n - 1, overlaps
178    return conds, overlaps
179
180
181def test_linear():
182    _test_linear(10)
183
184
185def _test_quadratic(n):
186    conds = []
187    for i in range(1, n + 1):
188        region = [{"X": (0, i / n), "Y": (0, (n + 1 - i) / n)}]
189        subst = {str(i): str(n + 1 - i)}
190        conds.append((region, subst))
191    overlaps = overlayFeatureVariations(conds)
192    assert len(overlaps) == n * (n + 1) // 2, overlaps
193    return conds, overlaps
194
195
196def test_quadratic():
197    _test_quadratic(10)
198
199
200def _merge_substitutions(substitutions):
201    merged = {}
202    for subst in substitutions:
203        merged.update(subst)
204    return merged
205
206
207def _match_condition(location, overlaps):
208    for box, substitutions in overlaps:
209        for tag, coord in location.items():
210            start, end = box[tag]
211            if start <= coord <= end:
212                return _merge_substitutions(substitutions)
213    return {}  # no match
214
215
216def test_overlaps_1():
217    # https://github.com/fonttools/fonttools/issues/1400
218    conds = [
219        ([{"abcd": (4, 9)}], {0: 0}),
220        ([{"abcd": (5, 10)}], {1: 1}),
221        ([{"abcd": (0, 8)}], {2: 2}),
222        ([{"abcd": (3, 7)}], {3: 3}),
223    ]
224    overlaps = overlayFeatureVariations(conds)
225    subst = _match_condition({"abcd": 0}, overlaps)
226    assert subst == {2: 2}
227    subst = _match_condition({"abcd": 1}, overlaps)
228    assert subst == {2: 2}
229    subst = _match_condition({"abcd": 3}, overlaps)
230    assert subst == {2: 2, 3: 3}
231    subst = _match_condition({"abcd": 4}, overlaps)
232    assert subst == {0: 0, 2: 2, 3: 3}
233    subst = _match_condition({"abcd": 5}, overlaps)
234    assert subst == {0: 0, 1: 1, 2: 2, 3: 3}
235    subst = _match_condition({"abcd": 7}, overlaps)
236    assert subst == {0: 0, 1: 1, 2: 2, 3: 3}
237    subst = _match_condition({"abcd": 8}, overlaps)
238    assert subst == {0: 0, 1: 1, 2: 2}
239    subst = _match_condition({"abcd": 9}, overlaps)
240    assert subst == {0: 0, 1: 1}
241    subst = _match_condition({"abcd": 10}, overlaps)
242    assert subst == {1: 1}
243
244
245def test_overlaps_2():
246    # https://github.com/fonttools/fonttools/issues/1400
247    conds = [
248        ([{"abcd": (1, 9)}], {0: 0}),
249        ([{"abcd": (8, 10)}], {1: 1}),
250        ([{"abcd": (3, 4)}], {2: 2}),
251        ([{"abcd": (1, 10)}], {3: 3}),
252    ]
253    overlaps = overlayFeatureVariations(conds)
254    subst = _match_condition({"abcd": 0}, overlaps)
255    assert subst == {}
256    subst = _match_condition({"abcd": 1}, overlaps)
257    assert subst == {0: 0, 3: 3}
258    subst = _match_condition({"abcd": 2}, overlaps)
259    assert subst == {0: 0, 3: 3}
260    subst = _match_condition({"abcd": 3}, overlaps)
261    assert subst == {0: 0, 2: 2, 3: 3}
262    subst = _match_condition({"abcd": 5}, overlaps)
263    assert subst == {0: 0, 3: 3}
264    subst = _match_condition({"abcd": 10}, overlaps)
265    assert subst == {1: 1, 3: 3}
266
267
268def test_overlayBox():
269    # https://github.com/fonttools/fonttools/issues/3003
270    top = {"opsz": (0.75, 1.0), "wght": (0.5, 1.0)}
271    bot = {"wght": (0.25, 1.0)}
272    intersection, remainder = overlayBox(top, bot)
273    assert intersection == {"opsz": (0.75, 1.0), "wght": (0.5, 1.0)}
274    assert remainder == {"wght": (0.25, 1.0)}
275
276
277def run(test, n, quiet):
278    print()
279    print("%s:" % test.__name__)
280    input, output = test(n)
281    if quiet:
282        print(len(output))
283    else:
284        print()
285        print("Input:")
286        pprint(input)
287        print()
288        print("Output:")
289        pprint(output)
290        print()
291
292
293if __name__ == "__main__":
294    import sys
295    from pprint import pprint
296
297    quiet = False
298    n = 3
299    if len(sys.argv) > 1 and sys.argv[1] == "-q":
300        quiet = True
301        del sys.argv[1]
302    if len(sys.argv) > 1:
303        n = int(sys.argv[1])
304
305    run(_test_linear, n=n, quiet=quiet)
306    run(_test_quadratic, n=n, quiet=quiet)
307