xref: /aosp_15_r20/external/fonttools/Tests/varLib/instancer/names_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.ttLib.tables import otTables
2from fontTools.otlLib.builder import buildStatTable
3from fontTools.varLib import instancer
4
5import pytest
6
7
8def test_pruningUnusedNames(varfont):
9    varNameIDs = instancer.names.getVariationNameIDs(varfont)
10
11    assert varNameIDs == set(range(256, 297 + 1))
12
13    fvar = varfont["fvar"]
14    stat = varfont["STAT"].table
15
16    with instancer.names.pruningUnusedNames(varfont):
17        del fvar.axes[0]  # Weight (nameID=256)
18        del fvar.instances[0]  # Thin (nameID=258)
19        del stat.DesignAxisRecord.Axis[0]  # Weight (nameID=256)
20        del stat.AxisValueArray.AxisValue[0]  # Thin (nameID=258)
21
22    assert not any(n for n in varfont["name"].names if n.nameID in {256, 258})
23
24    with instancer.names.pruningUnusedNames(varfont):
25        del varfont["fvar"]
26        del varfont["STAT"]
27
28    assert not any(n for n in varfont["name"].names if n.nameID in varNameIDs)
29    assert "ltag" not in varfont
30
31
32def _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x409]):
33    nametable = varfont["name"]
34    font_names = {
35        (r.nameID, r.platformID, r.platEncID, r.langID): r.toUnicode()
36        for r in nametable.names
37    }
38    for k in expected:
39        if k[-1] not in platforms:
40            continue
41        assert font_names[k] == expected[k]
42
43    font_nameids = set(i[0] for i in font_names)
44    if isNonRIBBI:
45        assert 16 in font_nameids
46        assert 17 in font_nameids
47
48    if "fvar" not in varfont:
49        assert 25 not in font_nameids
50
51
52@pytest.mark.parametrize(
53    "limits, expected, isNonRIBBI",
54    [
55        # Regular
56        (
57            {"wght": 400},
58            {
59                (1, 3, 1, 0x409): "Test Variable Font",
60                (2, 3, 1, 0x409): "Regular",
61                (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Regular",
62                (6, 3, 1, 0x409): "TestVariableFont-Regular",
63            },
64            False,
65        ),
66        # Regular Normal (width axis Normal isn't included since it is elided)
67        (
68            {"wght": 400, "wdth": 100},
69            {
70                (1, 3, 1, 0x409): "Test Variable Font",
71                (2, 3, 1, 0x409): "Regular",
72                (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Regular",
73                (6, 3, 1, 0x409): "TestVariableFont-Regular",
74            },
75            False,
76        ),
77        # Black
78        (
79            {"wght": 900},
80            {
81                (1, 3, 1, 0x409): "Test Variable Font Black",
82                (2, 3, 1, 0x409): "Regular",
83                (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Black",
84                (6, 3, 1, 0x409): "TestVariableFont-Black",
85                (16, 3, 1, 0x409): "Test Variable Font",
86                (17, 3, 1, 0x409): "Black",
87            },
88            True,
89        ),
90        # Thin
91        (
92            {"wght": 100},
93            {
94                (1, 3, 1, 0x409): "Test Variable Font Thin",
95                (2, 3, 1, 0x409): "Regular",
96                (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Thin",
97                (6, 3, 1, 0x409): "TestVariableFont-Thin",
98                (16, 3, 1, 0x409): "Test Variable Font",
99                (17, 3, 1, 0x409): "Thin",
100            },
101            True,
102        ),
103        # Thin Condensed
104        (
105            {"wght": 100, "wdth": 79},
106            {
107                (1, 3, 1, 0x409): "Test Variable Font Thin Condensed",
108                (2, 3, 1, 0x409): "Regular",
109                (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-ThinCondensed",
110                (6, 3, 1, 0x409): "TestVariableFont-ThinCondensed",
111                (16, 3, 1, 0x409): "Test Variable Font",
112                (17, 3, 1, 0x409): "Thin Condensed",
113            },
114            True,
115        ),
116        # Condensed with unpinned weights
117        (
118            {"wdth": 79, "wght": (400, 900)},
119            {
120                (1, 3, 1, 0x409): "Test Variable Font Condensed",
121                (2, 3, 1, 0x409): "Regular",
122                (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Condensed",
123                (6, 3, 1, 0x409): "TestVariableFont-Condensed",
124                (16, 3, 1, 0x409): "Test Variable Font",
125                (17, 3, 1, 0x409): "Condensed",
126            },
127            True,
128        ),
129        # Restrict weight and move default, new minimum (500) > old default (400)
130        (
131            {"wght": (500, 900)},
132            {
133                (1, 3, 1, 0x409): "Test Variable Font Medium",
134                (2, 3, 1, 0x409): "Regular",
135                (3, 3, 1, 0x409): "2.001;GOOG;TestVariableFont-Medium",
136                (6, 3, 1, 0x409): "TestVariableFont-Medium",
137                (16, 3, 1, 0x409): "Test Variable Font",
138                (17, 3, 1, 0x409): "Medium",
139            },
140            True,
141        ),
142    ],
143)
144def test_updateNameTable_with_registered_axes_ribbi(
145    varfont, limits, expected, isNonRIBBI
146):
147    instancer.names.updateNameTable(varfont, limits)
148    _test_name_records(varfont, expected, isNonRIBBI)
149
150
151def test_updatetNameTable_axis_order(varfont):
152    axes = [
153        dict(
154            tag="wght",
155            name="Weight",
156            values=[
157                dict(value=400, name="Regular"),
158            ],
159        ),
160        dict(
161            tag="wdth",
162            name="Width",
163            values=[
164                dict(value=75, name="Condensed"),
165            ],
166        ),
167    ]
168    nametable = varfont["name"]
169    buildStatTable(varfont, axes)
170    instancer.names.updateNameTable(varfont, {"wdth": 75, "wght": 400})
171    assert nametable.getName(17, 3, 1, 0x409).toUnicode() == "Regular Condensed"
172
173    # Swap the axes so the names get swapped
174    axes[0], axes[1] = axes[1], axes[0]
175
176    buildStatTable(varfont, axes)
177    instancer.names.updateNameTable(varfont, {"wdth": 75, "wght": 400})
178    assert nametable.getName(17, 3, 1, 0x409).toUnicode() == "Condensed Regular"
179
180
181@pytest.mark.parametrize(
182    "limits, expected, isNonRIBBI",
183    [
184        # Regular | Normal
185        (
186            {"wght": 400},
187            {
188                (1, 3, 1, 0x409): "Test Variable Font",
189                (2, 3, 1, 0x409): "Normal",
190            },
191            False,
192        ),
193        # Black | Negreta
194        (
195            {"wght": 900},
196            {
197                (1, 3, 1, 0x409): "Test Variable Font Negreta",
198                (2, 3, 1, 0x409): "Normal",
199                (16, 3, 1, 0x409): "Test Variable Font",
200                (17, 3, 1, 0x409): "Negreta",
201            },
202            True,
203        ),
204        # Black Condensed | Negreta Zhuštěné
205        (
206            {"wght": 900, "wdth": 79},
207            {
208                (1, 3, 1, 0x409): "Test Variable Font Negreta Zhuštěné",
209                (2, 3, 1, 0x409): "Normal",
210                (16, 3, 1, 0x409): "Test Variable Font",
211                (17, 3, 1, 0x409): "Negreta Zhuštěné",
212            },
213            True,
214        ),
215    ],
216)
217def test_updateNameTable_with_multilingual_names(varfont, limits, expected, isNonRIBBI):
218    name = varfont["name"]
219    # langID 0x405 is the Czech Windows langID
220    name.setName("Test Variable Font", 1, 3, 1, 0x405)
221    name.setName("Normal", 2, 3, 1, 0x405)
222    name.setName("Normal", 261, 3, 1, 0x405)  # nameID 261=Regular STAT entry
223    name.setName("Negreta", 266, 3, 1, 0x405)  # nameID 266=Black STAT entry
224    name.setName("Zhuštěné", 279, 3, 1, 0x405)  # nameID 279=Condensed STAT entry
225
226    instancer.names.updateNameTable(varfont, limits)
227    _test_name_records(varfont, expected, isNonRIBBI, platforms=[0x405])
228
229
230def test_updateNameTable_missing_axisValues(varfont):
231    with pytest.raises(ValueError, match="Cannot find Axis Values {'wght': 200}"):
232        instancer.names.updateNameTable(varfont, {"wght": 200})
233
234
235def test_updateNameTable_missing_stat(varfont):
236    del varfont["STAT"]
237    with pytest.raises(
238        ValueError, match="Cannot update name table since there is no STAT table."
239    ):
240        instancer.names.updateNameTable(varfont, {"wght": 400})
241
242
243@pytest.mark.parametrize(
244    "limits, expected, isNonRIBBI",
245    [
246        # Regular | Normal
247        (
248            {"wght": 400},
249            {
250                (1, 3, 1, 0x409): "Test Variable Font",
251                (2, 3, 1, 0x409): "Italic",
252                (6, 3, 1, 0x409): "TestVariableFont-Italic",
253            },
254            False,
255        ),
256        # Black Condensed Italic
257        (
258            {"wght": 900, "wdth": 79},
259            {
260                (1, 3, 1, 0x409): "Test Variable Font Black Condensed",
261                (2, 3, 1, 0x409): "Italic",
262                (6, 3, 1, 0x409): "TestVariableFont-BlackCondensedItalic",
263                (16, 3, 1, 0x409): "Test Variable Font",
264                (17, 3, 1, 0x409): "Black Condensed Italic",
265            },
266            True,
267        ),
268    ],
269)
270def test_updateNameTable_vf_with_italic_attribute(
271    varfont, limits, expected, isNonRIBBI
272):
273    font_link_axisValue = varfont["STAT"].table.AxisValueArray.AxisValue[5]
274    # Unset ELIDABLE_AXIS_VALUE_NAME flag
275    font_link_axisValue.Flags &= ~instancer.names.ELIDABLE_AXIS_VALUE_NAME
276    font_link_axisValue.ValueNameID = 294  # Roman --> Italic
277
278    instancer.names.updateNameTable(varfont, limits)
279    _test_name_records(varfont, expected, isNonRIBBI)
280
281
282def test_updateNameTable_format4_axisValues(varfont):
283    # format 4 axisValues should dominate the other axisValues
284    stat = varfont["STAT"].table
285
286    axisValue = otTables.AxisValue()
287    axisValue.Format = 4
288    axisValue.Flags = 0
289    varfont["name"].setName("Dominant Value", 297, 3, 1, 0x409)
290    axisValue.ValueNameID = 297
291    axisValue.AxisValueRecord = []
292    for tag, value in (("wght", 900), ("wdth", 79)):
293        rec = otTables.AxisValueRecord()
294        rec.AxisIndex = next(
295            i for i, a in enumerate(stat.DesignAxisRecord.Axis) if a.AxisTag == tag
296        )
297        rec.Value = value
298        axisValue.AxisValueRecord.append(rec)
299    stat.AxisValueArray.AxisValue.append(axisValue)
300
301    instancer.names.updateNameTable(varfont, {"wdth": 79, "wght": 900})
302    expected = {
303        (1, 3, 1, 0x409): "Test Variable Font Dominant Value",
304        (2, 3, 1, 0x409): "Regular",
305        (16, 3, 1, 0x409): "Test Variable Font",
306        (17, 3, 1, 0x409): "Dominant Value",
307    }
308    _test_name_records(varfont, expected, isNonRIBBI=True)
309
310
311def test_updateNameTable_elided_axisValues(varfont):
312    stat = varfont["STAT"].table
313    # set ELIDABLE_AXIS_VALUE_NAME flag for all axisValues
314    for axisValue in stat.AxisValueArray.AxisValue:
315        axisValue.Flags |= instancer.names.ELIDABLE_AXIS_VALUE_NAME
316
317    stat.ElidedFallbackNameID = 266  # Regular --> Black
318    instancer.names.updateNameTable(varfont, {"wght": 400})
319    # Since all axis values are elided, the elided fallback name
320    # must be used to construct the style names. Since we
321    # changed it to Black, we need both a typoSubFamilyName and
322    # the subFamilyName set so it conforms to the RIBBI model.
323    expected = {(2, 3, 1, 0x409): "Regular", (17, 3, 1, 0x409): "Black"}
324    _test_name_records(varfont, expected, isNonRIBBI=True)
325
326
327def test_updateNameTable_existing_subfamily_name_is_not_regular(varfont):
328    # Check the subFamily name will be set to Regular when we update a name
329    # table to a non-RIBBI style and the current subFamily name is a RIBBI
330    # style which isn't Regular.
331    varfont["name"].setName("Bold", 2, 3, 1, 0x409)  # subFamily Regular --> Bold
332
333    instancer.names.updateNameTable(varfont, {"wght": 100})
334    expected = {(2, 3, 1, 0x409): "Regular", (17, 3, 1, 0x409): "Thin"}
335    _test_name_records(varfont, expected, isNonRIBBI=True)
336
337
338def test_name_irrelevant_axes(varfont):
339    # Cannot update name table if not on a named axis value location
340    with pytest.raises(ValueError) as excinfo:
341        location = {"wght": 400, "wdth": 90}
342        instance = instancer.instantiateVariableFont(
343            varfont, location, updateFontNames=True
344        )
345    assert "Cannot find Axis Values" in str(excinfo.value)
346
347    # Now let's make the wdth axis "irrelevant" to naming (no axis values)
348    varfont["STAT"].table.AxisValueArray.AxisValue.pop(6)
349    varfont["STAT"].table.AxisValueArray.AxisValue.pop(4)
350    location = {"wght": 400, "wdth": 90}
351    instance = instancer.instantiateVariableFont(
352        varfont, location, updateFontNames=True
353    )
354