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