xref: /aosp_15_r20/external/fonttools/Tests/designspaceLib/split_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1import math
2import shutil
3from pathlib import Path
4
5import pytest
6from fontTools.designspaceLib import DesignSpaceDocument
7from fontTools.designspaceLib.split import (
8    _conditionSetFrom,
9    convert5to4,
10    splitInterpolable,
11    splitVariableFonts,
12)
13from fontTools.designspaceLib.types import ConditionSet, Range
14
15from .fixtures import datadir
16
17UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING = False
18
19
20@pytest.mark.parametrize(
21    "test_ds,expected_interpolable_spaces",
22    [
23        (
24            "test_v5_aktiv.designspace",
25            [
26                (
27                    {},
28                    {
29                        "AktivGroteskVF_Italics_Wght",
30                        "AktivGroteskVF_Italics_WghtWdth",
31                        "AktivGroteskVF_Wght",
32                        "AktivGroteskVF_WghtWdth",
33                        "AktivGroteskVF_WghtWdthItal",
34                    },
35                )
36            ],
37        ),
38        (
39            "test_v5_sourceserif.designspace",
40            [
41                (
42                    {"italic": 0},
43                    {"SourceSerif4Variable-Roman"},
44                ),
45                (
46                    {"italic": 1},
47                    {"SourceSerif4Variable-Italic"},
48                ),
49            ],
50        ),
51        (
52            "test_v5_MutatorSans_and_Serif.designspace",
53            [
54                (
55                    {"serif": 0},
56                    {
57                        "MutatorSansVariable_Weight_Width",
58                        "MutatorSansVariable_Weight",
59                        "MutatorSansVariable_Width",
60                    },
61                ),
62                (
63                    {"serif": 1},
64                    {
65                        "MutatorSerifVariable_Width",
66                    },
67                ),
68            ],
69        ),
70    ],
71)
72def test_split(datadir, tmpdir, test_ds, expected_interpolable_spaces):
73    data_in = datadir / test_ds
74    temp_in = Path(tmpdir) / test_ds
75    shutil.copy(data_in, temp_in)
76    doc = DesignSpaceDocument.fromfile(temp_in)
77
78    for i, (location, sub_doc) in enumerate(splitInterpolable(doc)):
79        expected_location, expected_vf_names = expected_interpolable_spaces[i]
80        assert location == expected_location
81        vfs = list(splitVariableFonts(sub_doc))
82        assert expected_vf_names == set(vf[0] for vf in vfs)
83
84        loc_str = "_".join(
85            f"{name}_{value}" for name, value in sorted(location.items())
86        )
87        data_out = datadir / "split_output" / f"{temp_in.stem}_{loc_str}.designspace"
88        temp_out = Path(tmpdir) / "out" / f"{temp_in.stem}_{loc_str}.designspace"
89        temp_out.parent.mkdir(exist_ok=True)
90        sub_doc.write(temp_out)
91
92        if UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING:
93            data_out.write_text(temp_out.read_text(encoding="utf-8"), encoding="utf-8")
94        else:
95            assert data_out.read_text(encoding="utf-8") == temp_out.read_text(
96                encoding="utf-8"
97            )
98
99        for vf_name, vf_doc in vfs:
100            data_out = (datadir / "split_output" / vf_name).with_suffix(".designspace")
101            temp_out = (Path(tmpdir) / "out" / vf_name).with_suffix(".designspace")
102            temp_out.parent.mkdir(exist_ok=True)
103            vf_doc.write(temp_out)
104
105            if UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING:
106                data_out.write_text(
107                    temp_out.read_text(encoding="utf-8"), encoding="utf-8"
108                )
109            else:
110                assert data_out.read_text(encoding="utf-8") == temp_out.read_text(
111                    encoding="utf-8"
112                )
113
114
115@pytest.mark.parametrize(
116    "test_ds,expected_vfs",
117    [
118        (
119            "test_v5_aktiv.designspace",
120            {
121                "AktivGroteskVF_Italics_Wght",
122                "AktivGroteskVF_Italics_WghtWdth",
123                "AktivGroteskVF_Wght",
124                "AktivGroteskVF_WghtWdth",
125                "AktivGroteskVF_WghtWdthItal",
126            },
127        ),
128        (
129            "test_v5_sourceserif.designspace",
130            {
131                "SourceSerif4Variable-Italic",
132                "SourceSerif4Variable-Roman",
133            },
134        ),
135    ],
136)
137def test_convert5to4(datadir, tmpdir, test_ds, expected_vfs):
138    data_in = datadir / test_ds
139    temp_in = tmpdir / test_ds
140    shutil.copy(data_in, temp_in)
141    doc = DesignSpaceDocument.fromfile(temp_in)
142
143    variable_fonts = convert5to4(doc)
144
145    assert variable_fonts.keys() == expected_vfs
146    for vf_name, vf in variable_fonts.items():
147        data_out = (datadir / "convert5to4_output" / vf_name).with_suffix(
148            ".designspace"
149        )
150        temp_out = (Path(tmpdir) / "out" / vf_name).with_suffix(".designspace")
151        temp_out.parent.mkdir(exist_ok=True)
152        vf.write(temp_out)
153
154        if UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING:
155            data_out.write_text(temp_out.read_text(encoding="utf-8"), encoding="utf-8")
156        else:
157            assert data_out.read_text(encoding="utf-8") == temp_out.read_text(
158                encoding="utf-8"
159            )
160
161
162@pytest.mark.parametrize(
163    ["unbounded_condition"],
164    [
165        ({"name": "Weight", "minimum": 500},),
166        ({"name": "Weight", "maximum": 500},),
167        ({"name": "Weight", "minimum": 500, "maximum": None},),
168        ({"name": "Weight", "minimum": None, "maximum": 500},),
169    ],
170)
171def test_optional_min_max(unbounded_condition):
172    """Check that split functions can handle conditions that are partially
173    unbounded without tripping over None values and missing keys."""
174    doc = DesignSpaceDocument()
175
176    doc.addAxisDescriptor(
177        name="Weight", tag="wght", minimum=400, maximum=1000, default=400
178    )
179
180    doc.addRuleDescriptor(
181        name="unbounded",
182        conditionSets=[[unbounded_condition]],
183    )
184
185    assert len(list(splitInterpolable(doc))) == 1
186    assert len(list(splitVariableFonts(doc))) == 1
187
188
189@pytest.mark.parametrize(
190    ["condition", "expected_set"],
191    [
192        (
193            {"name": "axis", "minimum": 0.5},
194            {"axis": Range(minimum=0.5, maximum=math.inf)},
195        ),
196        (
197            {"name": "axis", "maximum": 0.5},
198            {"axis": Range(minimum=-math.inf, maximum=0.5)},
199        ),
200        (
201            {"name": "axis", "minimum": 0.5, "maximum": None},
202            {"axis": Range(minimum=0.5, maximum=math.inf)},
203        ),
204        (
205            {"name": "axis", "minimum": None, "maximum": 0.5},
206            {"axis": Range(minimum=-math.inf, maximum=0.5)},
207        ),
208    ],
209)
210def test_optional_min_max_internal(condition, expected_set: ConditionSet):
211    """Check that split's internal helper functions produce the correct output
212    for conditions that are partially unbounded."""
213    assert _conditionSetFrom([condition]) == expected_set
214
215
216def test_avar2(datadir):
217    ds = DesignSpaceDocument()
218    ds.read(datadir / "test_avar2.designspace")
219    _, subDoc = next(splitInterpolable(ds))
220    assert len(subDoc.axisMappings) == 2
221
222    subDocs = list(splitVariableFonts(ds))
223    assert len(subDocs) == 5
224    for i, (_, subDoc) in enumerate(subDocs):
225        # Only the first one should have a mapping, according to the document
226        if i == 0:
227            assert len(subDoc.axisMappings) == 2
228        else:
229            assert len(subDoc.axisMappings) == 0
230