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