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