1from fontTools.misc.fixedTools import floatToFixedToFloat 2from fontTools.misc.testTools import stripVariableItemsFromTTX 3from fontTools.misc.textTools import Tag 4from fontTools import ttLib 5from fontTools import designspaceLib 6from fontTools.feaLib.builder import addOpenTypeFeaturesFromString 7from fontTools.ttLib.tables import _f_v_a_r, _g_l_y_f 8from fontTools.ttLib.tables import otTables 9from fontTools.ttLib.tables.TupleVariation import TupleVariation 10from fontTools import varLib 11from fontTools.varLib import instancer 12from fontTools.varLib.mvar import MVAR_ENTRIES 13from fontTools.varLib import builder 14from fontTools.varLib import featureVars 15from fontTools.varLib import models 16import collections 17from copy import deepcopy 18from io import BytesIO, StringIO 19import logging 20import os 21import re 22from types import SimpleNamespace 23import pytest 24 25 26# see Tests/varLib/instancer/conftest.py for "varfont" fixture definition 27 28TESTDATA = os.path.join(os.path.dirname(__file__), "data") 29 30 31@pytest.fixture(params=[True, False], ids=["optimize", "no-optimize"]) 32def optimize(request): 33 return request.param 34 35 36@pytest.fixture 37def fvarAxes(): 38 wght = _f_v_a_r.Axis() 39 wght.axisTag = Tag("wght") 40 wght.minValue = 100 41 wght.defaultValue = 400 42 wght.maxValue = 900 43 wdth = _f_v_a_r.Axis() 44 wdth.axisTag = Tag("wdth") 45 wdth.minValue = 70 46 wdth.defaultValue = 100 47 wdth.maxValue = 100 48 return [wght, wdth] 49 50 51def _get_coordinates(varfont, glyphname): 52 # converts GlyphCoordinates to a list of (x, y) tuples, so that pytest's 53 # assert will give us a nicer diff 54 return list(varfont["glyf"].getCoordinatesAndControls(glyphname, varfont)[0]) 55 56 57class InstantiateGvarTest(object): 58 @pytest.mark.parametrize("glyph_name", ["hyphen"]) 59 @pytest.mark.parametrize( 60 "location, expected", 61 [ 62 pytest.param( 63 {"wdth": -1.0}, 64 { 65 "hyphen": [ 66 (27, 229), 67 (27, 310), 68 (247, 310), 69 (247, 229), 70 (0, 0), 71 (274, 0), 72 (0, 536), 73 (0, 0), 74 ] 75 }, 76 id="wdth=-1.0", 77 ), 78 pytest.param( 79 {"wdth": -0.5}, 80 { 81 "hyphen": [ 82 (33.5, 229), 83 (33.5, 308.5), 84 (264.5, 308.5), 85 (264.5, 229), 86 (0, 0), 87 (298, 0), 88 (0, 536), 89 (0, 0), 90 ] 91 }, 92 id="wdth=-0.5", 93 ), 94 # an axis pinned at the default normalized location (0.0) means 95 # the default glyf outline stays the same 96 pytest.param( 97 {"wdth": 0.0}, 98 { 99 "hyphen": [ 100 (40, 229), 101 (40, 307), 102 (282, 307), 103 (282, 229), 104 (0, 0), 105 (322, 0), 106 (0, 536), 107 (0, 0), 108 ] 109 }, 110 id="wdth=0.0", 111 ), 112 ], 113 ) 114 def test_pin_and_drop_axis(self, varfont, glyph_name, location, expected, optimize): 115 instancer.instantiateGvar(varfont, location, optimize=optimize) 116 117 assert _get_coordinates(varfont, glyph_name) == expected[glyph_name] 118 119 # check that the pinned axis has been dropped from gvar 120 assert not any( 121 "wdth" in t.axes 122 for tuples in varfont["gvar"].variations.values() 123 for t in tuples 124 ) 125 126 def test_full_instance(self, varfont, optimize): 127 instancer.instantiateGvar( 128 varfont, {"wght": 0.0, "wdth": -0.5}, optimize=optimize 129 ) 130 131 assert _get_coordinates(varfont, "hyphen") == [ 132 (33.5, 229), 133 (33.5, 308.5), 134 (264.5, 308.5), 135 (264.5, 229), 136 (0, 0), 137 (298, 0), 138 (0, 536), 139 (0, 0), 140 ] 141 142 assert "gvar" not in varfont 143 144 def test_composite_glyph_not_in_gvar(self, varfont): 145 """The 'minus' glyph is a composite glyph, which references 'hyphen' as a 146 component, but has no tuple variations in gvar table, so the component offset 147 and the phantom points do not change; however the sidebearings and bounding box 148 do change as a result of the parent glyph 'hyphen' changing. 149 """ 150 hmtx = varfont["hmtx"] 151 vmtx = varfont["vmtx"] 152 153 hyphenCoords = _get_coordinates(varfont, "hyphen") 154 assert hyphenCoords == [ 155 (40, 229), 156 (40, 307), 157 (282, 307), 158 (282, 229), 159 (0, 0), 160 (322, 0), 161 (0, 536), 162 (0, 0), 163 ] 164 assert hmtx["hyphen"] == (322, 40) 165 assert vmtx["hyphen"] == (536, 229) 166 167 minusCoords = _get_coordinates(varfont, "minus") 168 assert minusCoords == [(0, 0), (0, 0), (422, 0), (0, 536), (0, 0)] 169 assert hmtx["minus"] == (422, 40) 170 assert vmtx["minus"] == (536, 229) 171 172 location = {"wght": -1.0, "wdth": -1.0} 173 174 instancer.instantiateGvar(varfont, location) 175 176 # check 'hyphen' coordinates changed 177 assert _get_coordinates(varfont, "hyphen") == [ 178 (26, 259), 179 (26, 286), 180 (237, 286), 181 (237, 259), 182 (0, 0), 183 (263, 0), 184 (0, 536), 185 (0, 0), 186 ] 187 # check 'minus' coordinates (i.e. component offset and phantom points) 188 # did _not_ change 189 assert _get_coordinates(varfont, "minus") == minusCoords 190 191 assert hmtx["hyphen"] == (263, 26) 192 assert vmtx["hyphen"] == (536, 250) 193 194 assert hmtx["minus"] == (422, 26) # 'minus' left sidebearing changed 195 assert vmtx["minus"] == (536, 250) # 'minus' top sidebearing too 196 197 198class InstantiateCvarTest(object): 199 @pytest.mark.parametrize( 200 "location, expected", 201 [ 202 pytest.param({"wght": -1.0}, [500, -400, 150, 250], id="wght=-1.0"), 203 pytest.param({"wdth": -1.0}, [500, -400, 180, 200], id="wdth=-1.0"), 204 pytest.param({"wght": -0.5}, [500, -400, 165, 250], id="wght=-0.5"), 205 pytest.param({"wdth": -0.3}, [500, -400, 180, 235], id="wdth=-0.3"), 206 ], 207 ) 208 def test_pin_and_drop_axis(self, varfont, location, expected): 209 instancer.instantiateCvar(varfont, location) 210 211 assert list(varfont["cvt "].values) == expected 212 213 # check that the pinned axis has been dropped from cvar 214 pinned_axes = location.keys() 215 assert not any( 216 axis in t.axes for t in varfont["cvar"].variations for axis in pinned_axes 217 ) 218 219 def test_full_instance(self, varfont): 220 instancer.instantiateCvar(varfont, {"wght": -0.5, "wdth": -0.5}) 221 222 assert list(varfont["cvt "].values) == [500, -400, 165, 225] 223 224 assert "cvar" not in varfont 225 226 227class InstantiateMVARTest(object): 228 @pytest.mark.parametrize( 229 "location, expected", 230 [ 231 pytest.param( 232 {"wght": 1.0}, 233 {"strs": 100, "undo": -200, "unds": 150, "xhgt": 530}, 234 id="wght=1.0", 235 ), 236 pytest.param( 237 {"wght": 0.5}, 238 {"strs": 75, "undo": -150, "unds": 100, "xhgt": 515}, 239 id="wght=0.5", 240 ), 241 pytest.param( 242 {"wght": 0.0}, 243 {"strs": 50, "undo": -100, "unds": 50, "xhgt": 500}, 244 id="wght=0.0", 245 ), 246 pytest.param( 247 {"wdth": -1.0}, 248 {"strs": 20, "undo": -100, "unds": 50, "xhgt": 500}, 249 id="wdth=-1.0", 250 ), 251 pytest.param( 252 {"wdth": -0.5}, 253 {"strs": 35, "undo": -100, "unds": 50, "xhgt": 500}, 254 id="wdth=-0.5", 255 ), 256 pytest.param( 257 {"wdth": 0.0}, 258 {"strs": 50, "undo": -100, "unds": 50, "xhgt": 500}, 259 id="wdth=0.0", 260 ), 261 ], 262 ) 263 def test_pin_and_drop_axis(self, varfont, location, expected): 264 mvar = varfont["MVAR"].table 265 # initially we have two VarData: the first contains deltas associated with 3 266 # regions: 1 with only wght, 1 with only wdth, and 1 with both wght and wdth 267 assert len(mvar.VarStore.VarData) == 2 268 assert mvar.VarStore.VarRegionList.RegionCount == 3 269 assert mvar.VarStore.VarData[0].VarRegionCount == 3 270 assert all(len(item) == 3 for item in mvar.VarStore.VarData[0].Item) 271 # The second VarData has deltas associated only with 1 region (wght only). 272 assert mvar.VarStore.VarData[1].VarRegionCount == 1 273 assert all(len(item) == 1 for item in mvar.VarStore.VarData[1].Item) 274 275 instancer.instantiateMVAR(varfont, location) 276 277 for mvar_tag, expected_value in expected.items(): 278 table_tag, item_name = MVAR_ENTRIES[mvar_tag] 279 assert getattr(varfont[table_tag], item_name) == expected_value 280 281 # check that regions and accompanying deltas have been dropped 282 num_regions_left = len(mvar.VarStore.VarRegionList.Region) 283 assert num_regions_left < 3 284 assert mvar.VarStore.VarRegionList.RegionCount == num_regions_left 285 assert mvar.VarStore.VarData[0].VarRegionCount == num_regions_left 286 # VarData subtables have been merged 287 assert len(mvar.VarStore.VarData) == 1 288 289 @pytest.mark.parametrize( 290 "location, expected", 291 [ 292 pytest.param( 293 {"wght": 1.0, "wdth": 0.0}, 294 {"strs": 100, "undo": -200, "unds": 150}, 295 id="wght=1.0,wdth=0.0", 296 ), 297 pytest.param( 298 {"wght": 0.0, "wdth": -1.0}, 299 {"strs": 20, "undo": -100, "unds": 50}, 300 id="wght=0.0,wdth=-1.0", 301 ), 302 pytest.param( 303 {"wght": 0.5, "wdth": -0.5}, 304 {"strs": 55, "undo": -145, "unds": 95}, 305 id="wght=0.5,wdth=-0.5", 306 ), 307 pytest.param( 308 {"wght": 1.0, "wdth": -1.0}, 309 {"strs": 50, "undo": -180, "unds": 130}, 310 id="wght=0.5,wdth=-0.5", 311 ), 312 ], 313 ) 314 def test_full_instance(self, varfont, location, expected): 315 instancer.instantiateMVAR(varfont, location) 316 317 for mvar_tag, expected_value in expected.items(): 318 table_tag, item_name = MVAR_ENTRIES[mvar_tag] 319 assert getattr(varfont[table_tag], item_name) == expected_value 320 321 assert "MVAR" not in varfont 322 323 324class InstantiateHVARTest(object): 325 # the 'expectedDeltas' below refer to the VarData item deltas for the "hyphen" 326 # glyph in the PartialInstancerTest-VF.ttx test font, that are left after 327 # partial instancing 328 @pytest.mark.parametrize( 329 "location, expectedRegions, expectedDeltas", 330 [ 331 ({"wght": -1.0}, [{"wdth": (-1.0, -1.0, 0)}], [-59]), 332 ({"wght": 0}, [{"wdth": (-1.0, -1.0, 0)}], [-48]), 333 ({"wght": 1.0}, [{"wdth": (-1.0, -1.0, 0)}], [7]), 334 ( 335 {"wdth": -1.0}, 336 [ 337 {"wght": (-1.0, -1.0, 0.0)}, 338 {"wght": (0.0, 0.6099854, 1.0)}, 339 {"wght": (0.6099854, 1.0, 1.0)}, 340 ], 341 [-11, 31, 51], 342 ), 343 ({"wdth": 0}, [{"wght": (0.6099854, 1.0, 1.0)}], [-4]), 344 ], 345 ) 346 def test_partial_instance(self, varfont, location, expectedRegions, expectedDeltas): 347 instancer.instantiateHVAR(varfont, location) 348 349 assert "HVAR" in varfont 350 hvar = varfont["HVAR"].table 351 varStore = hvar.VarStore 352 353 regions = varStore.VarRegionList.Region 354 fvarAxes = [a for a in varfont["fvar"].axes if a.axisTag not in location] 355 regionDicts = [reg.get_support(fvarAxes) for reg in regions] 356 assert len(regionDicts) == len(expectedRegions) 357 for region, expectedRegion in zip(regionDicts, expectedRegions): 358 assert region.keys() == expectedRegion.keys() 359 for axisTag, support in region.items(): 360 assert support == pytest.approx(expectedRegion[axisTag]) 361 362 assert len(varStore.VarData) == 1 363 assert varStore.VarData[0].ItemCount == 2 364 365 assert hvar.AdvWidthMap is not None 366 advWithMap = hvar.AdvWidthMap.mapping 367 368 assert advWithMap[".notdef"] == advWithMap["space"] 369 varIdx = advWithMap[".notdef"] 370 # these glyphs have no metrics variations in the test font 371 assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == ( 372 [0] * varStore.VarData[0].VarRegionCount 373 ) 374 375 varIdx = advWithMap["hyphen"] 376 assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == expectedDeltas 377 378 def test_full_instance(self, varfont): 379 instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0}) 380 381 assert "HVAR" not in varfont 382 383 def test_partial_instance_keep_empty_table(self, varfont): 384 # Append an additional dummy axis to fvar, for which the current HVAR table 385 # in our test 'varfont' contains no variation data. 386 # Instancing the other two wght and wdth axes should leave HVAR table empty, 387 # to signal there are variations to the glyph's advance widths. 388 fvar = varfont["fvar"] 389 axis = _f_v_a_r.Axis() 390 axis.axisTag = "TEST" 391 fvar.axes.append(axis) 392 393 instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0}) 394 395 assert "HVAR" in varfont 396 397 varStore = varfont["HVAR"].table.VarStore 398 399 assert varStore.VarRegionList.RegionCount == 0 400 assert not varStore.VarRegionList.Region 401 assert varStore.VarRegionList.RegionAxisCount == 1 402 403 404class InstantiateItemVariationStoreTest(object): 405 def test_VarRegion_get_support(self): 406 axisOrder = ["wght", "wdth", "opsz"] 407 regionAxes = {"wdth": (-1.0, -1.0, 0.0), "wght": (0.0, 1.0, 1.0)} 408 region = builder.buildVarRegion(regionAxes, axisOrder) 409 410 assert len(region.VarRegionAxis) == 3 411 assert region.VarRegionAxis[2].PeakCoord == 0 412 413 fvarAxes = [SimpleNamespace(axisTag=axisTag) for axisTag in axisOrder] 414 415 assert region.get_support(fvarAxes) == regionAxes 416 417 @pytest.fixture 418 def varStore(self): 419 return builder.buildVarStore( 420 builder.buildVarRegionList( 421 [ 422 {"wght": (-1.0, -1.0, 0)}, 423 {"wght": (0, 0.5, 1.0)}, 424 {"wght": (0.5, 1.0, 1.0)}, 425 {"wdth": (-1.0, -1.0, 0)}, 426 {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, 427 {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, 428 {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, 429 ], 430 ["wght", "wdth"], 431 ), 432 [ 433 builder.buildVarData([0, 1, 2], [[100, 100, 100], [100, 100, 100]]), 434 builder.buildVarData( 435 [3, 4, 5, 6], [[100, 100, 100, 100], [100, 100, 100, 100]] 436 ), 437 ], 438 ) 439 440 @pytest.mark.parametrize( 441 "location, expected_deltas, num_regions", 442 [ 443 ({"wght": 0}, [[0, 0], [0, 0]], 1), 444 ({"wght": 0.25}, [[50, 50], [0, 0]], 1), 445 ({"wdth": 0}, [[0, 0], [0, 0]], 3), 446 ({"wdth": -0.75}, [[0, 0], [75, 75]], 3), 447 ({"wght": 0, "wdth": 0}, [[0, 0], [0, 0]], 0), 448 ({"wght": 0.25, "wdth": 0}, [[50, 50], [0, 0]], 0), 449 ({"wght": 0, "wdth": -0.75}, [[0, 0], [75, 75]], 0), 450 ], 451 ) 452 def test_instantiate_default_deltas( 453 self, varStore, fvarAxes, location, expected_deltas, num_regions 454 ): 455 defaultDeltas = instancer.instantiateItemVariationStore( 456 varStore, fvarAxes, location 457 ) 458 459 defaultDeltaArray = [] 460 for varidx, delta in sorted(defaultDeltas.items()): 461 if varidx == varStore.NO_VARIATION_INDEX: 462 continue 463 major, minor = varidx >> 16, varidx & 0xFFFF 464 if major == len(defaultDeltaArray): 465 defaultDeltaArray.append([]) 466 assert len(defaultDeltaArray[major]) == minor 467 defaultDeltaArray[major].append(delta) 468 469 assert defaultDeltaArray == expected_deltas 470 assert varStore.VarRegionList.RegionCount == num_regions 471 472 473class TupleVarStoreAdapterTest(object): 474 def test_instantiate(self): 475 regions = [ 476 {"wght": (-1.0, -1.0, 0)}, 477 {"wght": (0.0, 1.0, 1.0)}, 478 {"wdth": (-1.0, -1.0, 0)}, 479 {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, 480 {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, 481 ] 482 axisOrder = ["wght", "wdth"] 483 tupleVarData = [ 484 [ 485 TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]), 486 TupleVariation({"wght": (0.0, 1.0, 1.0)}, [30, 90]), 487 TupleVariation( 488 {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100] 489 ), 490 TupleVariation( 491 {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120] 492 ), 493 ], 494 [ 495 TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]), 496 TupleVariation( 497 {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55] 498 ), 499 TupleVariation( 500 {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75] 501 ), 502 ], 503 ] 504 adapter = instancer._TupleVarStoreAdapter( 505 regions, axisOrder, tupleVarData, itemCounts=[2, 2] 506 ) 507 508 defaultDeltaArray = adapter.instantiate({"wght": 0.5}) 509 510 assert defaultDeltaArray == [[15, 45], [0, 0]] 511 assert adapter.regions == [{"wdth": (-1.0, -1.0, 0)}] 512 assert adapter.tupleVarData == [ 513 [TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-30, -60])], 514 [TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-12, 8])], 515 ] 516 517 def test_rebuildRegions(self): 518 regions = [ 519 {"wght": (-1.0, -1.0, 0)}, 520 {"wght": (0.0, 1.0, 1.0)}, 521 {"wdth": (-1.0, -1.0, 0)}, 522 {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, 523 {"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, 524 ] 525 axisOrder = ["wght", "wdth"] 526 variations = [] 527 for region in regions: 528 variations.append(TupleVariation(region, [100])) 529 tupleVarData = [variations[:3], variations[3:]] 530 adapter = instancer._TupleVarStoreAdapter( 531 regions, axisOrder, tupleVarData, itemCounts=[1, 1] 532 ) 533 534 adapter.rebuildRegions() 535 536 assert adapter.regions == regions 537 538 del tupleVarData[0][2] 539 tupleVarData[1][0].axes = {"wght": (-1.0, -0.5, 0)} 540 tupleVarData[1][1].axes = {"wght": (0, 0.5, 1.0)} 541 542 adapter.rebuildRegions() 543 544 assert adapter.regions == [ 545 {"wght": (-1.0, -1.0, 0)}, 546 {"wght": (0.0, 1.0, 1.0)}, 547 {"wght": (-1.0, -0.5, 0)}, 548 {"wght": (0, 0.5, 1.0)}, 549 ] 550 551 def test_roundtrip(self, fvarAxes): 552 regions = [ 553 {"wght": (-1.0, -1.0, 0)}, 554 {"wght": (0, 0.5, 1.0)}, 555 {"wght": (0.5, 1.0, 1.0)}, 556 {"wdth": (-1.0, -1.0, 0)}, 557 {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, 558 {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, 559 {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, 560 ] 561 axisOrder = [axis.axisTag for axis in fvarAxes] 562 563 itemVarStore = builder.buildVarStore( 564 builder.buildVarRegionList(regions, axisOrder), 565 [ 566 builder.buildVarData( 567 [0, 1, 2, 4, 5, 6], 568 [[10, -20, 30, -40, 50, -60], [70, -80, 90, -100, 110, -120]], 569 ), 570 builder.buildVarData( 571 [3, 4, 5, 6], [[5, -15, 25, -35], [45, -55, 65, -75]] 572 ), 573 ], 574 ) 575 576 adapter = instancer._TupleVarStoreAdapter.fromItemVarStore( 577 itemVarStore, fvarAxes 578 ) 579 580 assert adapter.tupleVarData == [ 581 [ 582 TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]), 583 TupleVariation({"wght": (0, 0.5, 1.0)}, [-20, -80]), 584 TupleVariation({"wght": (0.5, 1.0, 1.0)}, [30, 90]), 585 TupleVariation( 586 {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100] 587 ), 588 TupleVariation( 589 {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [50, 110] 590 ), 591 TupleVariation( 592 {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120] 593 ), 594 ], 595 [ 596 TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]), 597 TupleVariation( 598 {"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55] 599 ), 600 TupleVariation( 601 {"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [25, 65] 602 ), 603 TupleVariation( 604 {"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75] 605 ), 606 ], 607 ] 608 assert adapter.itemCounts == [data.ItemCount for data in itemVarStore.VarData] 609 assert adapter.regions == regions 610 assert adapter.axisOrder == axisOrder 611 612 itemVarStore2 = adapter.asItemVarStore() 613 614 assert [ 615 reg.get_support(fvarAxes) for reg in itemVarStore2.VarRegionList.Region 616 ] == regions 617 618 assert itemVarStore2.VarDataCount == 2 619 assert itemVarStore2.VarData[0].VarRegionIndex == [0, 1, 2, 4, 5, 6] 620 assert itemVarStore2.VarData[0].Item == [ 621 [10, -20, 30, -40, 50, -60], 622 [70, -80, 90, -100, 110, -120], 623 ] 624 assert itemVarStore2.VarData[1].VarRegionIndex == [3, 4, 5, 6] 625 assert itemVarStore2.VarData[1].Item == [[5, -15, 25, -35], [45, -55, 65, -75]] 626 627 628def makeTTFont(glyphOrder, features): 629 font = ttLib.TTFont() 630 font.setGlyphOrder(glyphOrder) 631 addOpenTypeFeaturesFromString(font, features) 632 font["name"] = ttLib.newTable("name") 633 return font 634 635 636def _makeDSAxesDict(axes): 637 dsAxes = collections.OrderedDict() 638 for axisTag, axisValues in axes: 639 axis = designspaceLib.AxisDescriptor() 640 axis.name = axis.tag = axis.labelNames["en"] = axisTag 641 axis.minimum, axis.default, axis.maximum = axisValues 642 dsAxes[axis.tag] = axis 643 return dsAxes 644 645 646def makeVariableFont(masters, baseIndex, axes, masterLocations): 647 vf = deepcopy(masters[baseIndex]) 648 dsAxes = _makeDSAxesDict(axes) 649 fvar = varLib._add_fvar(vf, dsAxes, instances=()) 650 axisTags = [axis.axisTag for axis in fvar.axes] 651 normalizedLocs = [models.normalizeLocation(m, dict(axes)) for m in masterLocations] 652 model = models.VariationModel(normalizedLocs, axisOrder=axisTags) 653 varLib._merge_OTL(vf, model, masters, axisTags) 654 return vf 655 656 657def makeParametrizedVF(glyphOrder, features, values, increments): 658 # Create a test VF with given glyphs and parametrized OTL features. 659 # The VF is built from 9 masters (3 x 3 along wght and wdth), with 660 # locations hard-coded and base master at wght=400 and wdth=100. 661 # 'values' is a list of initial values that are interpolated in the 662 # 'features' string, and incremented for each subsequent master by the 663 # given 'increments' (list of 2-tuple) along the two axes. 664 assert values and len(values) == len(increments) 665 assert all(len(i) == 2 for i in increments) 666 masterLocations = [ 667 {"wght": 100, "wdth": 50}, 668 {"wght": 100, "wdth": 100}, 669 {"wght": 100, "wdth": 150}, 670 {"wght": 400, "wdth": 50}, 671 {"wght": 400, "wdth": 100}, # base master 672 {"wght": 400, "wdth": 150}, 673 {"wght": 700, "wdth": 50}, 674 {"wght": 700, "wdth": 100}, 675 {"wght": 700, "wdth": 150}, 676 ] 677 n = len(values) 678 values = list(values) 679 masters = [] 680 for _ in range(3): 681 for _ in range(3): 682 master = makeTTFont(glyphOrder, features=features % tuple(values)) 683 masters.append(master) 684 for i in range(n): 685 values[i] += increments[i][1] 686 for i in range(n): 687 values[i] += increments[i][0] 688 baseIndex = 4 689 axes = [("wght", (100, 400, 700)), ("wdth", (50, 100, 150))] 690 vf = makeVariableFont(masters, baseIndex, axes, masterLocations) 691 return vf 692 693 694@pytest.fixture 695def varfontGDEF(): 696 glyphOrder = [".notdef", "f", "i", "f_i"] 697 features = ( 698 "feature liga { sub f i by f_i;} liga;" 699 "table GDEF { LigatureCaretByPos f_i %d; } GDEF;" 700 ) 701 values = [100] 702 increments = [(+30, +10)] 703 return makeParametrizedVF(glyphOrder, features, values, increments) 704 705 706@pytest.fixture 707def varfontGPOS(): 708 glyphOrder = [".notdef", "V", "A"] 709 features = "feature kern { pos V A %d; } kern;" 710 values = [-80] 711 increments = [(-10, -5)] 712 return makeParametrizedVF(glyphOrder, features, values, increments) 713 714 715@pytest.fixture 716def varfontGPOS2(): 717 glyphOrder = [".notdef", "V", "A", "acutecomb"] 718 features = ( 719 "markClass [acutecomb] <anchor 150 -10> @TOP_MARKS;" 720 "feature mark {" 721 " pos base A <anchor %d 450> mark @TOP_MARKS;" 722 "} mark;" 723 "feature kern {" 724 " pos V A %d;" 725 "} kern;" 726 ) 727 values = [200, -80] 728 increments = [(+30, +10), (-10, -5)] 729 return makeParametrizedVF(glyphOrder, features, values, increments) 730 731 732class InstantiateOTLTest(object): 733 @pytest.mark.parametrize( 734 "location, expected", 735 [ 736 ({"wght": -1.0}, 110), # -60 737 ({"wght": 0}, 170), 738 ({"wght": 0.5}, 200), # +30 739 ({"wght": 1.0}, 230), # +60 740 ({"wdth": -1.0}, 160), # -10 741 ({"wdth": -0.3}, 167), # -3 742 ({"wdth": 0}, 170), 743 ({"wdth": 1.0}, 180), # +10 744 ], 745 ) 746 def test_pin_and_drop_axis_GDEF(self, varfontGDEF, location, expected): 747 vf = varfontGDEF 748 assert "GDEF" in vf 749 750 instancer.instantiateOTL(vf, location) 751 752 assert "GDEF" in vf 753 gdef = vf["GDEF"].table 754 assert gdef.Version == 0x00010003 755 assert gdef.VarStore 756 assert gdef.LigCaretList 757 caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0] 758 assert caretValue.Format == 3 759 assert hasattr(caretValue, "DeviceTable") 760 assert caretValue.DeviceTable.DeltaFormat == 0x8000 761 assert caretValue.Coordinate == expected 762 763 @pytest.mark.parametrize( 764 "location, expected", 765 [ 766 ({"wght": -1.0, "wdth": -1.0}, 100), # -60 - 10 767 ({"wght": -1.0, "wdth": 0.0}, 110), # -60 768 ({"wght": -1.0, "wdth": 1.0}, 120), # -60 + 10 769 ({"wght": 0.0, "wdth": -1.0}, 160), # -10 770 ({"wght": 0.0, "wdth": 0.0}, 170), 771 ({"wght": 0.0, "wdth": 1.0}, 180), # +10 772 ({"wght": 1.0, "wdth": -1.0}, 220), # +60 - 10 773 ({"wght": 1.0, "wdth": 0.0}, 230), # +60 774 ({"wght": 1.0, "wdth": 1.0}, 240), # +60 + 10 775 ], 776 ) 777 def test_full_instance_GDEF(self, varfontGDEF, location, expected): 778 vf = varfontGDEF 779 assert "GDEF" in vf 780 781 instancer.instantiateOTL(vf, location) 782 783 assert "GDEF" in vf 784 gdef = vf["GDEF"].table 785 assert gdef.Version == 0x00010000 786 assert not hasattr(gdef, "VarStore") 787 assert gdef.LigCaretList 788 caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0] 789 assert caretValue.Format == 1 790 assert not hasattr(caretValue, "DeviceTable") 791 assert caretValue.Coordinate == expected 792 793 @pytest.mark.parametrize( 794 "location, expected", 795 [ 796 ({"wght": -1.0}, -85), # +25 797 ({"wght": 0}, -110), 798 ({"wght": 1.0}, -135), # -25 799 ({"wdth": -1.0}, -105), # +5 800 ({"wdth": 0}, -110), 801 ({"wdth": 1.0}, -115), # -5 802 ], 803 ) 804 def test_pin_and_drop_axis_GPOS_kern(self, varfontGPOS, location, expected): 805 vf = varfontGPOS 806 assert "GDEF" in vf 807 assert "GPOS" in vf 808 809 instancer.instantiateOTL(vf, location) 810 811 gdef = vf["GDEF"].table 812 gpos = vf["GPOS"].table 813 assert gdef.Version == 0x00010003 814 assert gdef.VarStore 815 816 assert gpos.LookupList.Lookup[0].LookupType == 2 # PairPos 817 pairPos = gpos.LookupList.Lookup[0].SubTable[0] 818 valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 819 assert valueRec1.XAdvDevice 820 assert valueRec1.XAdvDevice.DeltaFormat == 0x8000 821 assert valueRec1.XAdvance == expected 822 823 @pytest.mark.parametrize( 824 "location, expected", 825 [ 826 ({"wght": -1.0, "wdth": -1.0}, -80), # +25 + 5 827 ({"wght": -1.0, "wdth": 0.0}, -85), # +25 828 ({"wght": -1.0, "wdth": 1.0}, -90), # +25 - 5 829 ({"wght": 0.0, "wdth": -1.0}, -105), # +5 830 ({"wght": 0.0, "wdth": 0.0}, -110), 831 ({"wght": 0.0, "wdth": 1.0}, -115), # -5 832 ({"wght": 1.0, "wdth": -1.0}, -130), # -25 + 5 833 ({"wght": 1.0, "wdth": 0.0}, -135), # -25 834 ({"wght": 1.0, "wdth": 1.0}, -140), # -25 - 5 835 ], 836 ) 837 def test_full_instance_GPOS_kern(self, varfontGPOS, location, expected): 838 vf = varfontGPOS 839 assert "GDEF" in vf 840 assert "GPOS" in vf 841 842 instancer.instantiateOTL(vf, location) 843 844 assert "GDEF" not in vf 845 gpos = vf["GPOS"].table 846 847 assert gpos.LookupList.Lookup[0].LookupType == 2 # PairPos 848 pairPos = gpos.LookupList.Lookup[0].SubTable[0] 849 valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 850 assert not hasattr(valueRec1, "XAdvDevice") 851 assert valueRec1.XAdvance == expected 852 853 @pytest.mark.parametrize( 854 "location, expected", 855 [ 856 ({"wght": -1.0}, (210, -85)), # -60, +25 857 ({"wght": 0}, (270, -110)), 858 ({"wght": 0.5}, (300, -122)), # +30, -12 859 ({"wght": 1.0}, (330, -135)), # +60, -25 860 ({"wdth": -1.0}, (260, -105)), # -10, +5 861 ({"wdth": -0.3}, (267, -108)), # -3, +2 862 ({"wdth": 0}, (270, -110)), 863 ({"wdth": 1.0}, (280, -115)), # +10, -5 864 ], 865 ) 866 def test_pin_and_drop_axis_GPOS_mark_and_kern( 867 self, varfontGPOS2, location, expected 868 ): 869 vf = varfontGPOS2 870 assert "GDEF" in vf 871 assert "GPOS" in vf 872 873 instancer.instantiateOTL(vf, location) 874 875 v1, v2 = expected 876 gdef = vf["GDEF"].table 877 gpos = vf["GPOS"].table 878 assert gdef.Version == 0x00010003 879 assert gdef.VarStore 880 assert gdef.GlyphClassDef 881 882 assert gpos.LookupList.Lookup[0].LookupType == 4 # MarkBasePos 883 markBasePos = gpos.LookupList.Lookup[0].SubTable[0] 884 baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0] 885 assert baseAnchor.Format == 3 886 assert baseAnchor.XDeviceTable 887 assert baseAnchor.XDeviceTable.DeltaFormat == 0x8000 888 assert not baseAnchor.YDeviceTable 889 assert baseAnchor.XCoordinate == v1 890 assert baseAnchor.YCoordinate == 450 891 892 assert gpos.LookupList.Lookup[1].LookupType == 2 # PairPos 893 pairPos = gpos.LookupList.Lookup[1].SubTable[0] 894 valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 895 assert valueRec1.XAdvDevice 896 assert valueRec1.XAdvDevice.DeltaFormat == 0x8000 897 assert valueRec1.XAdvance == v2 898 899 @pytest.mark.parametrize( 900 "location, expected", 901 [ 902 ({"wght": -1.0, "wdth": -1.0}, (200, -80)), # -60 - 10, +25 + 5 903 ({"wght": -1.0, "wdth": 0.0}, (210, -85)), # -60, +25 904 ({"wght": -1.0, "wdth": 1.0}, (220, -90)), # -60 + 10, +25 - 5 905 ({"wght": 0.0, "wdth": -1.0}, (260, -105)), # -10, +5 906 ({"wght": 0.0, "wdth": 0.0}, (270, -110)), 907 ({"wght": 0.0, "wdth": 1.0}, (280, -115)), # +10, -5 908 ({"wght": 1.0, "wdth": -1.0}, (320, -130)), # +60 - 10, -25 + 5 909 ({"wght": 1.0, "wdth": 0.0}, (330, -135)), # +60, -25 910 ({"wght": 1.0, "wdth": 1.0}, (340, -140)), # +60 + 10, -25 - 5 911 ], 912 ) 913 def test_full_instance_GPOS_mark_and_kern(self, varfontGPOS2, location, expected): 914 vf = varfontGPOS2 915 assert "GDEF" in vf 916 assert "GPOS" in vf 917 918 instancer.instantiateOTL(vf, location) 919 920 v1, v2 = expected 921 gdef = vf["GDEF"].table 922 gpos = vf["GPOS"].table 923 assert gdef.Version == 0x00010000 924 assert not hasattr(gdef, "VarStore") 925 assert gdef.GlyphClassDef 926 927 assert gpos.LookupList.Lookup[0].LookupType == 4 # MarkBasePos 928 markBasePos = gpos.LookupList.Lookup[0].SubTable[0] 929 baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0] 930 assert baseAnchor.Format == 1 931 assert not hasattr(baseAnchor, "XDeviceTable") 932 assert not hasattr(baseAnchor, "YDeviceTable") 933 assert baseAnchor.XCoordinate == v1 934 assert baseAnchor.YCoordinate == 450 935 936 assert gpos.LookupList.Lookup[1].LookupType == 2 # PairPos 937 pairPos = gpos.LookupList.Lookup[1].SubTable[0] 938 valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 939 assert not hasattr(valueRec1, "XAdvDevice") 940 assert valueRec1.XAdvance == v2 941 942 def test_GPOS_ValueRecord_XAdvDevice_wtihout_XAdvance(self): 943 # Test VF contains a PairPos adjustment in which the default instance 944 # has no XAdvance but there are deltas in XAdvDevice (VariationIndex). 945 vf = ttLib.TTFont() 946 vf.importXML(os.path.join(TESTDATA, "PartialInstancerTest4-VF.ttx")) 947 pairPos = vf["GPOS"].table.LookupList.Lookup[0].SubTable[0] 948 assert pairPos.ValueFormat1 == 0x40 949 valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 950 assert not hasattr(valueRec1, "XAdvance") 951 assert valueRec1.XAdvDevice.DeltaFormat == 0x8000 952 outer = valueRec1.XAdvDevice.StartSize 953 inner = valueRec1.XAdvDevice.EndSize 954 assert vf["GDEF"].table.VarStore.VarData[outer].Item[inner] == [-50] 955 956 # check that MutatorMerger for ValueRecord doesn't raise AttributeError 957 # when XAdvDevice is present but there's no corresponding XAdvance. 958 instancer.instantiateOTL(vf, {"wght": 0.5}) 959 960 pairPos = vf["GPOS"].table.LookupList.Lookup[0].SubTable[0] 961 assert pairPos.ValueFormat1 == 0x4 962 valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1 963 assert not hasattr(valueRec1, "XAdvDevice") 964 assert valueRec1.XAdvance == -25 965 966 967class InstantiateAvarTest(object): 968 @pytest.mark.parametrize("location", [{"wght": 0.0}, {"wdth": 0.0}]) 969 def test_pin_and_drop_axis(self, varfont, location): 970 instancer.instantiateAvar(varfont, location) 971 972 assert set(varfont["avar"].segments).isdisjoint(location) 973 974 def test_full_instance(self, varfont): 975 instancer.instantiateAvar(varfont, {"wght": 0.0, "wdth": 0.0}) 976 977 assert "avar" not in varfont 978 979 @staticmethod 980 def quantizeF2Dot14Floats(mapping): 981 return { 982 floatToFixedToFloat(k, 14): floatToFixedToFloat(v, 14) 983 for k, v in mapping.items() 984 } 985 986 # the following values come from NotoSans-VF.ttf 987 DFLT_WGHT_MAPPING = { 988 -1.0: -1.0, 989 -0.6667: -0.7969, 990 -0.3333: -0.5, 991 0: 0, 992 0.2: 0.18, 993 0.4: 0.38, 994 0.6: 0.61, 995 0.8: 0.79, 996 1.0: 1.0, 997 } 998 999 DFLT_WDTH_MAPPING = {-1.0: -1.0, -0.6667: -0.7, -0.3333: -0.36664, 0: 0, 1.0: 1.0} 1000 1001 @pytest.fixture 1002 def varfont(self): 1003 fvarAxes = ("wght", (100, 400, 900)), ("wdth", (62.5, 100, 100)) 1004 avarSegments = { 1005 "wght": self.quantizeF2Dot14Floats(self.DFLT_WGHT_MAPPING), 1006 "wdth": self.quantizeF2Dot14Floats(self.DFLT_WDTH_MAPPING), 1007 } 1008 varfont = ttLib.TTFont() 1009 varfont["name"] = ttLib.newTable("name") 1010 varLib._add_fvar(varfont, _makeDSAxesDict(fvarAxes), instances=()) 1011 avar = varfont["avar"] = ttLib.newTable("avar") 1012 avar.segments = avarSegments 1013 return varfont 1014 1015 @pytest.mark.parametrize( 1016 "axisLimits, expectedSegments", 1017 [ 1018 pytest.param( 1019 {"wght": (100, 900)}, 1020 {"wght": DFLT_WGHT_MAPPING, "wdth": DFLT_WDTH_MAPPING}, 1021 id="wght=100:900", 1022 ), 1023 pytest.param( 1024 {"wght": (400, 900)}, 1025 { 1026 "wght": { 1027 -1.0: -1.0, 1028 0: 0, 1029 0.2: 0.18, 1030 0.4: 0.38, 1031 0.6: 0.61, 1032 0.8: 0.79, 1033 1.0: 1.0, 1034 }, 1035 "wdth": DFLT_WDTH_MAPPING, 1036 }, 1037 id="wght=400:900", 1038 ), 1039 pytest.param( 1040 {"wght": (100, 400)}, 1041 { 1042 "wght": { 1043 -1.0: -1.0, 1044 -0.6667: -0.7969, 1045 -0.3333: -0.5, 1046 0: 0, 1047 1.0: 1.0, 1048 }, 1049 "wdth": DFLT_WDTH_MAPPING, 1050 }, 1051 id="wght=100:400", 1052 ), 1053 pytest.param( 1054 {"wght": (400, 800)}, 1055 { 1056 "wght": { 1057 -1.0: -1.0, 1058 0: 0, 1059 0.25: 0.22784, 1060 0.50006: 0.48103, 1061 0.75: 0.77214, 1062 1.0: 1.0, 1063 }, 1064 "wdth": DFLT_WDTH_MAPPING, 1065 }, 1066 id="wght=400:800", 1067 ), 1068 pytest.param( 1069 {"wght": (400, 700)}, 1070 { 1071 "wght": { 1072 -1.0: -1.0, 1073 0: 0, 1074 0.3334: 0.2951, 1075 0.66675: 0.623, 1076 1.0: 1.0, 1077 }, 1078 "wdth": DFLT_WDTH_MAPPING, 1079 }, 1080 id="wght=400:700", 1081 ), 1082 pytest.param( 1083 {"wght": (400, 600)}, 1084 { 1085 "wght": {-1.0: -1.0, 0: 0, 0.5: 0.47363, 1.0: 1.0}, 1086 "wdth": DFLT_WDTH_MAPPING, 1087 }, 1088 id="wght=400:600", 1089 ), 1090 pytest.param( 1091 {"wdth": (62.5, 100)}, 1092 { 1093 "wght": DFLT_WGHT_MAPPING, 1094 "wdth": { 1095 -1.0: -1.0, 1096 -0.6667: -0.7, 1097 -0.3333: -0.36664, 1098 0: 0, 1099 1.0: 1.0, 1100 }, 1101 }, 1102 id="wdth=62.5:100", 1103 ), 1104 pytest.param( 1105 {"wdth": (70, 100)}, 1106 { 1107 "wght": DFLT_WGHT_MAPPING, 1108 "wdth": { 1109 -1.0: -1.0, 1110 -0.8334: -0.85364, 1111 -0.4166: -0.44714, 1112 0: 0, 1113 1.0: 1.0, 1114 }, 1115 }, 1116 id="wdth=70:100", 1117 ), 1118 pytest.param( 1119 {"wdth": (75, 100)}, 1120 { 1121 "wght": DFLT_WGHT_MAPPING, 1122 "wdth": {-1.0: -1.0, -0.49994: -0.52374, 0: 0, 1.0: 1.0}, 1123 }, 1124 id="wdth=75:100", 1125 ), 1126 pytest.param( 1127 {"wdth": (77, 100)}, 1128 { 1129 "wght": DFLT_WGHT_MAPPING, 1130 "wdth": {-1.0: -1.0, -0.54346: -0.56696, 0: 0, 1.0: 1.0}, 1131 }, 1132 id="wdth=77:100", 1133 ), 1134 pytest.param( 1135 {"wdth": (87.5, 100)}, 1136 {"wght": DFLT_WGHT_MAPPING, "wdth": {-1.0: -1.0, 0: 0, 1.0: 1.0}}, 1137 id="wdth=87.5:100", 1138 ), 1139 ], 1140 ) 1141 def test_limit_axes(self, varfont, axisLimits, expectedSegments): 1142 instancer.instantiateAvar(varfont, axisLimits) 1143 1144 newSegments = varfont["avar"].segments 1145 expectedSegments = { 1146 axisTag: self.quantizeF2Dot14Floats(mapping) 1147 for axisTag, mapping in expectedSegments.items() 1148 } 1149 assert newSegments == expectedSegments 1150 1151 @pytest.mark.parametrize( 1152 "invalidSegmentMap", 1153 [ 1154 pytest.param({0.5: 0.5}, id="missing-required-maps-1"), 1155 pytest.param({-1.0: -1.0, 1.0: 1.0}, id="missing-required-maps-2"), 1156 pytest.param( 1157 {-1.0: -1.0, 0: 0, 0.5: 0.5, 0.6: 0.4, 1.0: 1.0}, 1158 id="retrograde-value-maps", 1159 ), 1160 ], 1161 ) 1162 def test_drop_invalid_segment_map(self, varfont, invalidSegmentMap, caplog): 1163 varfont["avar"].segments["wght"] = invalidSegmentMap 1164 1165 with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"): 1166 instancer.instantiateAvar(varfont, {"wght": (100, 400)}) 1167 1168 assert "Invalid avar" in caplog.text 1169 assert "wght" not in varfont["avar"].segments 1170 1171 def test_isValidAvarSegmentMap(self): 1172 assert instancer._isValidAvarSegmentMap("FOOO", {}) 1173 assert instancer._isValidAvarSegmentMap("FOOO", {-1.0: -1.0, 0: 0, 1.0: 1.0}) 1174 assert instancer._isValidAvarSegmentMap( 1175 "FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 1.0: 1.0} 1176 ) 1177 assert instancer._isValidAvarSegmentMap( 1178 "FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 0.7: 0.5, 1.0: 1.0} 1179 ) 1180 1181 1182class InstantiateFvarTest(object): 1183 @pytest.mark.parametrize( 1184 "location, instancesLeft", 1185 [ 1186 ( 1187 {"wght": 400.0}, 1188 ["Regular", "SemiCondensed", "Condensed", "ExtraCondensed"], 1189 ), 1190 ( 1191 {"wght": 100.0}, 1192 ["Thin", "SemiCondensed Thin", "Condensed Thin", "ExtraCondensed Thin"], 1193 ), 1194 ( 1195 {"wdth": 100.0}, 1196 [ 1197 "Thin", 1198 "ExtraLight", 1199 "Light", 1200 "Regular", 1201 "Medium", 1202 "SemiBold", 1203 "Bold", 1204 "ExtraBold", 1205 "Black", 1206 ], 1207 ), 1208 # no named instance at pinned location 1209 ({"wdth": 90.0}, []), 1210 ], 1211 ) 1212 def test_pin_and_drop_axis(self, varfont, location, instancesLeft): 1213 instancer.instantiateFvar(varfont, location) 1214 1215 fvar = varfont["fvar"] 1216 assert {a.axisTag for a in fvar.axes}.isdisjoint(location) 1217 1218 for instance in fvar.instances: 1219 assert set(instance.coordinates).isdisjoint(location) 1220 1221 name = varfont["name"] 1222 assert [ 1223 name.getDebugName(instance.subfamilyNameID) for instance in fvar.instances 1224 ] == instancesLeft 1225 1226 def test_full_instance(self, varfont): 1227 instancer.instantiateFvar(varfont, {"wght": 0.0, "wdth": 0.0}) 1228 1229 assert "fvar" not in varfont 1230 1231 1232class InstantiateSTATTest(object): 1233 @pytest.mark.parametrize( 1234 "location, expected", 1235 [ 1236 ({"wght": 400}, ["Regular", "Condensed", "Upright", "Normal"]), 1237 ({"wdth": 100}, ["Thin", "Regular", "Black", "Upright", "Normal"]), 1238 ], 1239 ) 1240 def test_pin_and_drop_axis(self, varfont, location, expected): 1241 instancer.instantiateSTAT(varfont, location) 1242 1243 stat = varfont["STAT"].table 1244 designAxes = {a.AxisTag for a in stat.DesignAxisRecord.Axis} 1245 1246 assert designAxes == {"wght", "wdth", "ital"} 1247 1248 name = varfont["name"] 1249 valueNames = [] 1250 for axisValueTable in stat.AxisValueArray.AxisValue: 1251 valueName = name.getDebugName(axisValueTable.ValueNameID) 1252 valueNames.append(valueName) 1253 1254 assert valueNames == expected 1255 1256 def test_skip_table_no_axis_value_array(self, varfont): 1257 varfont["STAT"].table.AxisValueArray = None 1258 1259 instancer.instantiateSTAT(varfont, {"wght": 100}) 1260 1261 assert len(varfont["STAT"].table.DesignAxisRecord.Axis) == 3 1262 assert varfont["STAT"].table.AxisValueArray is None 1263 1264 def test_skip_table_axis_value_array_empty(self, varfont): 1265 varfont["STAT"].table.AxisValueArray.AxisValue = [] 1266 1267 instancer.instantiateSTAT(varfont, {"wght": 100}) 1268 1269 assert len(varfont["STAT"].table.DesignAxisRecord.Axis) == 3 1270 assert not varfont["STAT"].table.AxisValueArray.AxisValue 1271 1272 def test_skip_table_no_design_axes(self, varfont): 1273 stat = otTables.STAT() 1274 stat.Version = 0x00010001 1275 stat.populateDefaults() 1276 assert not stat.DesignAxisRecord 1277 assert not stat.AxisValueArray 1278 varfont["STAT"].table = stat 1279 1280 instancer.instantiateSTAT(varfont, {"wght": 100}) 1281 1282 assert not varfont["STAT"].table.DesignAxisRecord 1283 1284 @staticmethod 1285 def get_STAT_axis_values(stat): 1286 axes = stat.DesignAxisRecord.Axis 1287 result = [] 1288 for axisValue in stat.AxisValueArray.AxisValue: 1289 if axisValue.Format == 1: 1290 result.append((axes[axisValue.AxisIndex].AxisTag, axisValue.Value)) 1291 elif axisValue.Format == 3: 1292 result.append( 1293 ( 1294 axes[axisValue.AxisIndex].AxisTag, 1295 (axisValue.Value, axisValue.LinkedValue), 1296 ) 1297 ) 1298 elif axisValue.Format == 2: 1299 result.append( 1300 ( 1301 axes[axisValue.AxisIndex].AxisTag, 1302 ( 1303 axisValue.RangeMinValue, 1304 axisValue.NominalValue, 1305 axisValue.RangeMaxValue, 1306 ), 1307 ) 1308 ) 1309 elif axisValue.Format == 4: 1310 result.append( 1311 tuple( 1312 (axes[rec.AxisIndex].AxisTag, rec.Value) 1313 for rec in axisValue.AxisValueRecord 1314 ) 1315 ) 1316 else: 1317 raise AssertionError(axisValue.Format) 1318 return result 1319 1320 def test_limit_axes(self, varfont2): 1321 instancer.instantiateSTAT(varfont2, {"wght": (400, 500), "wdth": (75, 100)}) 1322 1323 assert len(varfont2["STAT"].table.AxisValueArray.AxisValue) == 5 1324 assert self.get_STAT_axis_values(varfont2["STAT"].table) == [ 1325 ("wght", (400.0, 700.0)), 1326 ("wght", 500.0), 1327 ("wdth", (93.75, 100.0, 100.0)), 1328 ("wdth", (81.25, 87.5, 93.75)), 1329 ("wdth", (68.75, 75.0, 81.25)), 1330 ] 1331 1332 def test_limit_axis_value_format_4(self, varfont2): 1333 stat = varfont2["STAT"].table 1334 1335 axisValue = otTables.AxisValue() 1336 axisValue.Format = 4 1337 axisValue.AxisValueRecord = [] 1338 for tag, value in (("wght", 575), ("wdth", 90)): 1339 rec = otTables.AxisValueRecord() 1340 rec.AxisIndex = next( 1341 i for i, a in enumerate(stat.DesignAxisRecord.Axis) if a.AxisTag == tag 1342 ) 1343 rec.Value = value 1344 axisValue.AxisValueRecord.append(rec) 1345 stat.AxisValueArray.AxisValue.append(axisValue) 1346 1347 instancer.instantiateSTAT(varfont2, {"wght": (100, 600)}) 1348 1349 assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue 1350 1351 instancer.instantiateSTAT(varfont2, {"wdth": (62.5, 87.5)}) 1352 1353 assert axisValue not in varfont2["STAT"].table.AxisValueArray.AxisValue 1354 1355 def test_unknown_axis_value_format(self, varfont2, caplog): 1356 stat = varfont2["STAT"].table 1357 axisValue = otTables.AxisValue() 1358 axisValue.Format = 5 1359 stat.AxisValueArray.AxisValue.append(axisValue) 1360 1361 with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"): 1362 instancer.instantiateSTAT(varfont2, {"wght": 400}) 1363 1364 assert "Unknown AxisValue table format (5)" in caplog.text 1365 assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue 1366 1367 1368def test_setMacOverlapFlags(): 1369 flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND 1370 flagOverlapSimple = _g_l_y_f.flagOverlapSimple 1371 1372 glyf = ttLib.newTable("glyf") 1373 glyf.glyphOrder = ["a", "b", "c"] 1374 a = _g_l_y_f.Glyph() 1375 a.numberOfContours = 1 1376 a.flags = [0] 1377 b = _g_l_y_f.Glyph() 1378 b.numberOfContours = -1 1379 comp = _g_l_y_f.GlyphComponent() 1380 comp.flags = 0 1381 b.components = [comp] 1382 c = _g_l_y_f.Glyph() 1383 c.numberOfContours = 0 1384 glyf.glyphs = {"a": a, "b": b, "c": c} 1385 1386 instancer.setMacOverlapFlags(glyf) 1387 1388 assert a.flags[0] & flagOverlapSimple != 0 1389 assert b.components[0].flags & flagOverlapCompound != 0 1390 1391 1392@pytest.fixture 1393def varfont2(): 1394 f = ttLib.TTFont(recalcTimestamp=False) 1395 f.importXML(os.path.join(TESTDATA, "PartialInstancerTest2-VF.ttx")) 1396 return f 1397 1398 1399@pytest.fixture 1400def varfont3(): 1401 f = ttLib.TTFont(recalcTimestamp=False) 1402 f.importXML(os.path.join(TESTDATA, "PartialInstancerTest3-VF.ttx")) 1403 return f 1404 1405 1406def _dump_ttx(ttFont): 1407 # compile to temporary bytes stream, reload and dump to XML 1408 tmp = BytesIO() 1409 ttFont.save(tmp) 1410 tmp.seek(0) 1411 ttFont2 = ttLib.TTFont(tmp, recalcBBoxes=False, recalcTimestamp=False) 1412 s = StringIO() 1413 ttFont2.saveXML(s) 1414 return stripVariableItemsFromTTX(s.getvalue()) 1415 1416 1417def _get_expected_instance_ttx( 1418 name, *locations, overlap=instancer.OverlapMode.KEEP_AND_SET_FLAGS 1419): 1420 filename = f"{name}-VF-instance-{','.join(str(loc) for loc in locations)}" 1421 if overlap == instancer.OverlapMode.KEEP_AND_DONT_SET_FLAGS: 1422 filename += "-no-overlap-flags" 1423 elif overlap == instancer.OverlapMode.REMOVE: 1424 filename += "-no-overlaps" 1425 with open( 1426 os.path.join(TESTDATA, "test_results", f"{filename}.ttx"), 1427 "r", 1428 encoding="utf-8", 1429 ) as fp: 1430 return stripVariableItemsFromTTX(fp.read()) 1431 1432 1433class InstantiateVariableFontTest(object): 1434 @pytest.mark.parametrize( 1435 "wght, wdth", 1436 [(100, 100), (400, 100), (900, 100), (100, 62.5), (400, 62.5), (900, 62.5)], 1437 ) 1438 def test_multiple_instancing(self, varfont2, wght, wdth): 1439 partial = instancer.instantiateVariableFont(varfont2, {"wght": wght}) 1440 instance = instancer.instantiateVariableFont(partial, {"wdth": wdth}) 1441 1442 expected = _get_expected_instance_ttx("PartialInstancerTest2", wght, wdth) 1443 1444 assert _dump_ttx(instance) == expected 1445 1446 def test_default_instance(self, varfont2): 1447 instance = instancer.instantiateVariableFont( 1448 varfont2, {"wght": None, "wdth": None} 1449 ) 1450 1451 expected = _get_expected_instance_ttx("PartialInstancerTest2", 400, 100) 1452 1453 assert _dump_ttx(instance) == expected 1454 1455 @pytest.mark.parametrize( 1456 "overlap, wght", 1457 [ 1458 (instancer.OverlapMode.KEEP_AND_DONT_SET_FLAGS, 400), 1459 (instancer.OverlapMode.REMOVE, 400), 1460 (instancer.OverlapMode.REMOVE, 700), 1461 ], 1462 ) 1463 def test_overlap(self, varfont3, wght, overlap): 1464 pytest.importorskip("pathops") 1465 1466 location = {"wght": wght} 1467 1468 instance = instancer.instantiateVariableFont( 1469 varfont3, location, overlap=overlap 1470 ) 1471 1472 expected = _get_expected_instance_ttx( 1473 "PartialInstancerTest3", wght, overlap=overlap 1474 ) 1475 1476 assert _dump_ttx(instance) == expected 1477 1478 def test_singlepos(self): 1479 varfont = ttLib.TTFont(recalcTimestamp=False) 1480 varfont.importXML(os.path.join(TESTDATA, "SinglePos.ttx")) 1481 1482 location = {"wght": 280, "opsz": 18} 1483 1484 instance = instancer.instantiateVariableFont( 1485 varfont, location, 1486 ) 1487 1488 expected = _get_expected_instance_ttx( 1489 "SinglePos", *location.values() 1490 ) 1491 1492 assert _dump_ttx(instance) == expected 1493 1494 1495 1496def _conditionSetAsDict(conditionSet, axisOrder): 1497 result = {} 1498 for cond in conditionSet.ConditionTable: 1499 assert cond.Format == 1 1500 axisTag = axisOrder[cond.AxisIndex] 1501 result[axisTag] = (cond.FilterRangeMinValue, cond.FilterRangeMaxValue) 1502 return result 1503 1504 1505def _getSubstitutions(gsub, lookupIndices): 1506 subs = {} 1507 for index, lookup in enumerate(gsub.LookupList.Lookup): 1508 if index in lookupIndices: 1509 for subtable in lookup.SubTable: 1510 subs.update(subtable.mapping) 1511 return subs 1512 1513 1514def makeFeatureVarsFont(conditionalSubstitutions): 1515 axes = set() 1516 glyphs = set() 1517 for region, substitutions in conditionalSubstitutions: 1518 for box in region: 1519 axes.update(box.keys()) 1520 glyphs.update(*substitutions.items()) 1521 1522 varfont = ttLib.TTFont() 1523 varfont.setGlyphOrder(sorted(glyphs)) 1524 1525 fvar = varfont["fvar"] = ttLib.newTable("fvar") 1526 fvar.axes = [] 1527 for axisTag in sorted(axes): 1528 axis = _f_v_a_r.Axis() 1529 axis.axisTag = Tag(axisTag) 1530 fvar.axes.append(axis) 1531 1532 featureVars.addFeatureVariations(varfont, conditionalSubstitutions) 1533 1534 return varfont 1535 1536 1537class InstantiateFeatureVariationsTest(object): 1538 @pytest.mark.parametrize( 1539 "location, appliedSubs, expectedRecords", 1540 [ 1541 ({"wght": 0}, {}, [({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"})]), 1542 ( 1543 {"wght": -1.0}, 1544 {}, 1545 [ 1546 ({"cntr": (0, 0.25)}, {"uni0061": "uni0041"}), 1547 ({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"}), 1548 ], 1549 ), 1550 ( 1551 {"wght": 1.0}, 1552 {"uni0024": "uni0024.nostroke"}, 1553 [ 1554 ( 1555 {"cntr": (0.75, 1.0)}, 1556 {"uni0024": "uni0024.nostroke", "uni0041": "uni0061"}, 1557 ) 1558 ], 1559 ), 1560 ( 1561 {"cntr": 0}, 1562 {}, 1563 [ 1564 ({"wght": (-1.0, -0.45654)}, {"uni0061": "uni0041"}), 1565 ({"wght": (0.20886, 1.0)}, {"uni0024": "uni0024.nostroke"}), 1566 ], 1567 ), 1568 ( 1569 {"cntr": 1.0}, 1570 {"uni0041": "uni0061"}, 1571 [ 1572 ( 1573 {"wght": (0.20886, 1.0)}, 1574 {"uni0024": "uni0024.nostroke", "uni0041": "uni0061"}, 1575 ) 1576 ], 1577 ), 1578 ], 1579 ) 1580 def test_partial_instance(self, location, appliedSubs, expectedRecords): 1581 font = makeFeatureVarsFont( 1582 [ 1583 ([{"wght": (0.20886, 1.0)}], {"uni0024": "uni0024.nostroke"}), 1584 ([{"cntr": (0.75, 1.0)}], {"uni0041": "uni0061"}), 1585 ( 1586 [{"wght": (-1.0, -0.45654), "cntr": (0, 0.25)}], 1587 {"uni0061": "uni0041"}, 1588 ), 1589 ] 1590 ) 1591 1592 instancer.instantiateFeatureVariations(font, location) 1593 1594 gsub = font["GSUB"].table 1595 featureVariations = gsub.FeatureVariations 1596 1597 assert featureVariations.FeatureVariationCount == len(expectedRecords) 1598 1599 axisOrder = [a.axisTag for a in font["fvar"].axes if a.axisTag not in location] 1600 for i, (expectedConditionSet, expectedSubs) in enumerate(expectedRecords): 1601 rec = featureVariations.FeatureVariationRecord[i] 1602 conditionSet = _conditionSetAsDict(rec.ConditionSet, axisOrder) 1603 1604 assert conditionSet == expectedConditionSet 1605 1606 subsRecord = rec.FeatureTableSubstitution.SubstitutionRecord[0] 1607 lookupIndices = subsRecord.Feature.LookupListIndex 1608 substitutions = _getSubstitutions(gsub, lookupIndices) 1609 1610 assert substitutions == expectedSubs 1611 1612 appliedLookupIndices = gsub.FeatureList.FeatureRecord[0].Feature.LookupListIndex 1613 1614 assert _getSubstitutions(gsub, appliedLookupIndices) == appliedSubs 1615 1616 @pytest.mark.parametrize( 1617 "location, appliedSubs", 1618 [ 1619 ({"wght": 0, "cntr": 0}, None), 1620 ({"wght": -1.0, "cntr": 0}, {"uni0061": "uni0041"}), 1621 ({"wght": 1.0, "cntr": 0}, {"uni0024": "uni0024.nostroke"}), 1622 ({"wght": 0.0, "cntr": 1.0}, {"uni0041": "uni0061"}), 1623 ( 1624 {"wght": 1.0, "cntr": 1.0}, 1625 {"uni0041": "uni0061", "uni0024": "uni0024.nostroke"}, 1626 ), 1627 ({"wght": -1.0, "cntr": 0.3}, None), 1628 ], 1629 ) 1630 def test_full_instance(self, location, appliedSubs): 1631 font = makeFeatureVarsFont( 1632 [ 1633 ([{"wght": (0.20886, 1.0)}], {"uni0024": "uni0024.nostroke"}), 1634 ([{"cntr": (0.75, 1.0)}], {"uni0041": "uni0061"}), 1635 ( 1636 [{"wght": (-1.0, -0.45654), "cntr": (0, 0.25)}], 1637 {"uni0061": "uni0041"}, 1638 ), 1639 ] 1640 ) 1641 1642 instancer.instantiateFeatureVariations(font, location) 1643 1644 gsub = font["GSUB"].table 1645 assert not hasattr(gsub, "FeatureVariations") 1646 1647 if appliedSubs: 1648 lookupIndices = gsub.FeatureList.FeatureRecord[0].Feature.LookupListIndex 1649 assert _getSubstitutions(gsub, lookupIndices) == appliedSubs 1650 else: 1651 assert not gsub.FeatureList.FeatureRecord 1652 1653 def test_unsupported_condition_format(self, caplog): 1654 font = makeFeatureVarsFont( 1655 [ 1656 ( 1657 [{"wdth": (-1.0, -0.5), "wght": (0.5, 1.0)}], 1658 {"dollar": "dollar.nostroke"}, 1659 ) 1660 ] 1661 ) 1662 featureVariations = font["GSUB"].table.FeatureVariations 1663 rec1 = featureVariations.FeatureVariationRecord[0] 1664 assert len(rec1.ConditionSet.ConditionTable) == 2 1665 rec1.ConditionSet.ConditionTable[0].Format = 2 1666 1667 with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"): 1668 instancer.instantiateFeatureVariations(font, {"wdth": 0}) 1669 1670 assert ( 1671 "Condition table 0 of FeatureVariationRecord 0 " 1672 "has unsupported format (2); ignored" 1673 ) in caplog.text 1674 1675 # check that record with unsupported condition format (but whose other 1676 # conditions do not reference pinned axes) is kept as is 1677 featureVariations = font["GSUB"].table.FeatureVariations 1678 assert featureVariations.FeatureVariationRecord[0] is rec1 1679 assert len(rec1.ConditionSet.ConditionTable) == 2 1680 assert rec1.ConditionSet.ConditionTable[0].Format == 2 1681 1682 def test_GSUB_FeatureVariations_is_None(self, varfont2): 1683 varfont2["GSUB"].table.Version = 0x00010001 1684 varfont2["GSUB"].table.FeatureVariations = None 1685 tmp = BytesIO() 1686 varfont2.save(tmp) 1687 varfont = ttLib.TTFont(tmp) 1688 1689 # DO NOT raise an exception when the optional 'FeatureVariations' attribute is 1690 # present but is set to None (e.g. with GSUB 1.1); skip and do nothing. 1691 assert varfont["GSUB"].table.FeatureVariations is None 1692 instancer.instantiateFeatureVariations(varfont, {"wght": 400, "wdth": 100}) 1693 assert varfont["GSUB"].table.FeatureVariations is None 1694 1695 1696class LimitTupleVariationAxisRangesTest: 1697 def check_limit_single_var_axis_range(self, var, axisTag, axisRange, expected): 1698 result = instancer.limitTupleVariationAxisRange(var, axisTag, axisRange) 1699 print(result) 1700 1701 assert len(result) == len(expected) 1702 for v1, v2 in zip(result, expected): 1703 assert v1.coordinates == pytest.approx(v2.coordinates) 1704 assert v1.axes.keys() == v2.axes.keys() 1705 for k in v1.axes: 1706 p, q = v1.axes[k], v2.axes[k] 1707 assert p == pytest.approx(q) 1708 1709 @pytest.mark.parametrize( 1710 "var, axisTag, newMax, expected", 1711 [ 1712 ( 1713 TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), 1714 "wdth", 1715 0.5, 1716 [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])], 1717 ), 1718 ( 1719 TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), 1720 "wght", 1721 0.5, 1722 [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [50, 50])], 1723 ), 1724 ( 1725 TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), 1726 "wght", 1727 0.8, 1728 [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])], 1729 ), 1730 ( 1731 TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), 1732 "wght", 1733 1.0, 1734 [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])], 1735 ), 1736 (TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), "wght", 0.0, []), 1737 (TupleVariation({"wght": (0.5, 1.0, 1.0)}, [100, 100]), "wght", 0.4, []), 1738 ( 1739 TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]), 1740 "wght", 1741 0.5, 1742 [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])], 1743 ), 1744 ( 1745 TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]), 1746 "wght", 1747 0.4, 1748 [TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])], 1749 ), 1750 ( 1751 TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]), 1752 "wght", 1753 0.6, 1754 [TupleVariation({"wght": (0.0, 0.833334, 1.666667)}, [100, 100])], 1755 ), 1756 ( 1757 TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]), 1758 "wght", 1759 0.4, 1760 [ 1761 TupleVariation({"wght": (0.0, 0.5, 1.99994)}, [100, 100]), 1762 TupleVariation({"wght": (0.5, 1.0, 1.0)}, [8.33333, 8.33333]), 1763 ], 1764 ), 1765 ( 1766 TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]), 1767 "wght", 1768 0.5, 1769 [TupleVariation({"wght": (0.0, 0.4, 1.99994)}, [100, 100])], 1770 ), 1771 ( 1772 TupleVariation({"wght": (0.5, 0.5, 1.0)}, [100, 100]), 1773 "wght", 1774 0.5, 1775 [TupleVariation({"wght": (1.0, 1.0, 1.0)}, [100, 100])], 1776 ), 1777 ], 1778 ) 1779 def test_positive_var(self, var, axisTag, newMax, expected): 1780 axisRange = instancer.NormalizedAxisRange(0, newMax) 1781 self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected) 1782 1783 @pytest.mark.parametrize( 1784 "var, axisTag, newMin, expected", 1785 [ 1786 ( 1787 TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), 1788 "wdth", 1789 -0.5, 1790 [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])], 1791 ), 1792 ( 1793 TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), 1794 "wght", 1795 -0.5, 1796 [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [50, 50])], 1797 ), 1798 ( 1799 TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), 1800 "wght", 1801 -0.8, 1802 [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])], 1803 ), 1804 ( 1805 TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), 1806 "wght", 1807 -1.0, 1808 [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])], 1809 ), 1810 (TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), "wght", 0.0, []), 1811 ( 1812 TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [100, 100]), 1813 "wght", 1814 -0.4, 1815 [], 1816 ), 1817 ( 1818 TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]), 1819 "wght", 1820 -0.5, 1821 [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])], 1822 ), 1823 ( 1824 TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]), 1825 "wght", 1826 -0.4, 1827 [TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])], 1828 ), 1829 ( 1830 TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]), 1831 "wght", 1832 -0.6, 1833 [TupleVariation({"wght": (-1.666667, -0.833334, 0.0)}, [100, 100])], 1834 ), 1835 ( 1836 TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]), 1837 "wght", 1838 -0.4, 1839 [ 1840 TupleVariation({"wght": (-2.0, -0.5, -0.0)}, [100, 100]), 1841 TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [8.33333, 8.33333]), 1842 ], 1843 ), 1844 ( 1845 TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]), 1846 "wght", 1847 -0.5, 1848 [TupleVariation({"wght": (-2.0, -0.4, 0.0)}, [100, 100])], 1849 ), 1850 ( 1851 TupleVariation({"wght": (-1.0, -0.5, -0.5)}, [100, 100]), 1852 "wght", 1853 -0.5, 1854 [TupleVariation({"wght": (-1.0, -1.0, -1.0)}, [100, 100])], 1855 ), 1856 ], 1857 ) 1858 def test_negative_var(self, var, axisTag, newMin, expected): 1859 axisRange = instancer.NormalizedAxisRange(newMin, 0) 1860 self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected) 1861 1862 1863@pytest.mark.parametrize( 1864 "oldRange, newRange, expected", 1865 [ 1866 ((1.0, -1.0), (-1.0, 1.0), None), # invalid oldRange min > max 1867 ((0.6, 1.0), (0, 0.5), None), 1868 ((-1.0, -0.6), (-0.5, 0), None), 1869 ((0.4, 1.0), (0, 0.5), (0.8, 1.0)), 1870 ((-1.0, -0.4), (-0.5, 0), (-1.0, -0.8)), 1871 ((0.4, 1.0), (0, 0.4), (1.0, 1.0)), 1872 ((-1.0, -0.4), (-0.4, 0), (-1.0, -1.0)), 1873 ((-0.5, 0.5), (-0.4, 0.4), (-1.0, 1.0)), 1874 ((0, 1.0), (-1.0, 0), (0, 0)), # or None? 1875 ((-1.0, 0), (0, 1.0), (0, 0)), # or None? 1876 ], 1877) 1878def test_limitFeatureVariationConditionRange(oldRange, newRange, expected): 1879 condition = featureVars.buildConditionTable(0, *oldRange) 1880 1881 result = instancer._limitFeatureVariationConditionRange( 1882 condition, instancer.NormalizedAxisRange(*newRange) 1883 ) 1884 1885 assert result == expected 1886 1887 1888@pytest.mark.parametrize( 1889 "limits, expected", 1890 [ 1891 (["wght=400", "wdth=100"], {"wght": 400, "wdth": 100}), 1892 (["wght=400:900"], {"wght": (400, 900)}), 1893 (["slnt=11.4"], {"slnt": pytest.approx(11.399994)}), 1894 (["ABCD=drop"], {"ABCD": None}), 1895 ], 1896) 1897def test_parseLimits(limits, expected): 1898 assert instancer.parseLimits(limits) == expected 1899 1900 1901@pytest.mark.parametrize( 1902 "limits", [["abcde=123", "=0", "wght=:", "wght=1:", "wght=abcd", "wght=x:y"]] 1903) 1904def test_parseLimits_invalid(limits): 1905 with pytest.raises(ValueError, match="invalid location format"): 1906 instancer.parseLimits(limits) 1907 1908 1909def test_normalizeAxisLimits_tuple(varfont): 1910 normalized = instancer.normalizeAxisLimits(varfont, {"wght": (100, 400)}) 1911 assert normalized == {"wght": (-1.0, 0)} 1912 1913 1914def test_normalizeAxisLimits_unsupported_range(varfont): 1915 with pytest.raises(NotImplementedError, match="Unsupported range"): 1916 instancer.normalizeAxisLimits(varfont, {"wght": (401, 700)}) 1917 1918 1919def test_normalizeAxisLimits_no_avar(varfont): 1920 del varfont["avar"] 1921 1922 normalized = instancer.normalizeAxisLimits(varfont, {"wght": (400, 500)}) 1923 1924 assert normalized["wght"] == pytest.approx((0, 0.2), 1e-4) 1925 1926 1927def test_normalizeAxisLimits_missing_from_fvar(varfont): 1928 with pytest.raises(ValueError, match="not present in fvar"): 1929 instancer.normalizeAxisLimits(varfont, {"ZZZZ": 1000}) 1930 1931 1932def test_sanityCheckVariableTables(varfont): 1933 font = ttLib.TTFont() 1934 with pytest.raises(ValueError, match="Missing required table fvar"): 1935 instancer.sanityCheckVariableTables(font) 1936 1937 del varfont["glyf"] 1938 1939 with pytest.raises(ValueError, match="Can't have gvar without glyf"): 1940 instancer.sanityCheckVariableTables(varfont) 1941 1942 1943def test_main(varfont, tmpdir): 1944 fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf") 1945 varfont.save(fontfile) 1946 args = [fontfile, "wght=400"] 1947 1948 # exits without errors 1949 assert instancer.main(args) is None 1950 1951 1952def test_main_exit_nonexistent_file(capsys): 1953 with pytest.raises(SystemExit): 1954 instancer.main([""]) 1955 captured = capsys.readouterr() 1956 1957 assert "No such file ''" in captured.err 1958 1959 1960def test_main_exit_invalid_location(varfont, tmpdir, capsys): 1961 fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf") 1962 varfont.save(fontfile) 1963 1964 with pytest.raises(SystemExit): 1965 instancer.main([fontfile, "wght:100"]) 1966 captured = capsys.readouterr() 1967 1968 assert "invalid location format" in captured.err 1969 1970 1971def test_main_exit_multiple_limits(varfont, tmpdir, capsys): 1972 fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf") 1973 varfont.save(fontfile) 1974 1975 with pytest.raises(SystemExit): 1976 instancer.main([fontfile, "wght=400", "wght=90"]) 1977 captured = capsys.readouterr() 1978 1979 assert "Specified multiple limits for the same axis" in captured.err 1980 1981 1982def test_set_ribbi_bits(): 1983 varfont = ttLib.TTFont() 1984 varfont.importXML(os.path.join(TESTDATA, "STATInstancerTest.ttx")) 1985 1986 for location in [instance.coordinates for instance in varfont["fvar"].instances]: 1987 instance = instancer.instantiateVariableFont( 1988 varfont, location, updateFontNames=True 1989 ) 1990 name_id_2 = instance["name"].getDebugName(2) 1991 mac_style = instance["head"].macStyle 1992 fs_selection = instance["OS/2"].fsSelection & 0b1100001 # Just bits 0, 5, 6 1993 1994 if location["ital"] == 0: 1995 if location["wght"] == 700: 1996 assert name_id_2 == "Bold", location 1997 assert mac_style == 0b01, location 1998 assert fs_selection == 0b0100000, location 1999 else: 2000 assert name_id_2 == "Regular", location 2001 assert mac_style == 0b00, location 2002 assert fs_selection == 0b1000000, location 2003 else: 2004 if location["wght"] == 700: 2005 assert name_id_2 == "Bold Italic", location 2006 assert mac_style == 0b11, location 2007 assert fs_selection == 0b0100001, location 2008 else: 2009 assert name_id_2 == "Italic", location 2010 assert mac_style == 0b10, location 2011 assert fs_selection == 0b0000001, location 2012