1from fontTools.varLib.models import ( 2 normalizeLocation, 3 supportScalar, 4 VariationModel, 5 VariationModelError, 6) 7import pytest 8 9 10def test_normalizeLocation(): 11 axes = {"wght": (100, 400, 900)} 12 assert normalizeLocation({"wght": 400}, axes) == {"wght": 0.0} 13 assert normalizeLocation({"wght": 100}, axes) == {"wght": -1.0} 14 assert normalizeLocation({"wght": 900}, axes) == {"wght": 1.0} 15 assert normalizeLocation({"wght": 650}, axes) == {"wght": 0.5} 16 assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0} 17 assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0} 18 19 axes = {"wght": (0, 0, 1000)} 20 assert normalizeLocation({"wght": 0}, axes) == {"wght": 0.0} 21 assert normalizeLocation({"wght": -1}, axes) == {"wght": 0.0} 22 assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0} 23 assert normalizeLocation({"wght": 500}, axes) == {"wght": 0.5} 24 assert normalizeLocation({"wght": 1001}, axes) == {"wght": 1.0} 25 26 axes = {"wght": (0, 1000, 1000)} 27 assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0} 28 assert normalizeLocation({"wght": -1}, axes) == {"wght": -1.0} 29 assert normalizeLocation({"wght": 500}, axes) == {"wght": -0.5} 30 assert normalizeLocation({"wght": 1000}, axes) == {"wght": 0.0} 31 assert normalizeLocation({"wght": 1001}, axes) == {"wght": 0.0} 32 33 34@pytest.mark.parametrize( 35 "axes, location, expected", 36 [ 37 # lower != default != upper 38 ({"wght": (100, 400, 900)}, {"wght": 1000}, {"wght": 1.2}), 39 ({"wght": (100, 400, 900)}, {"wght": 900}, {"wght": 1.0}), 40 ({"wght": (100, 400, 900)}, {"wght": 650}, {"wght": 0.5}), 41 ({"wght": (100, 400, 900)}, {"wght": 400}, {"wght": 0.0}), 42 ({"wght": (100, 400, 900)}, {"wght": 250}, {"wght": -0.5}), 43 ({"wght": (100, 400, 900)}, {"wght": 100}, {"wght": -1.0}), 44 ({"wght": (100, 400, 900)}, {"wght": 25}, {"wght": -1.25}), 45 # lower == default != upper 46 ( 47 {"wght": (400, 400, 900), "wdth": (100, 100, 150)}, 48 {"wght": 1000, "wdth": 200}, 49 {"wght": 1.2, "wdth": 2.0}, 50 ), 51 ( 52 {"wght": (400, 400, 900), "wdth": (100, 100, 150)}, 53 {"wght": 25, "wdth": 25}, 54 {"wght": -0.75, "wdth": -1.5}, 55 ), 56 # lower != default == upper 57 ( 58 {"wght": (100, 400, 400), "wdth": (50, 100, 100)}, 59 {"wght": 700, "wdth": 150}, 60 {"wght": 1.0, "wdth": 1.0}, 61 ), 62 ( 63 {"wght": (100, 400, 400), "wdth": (50, 100, 100)}, 64 {"wght": -50, "wdth": 25}, 65 {"wght": -1.5, "wdth": -1.5}, 66 ), 67 # degenerate case with lower == default == upper, normalized location always 0 68 ({"wght": (400, 400, 400)}, {"wght": 100}, {"wght": 0.0}), 69 ({"wght": (400, 400, 400)}, {"wght": 400}, {"wght": 0.0}), 70 ({"wght": (400, 400, 400)}, {"wght": 700}, {"wght": 0.0}), 71 ], 72) 73def test_normalizeLocation_extrapolate(axes, location, expected): 74 assert normalizeLocation(location, axes, extrapolate=True) == expected 75 76 77def test_supportScalar(): 78 assert supportScalar({}, {}) == 1.0 79 assert supportScalar({"wght": 0.2}, {}) == 1.0 80 assert supportScalar({"wght": 0.2}, {"wght": (0, 2, 3)}) == 0.1 81 assert supportScalar({"wght": 2.5}, {"wght": (0, 2, 4)}) == 0.75 82 assert supportScalar({"wght": 3}, {"wght": (0, 2, 2)}) == 0.0 83 assert ( 84 supportScalar( 85 {"wght": 3}, 86 {"wght": (0, 2, 2)}, 87 extrapolate=True, 88 axisRanges={"wght": (0, 2)}, 89 ) 90 == 1.5 91 ) 92 assert ( 93 supportScalar( 94 {"wght": -1}, 95 {"wght": (0, 2, 2)}, 96 extrapolate=True, 97 axisRanges={"wght": (0, 2)}, 98 ) 99 == -0.5 100 ) 101 assert ( 102 supportScalar( 103 {"wght": 3}, 104 {"wght": (0, 1, 2)}, 105 extrapolate=True, 106 axisRanges={"wght": (0, 2)}, 107 ) 108 == -1.0 109 ) 110 assert ( 111 supportScalar( 112 {"wght": -1}, 113 {"wght": (0, 1, 2)}, 114 extrapolate=True, 115 axisRanges={"wght": (0, 2)}, 116 ) 117 == -1.0 118 ) 119 assert ( 120 supportScalar( 121 {"wght": 2}, 122 {"wght": (0, 0.75, 1)}, 123 extrapolate=True, 124 axisRanges={"wght": (0, 1)}, 125 ) 126 == -4.0 127 ) 128 with pytest.raises(TypeError): 129 supportScalar( 130 {"wght": 2}, {"wght": (0, 0.75, 1)}, extrapolate=True, axisRanges=None 131 ) 132 133 134def test_model_extrapolate(): 135 locations = [{}, {"a": 1}, {"b": 1}, {"a": 1, "b": 1}] 136 model = VariationModel(locations, extrapolate=True) 137 masterValues = [100, 200, 300, 400] 138 testLocsAndValues = [ 139 ({"a": -1, "b": -1}, -200), 140 ({"a": -1, "b": 0}, 0), 141 ({"a": -1, "b": 1}, 200), 142 ({"a": -1, "b": 2}, 400), 143 ({"a": 0, "b": -1}, -100), 144 ({"a": 0, "b": 0}, 100), 145 ({"a": 0, "b": 1}, 300), 146 ({"a": 0, "b": 2}, 500), 147 ({"a": 1, "b": -1}, 0), 148 ({"a": 1, "b": 0}, 200), 149 ({"a": 1, "b": 1}, 400), 150 ({"a": 1, "b": 2}, 600), 151 ({"a": 2, "b": -1}, 100), 152 ({"a": 2, "b": 0}, 300), 153 ({"a": 2, "b": 1}, 500), 154 ({"a": 2, "b": 2}, 700), 155 ] 156 for loc, expectedValue in testLocsAndValues: 157 assert expectedValue == model.interpolateFromMasters(loc, masterValues) 158 159 160@pytest.mark.parametrize( 161 "numLocations, numSamples", 162 [ 163 pytest.param(127, 509, marks=pytest.mark.slow), 164 (31, 251), 165 ], 166) 167def test_modeling_error(numLocations, numSamples): 168 # https://github.com/fonttools/fonttools/issues/2213 169 locations = [{"axis": float(i) / numLocations} for i in range(numLocations)] 170 masterValues = [100.0 if i else 0.0 for i in range(numLocations)] 171 172 model = VariationModel(locations) 173 174 for i in range(numSamples): 175 loc = {"axis": float(i) / numSamples} 176 scalars = model.getScalars(loc) 177 178 deltas_float = model.getDeltas(masterValues) 179 deltas_round = model.getDeltas(masterValues, round=round) 180 181 expected = model.interpolateFromDeltasAndScalars(deltas_float, scalars) 182 actual = model.interpolateFromDeltasAndScalars(deltas_round, scalars) 183 184 err = abs(actual - expected) 185 assert err <= 0.5, (i, err) 186 187 # This is how NOT to round deltas. 188 # deltas_late_round = [round(d) for d in deltas_float] 189 # bad = model.interpolateFromDeltasAndScalars(deltas_late_round, scalars) 190 # err_bad = abs(bad - expected) 191 # if err != err_bad: 192 # print("{:d} {:.2} {:.2}".format(i, err, err_bad)) 193 194 195locationsA = [{}, {"wght": 1}, {"wdth": 1}] 196locationsB = [{}, {"wght": 1}, {"wdth": 1}, {"wght": 1, "wdth": 1}] 197locationsC = [ 198 {}, 199 {"wght": 0.5}, 200 {"wght": 1}, 201 {"wdth": 1}, 202 {"wght": 1, "wdth": 1}, 203] 204 205 206class VariationModelTest(object): 207 @pytest.mark.parametrize( 208 "locations, axisOrder, sortedLocs, supports, deltaWeights", 209 [ 210 ( 211 [ 212 {"wght": 0.55, "wdth": 0.0}, 213 {"wght": -0.55, "wdth": 0.0}, 214 {"wght": -1.0, "wdth": 0.0}, 215 {"wght": 0.0, "wdth": 1.0}, 216 {"wght": 0.66, "wdth": 1.0}, 217 {"wght": 0.66, "wdth": 0.66}, 218 {"wght": 0.0, "wdth": 0.0}, 219 {"wght": 1.0, "wdth": 1.0}, 220 {"wght": 1.0, "wdth": 0.0}, 221 ], 222 ["wght"], 223 [ 224 {}, 225 {"wght": -0.55}, 226 {"wght": -1.0}, 227 {"wght": 0.55}, 228 {"wght": 1.0}, 229 {"wdth": 1.0}, 230 {"wdth": 1.0, "wght": 1.0}, 231 {"wdth": 1.0, "wght": 0.66}, 232 {"wdth": 0.66, "wght": 0.66}, 233 ], 234 [ 235 {}, 236 {"wght": (-1.0, -0.55, 0)}, 237 {"wght": (-1.0, -1.0, -0.55)}, 238 {"wght": (0, 0.55, 1.0)}, 239 {"wght": (0.55, 1.0, 1.0)}, 240 {"wdth": (0, 1.0, 1.0)}, 241 {"wdth": (0, 1.0, 1.0), "wght": (0, 1.0, 1.0)}, 242 {"wdth": (0, 1.0, 1.0), "wght": (0, 0.66, 1.0)}, 243 {"wdth": (0, 0.66, 1.0), "wght": (0, 0.66, 1.0)}, 244 ], 245 [ 246 {}, 247 {0: 1.0}, 248 {0: 1.0}, 249 {0: 1.0}, 250 {0: 1.0}, 251 {0: 1.0}, 252 {0: 1.0, 4: 1.0, 5: 1.0}, 253 { 254 0: 1.0, 255 3: 0.7555555555555555, 256 4: 0.24444444444444444, 257 5: 1.0, 258 6: 0.66, 259 }, 260 { 261 0: 1.0, 262 3: 0.7555555555555555, 263 4: 0.24444444444444444, 264 5: 0.66, 265 6: 0.43560000000000004, 266 7: 0.66, 267 }, 268 ], 269 ), 270 ( 271 [ 272 {}, 273 {"bar": 0.5}, 274 {"bar": 1.0}, 275 {"foo": 1.0}, 276 {"bar": 0.5, "foo": 1.0}, 277 {"bar": 1.0, "foo": 1.0}, 278 ], 279 None, 280 [ 281 {}, 282 {"bar": 0.5}, 283 {"bar": 1.0}, 284 {"foo": 1.0}, 285 {"bar": 0.5, "foo": 1.0}, 286 {"bar": 1.0, "foo": 1.0}, 287 ], 288 [ 289 {}, 290 {"bar": (0, 0.5, 1.0)}, 291 {"bar": (0.5, 1.0, 1.0)}, 292 {"foo": (0, 1.0, 1.0)}, 293 {"bar": (0, 0.5, 1.0), "foo": (0, 1.0, 1.0)}, 294 {"bar": (0.5, 1.0, 1.0), "foo": (0, 1.0, 1.0)}, 295 ], 296 [ 297 {}, 298 {0: 1.0}, 299 {0: 1.0}, 300 {0: 1.0}, 301 {0: 1.0, 1: 1.0, 3: 1.0}, 302 {0: 1.0, 2: 1.0, 3: 1.0}, 303 ], 304 ), 305 ( 306 [ 307 {}, 308 {"foo": 0.25}, 309 {"foo": 0.5}, 310 {"foo": 0.75}, 311 {"foo": 1.0}, 312 {"bar": 0.25}, 313 {"bar": 0.75}, 314 {"bar": 1.0}, 315 ], 316 None, 317 [ 318 {}, 319 {"bar": 0.25}, 320 {"bar": 0.75}, 321 {"bar": 1.0}, 322 {"foo": 0.25}, 323 {"foo": 0.5}, 324 {"foo": 0.75}, 325 {"foo": 1.0}, 326 ], 327 [ 328 {}, 329 {"bar": (0.0, 0.25, 1.0)}, 330 {"bar": (0.25, 0.75, 1.0)}, 331 {"bar": (0.75, 1.0, 1.0)}, 332 {"foo": (0.0, 0.25, 1.0)}, 333 {"foo": (0.25, 0.5, 1.0)}, 334 {"foo": (0.5, 0.75, 1.0)}, 335 {"foo": (0.75, 1.0, 1.0)}, 336 ], 337 [ 338 {}, 339 {0: 1.0}, 340 {0: 1.0, 1: 0.3333333333333333}, 341 {0: 1.0}, 342 {0: 1.0}, 343 {0: 1.0, 4: 0.6666666666666666}, 344 {0: 1.0, 4: 0.3333333333333333, 5: 0.5}, 345 {0: 1.0}, 346 ], 347 ), 348 ( 349 [ 350 {}, 351 {"foo": 0.25}, 352 {"foo": 0.5}, 353 {"foo": 0.75}, 354 {"foo": 1.0}, 355 {"bar": 0.25}, 356 {"bar": 0.75}, 357 {"bar": 1.0}, 358 ], 359 None, 360 [ 361 {}, 362 {"bar": 0.25}, 363 {"bar": 0.75}, 364 {"bar": 1.0}, 365 {"foo": 0.25}, 366 {"foo": 0.5}, 367 {"foo": 0.75}, 368 {"foo": 1.0}, 369 ], 370 [ 371 {}, 372 {"bar": (0, 0.25, 1.0)}, 373 {"bar": (0.25, 0.75, 1.0)}, 374 {"bar": (0.75, 1.0, 1.0)}, 375 {"foo": (0, 0.25, 1.0)}, 376 {"foo": (0.25, 0.5, 1.0)}, 377 {"foo": (0.5, 0.75, 1.0)}, 378 {"foo": (0.75, 1.0, 1.0)}, 379 ], 380 [ 381 {}, 382 {0: 1.0}, 383 {0: 1.0, 1: 0.3333333333333333}, 384 {0: 1.0}, 385 {0: 1.0}, 386 {0: 1.0, 4: 0.6666666666666666}, 387 {0: 1.0, 4: 0.3333333333333333, 5: 0.5}, 388 {0: 1.0}, 389 ], 390 ), 391 ], 392 ) 393 def test_init(self, locations, axisOrder, sortedLocs, supports, deltaWeights): 394 model = VariationModel(locations, axisOrder=axisOrder) 395 396 assert model.locations == sortedLocs 397 assert model.supports == supports 398 assert model.deltaWeights == deltaWeights 399 400 def test_init_duplicate_locations(self): 401 with pytest.raises(VariationModelError, match="Locations must be unique."): 402 VariationModel( 403 [ 404 {"foo": 0.0, "bar": 0.0}, 405 {"foo": 1.0, "bar": 1.0}, 406 {"bar": 1.0, "foo": 1.0}, 407 ] 408 ) 409 410 @pytest.mark.parametrize( 411 "locations, axisOrder, masterValues, instanceLocation, expectedValue, masterScalars", 412 [ 413 ( 414 [ 415 {}, 416 {"axis_A": 1.0}, 417 {"axis_B": 1.0}, 418 {"axis_A": 1.0, "axis_B": 1.0}, 419 {"axis_A": 0.5, "axis_B": 1.0}, 420 {"axis_A": 1.0, "axis_B": 0.5}, 421 ], 422 ["axis_A", "axis_B"], 423 [ 424 0, 425 10, 426 20, 427 70, 428 50, 429 60, 430 ], 431 { 432 "axis_A": 0.5, 433 "axis_B": 0.5, 434 }, 435 37.5, 436 [0.25, 0.0, 0.0, -0.25, 0.5, 0.5], 437 ), 438 ], 439 ) 440 def test_interpolation( 441 self, 442 locations, 443 axisOrder, 444 masterValues, 445 instanceLocation, 446 expectedValue, 447 masterScalars, 448 ): 449 model = VariationModel(locations, axisOrder=axisOrder) 450 451 interpolatedValue = model.interpolateFromMasters(instanceLocation, masterValues) 452 assert interpolatedValue == expectedValue 453 454 assert masterScalars == model.getMasterScalars(instanceLocation) 455 456 assert model.interpolateFromValuesAndScalars( 457 masterValues, masterScalars 458 ) == pytest.approx(interpolatedValue) 459 460 @pytest.mark.parametrize( 461 "masterLocations, location, expected", 462 [ 463 (locationsA, {"wght": 0, "wdth": 0}, [1, 0, 0]), 464 ( 465 locationsA, 466 {"wght": 0.5, "wdth": 0}, 467 [0.5, 0.5, 0], 468 ), 469 (locationsA, {"wght": 1, "wdth": 0}, [0, 1, 0]), 470 ( 471 locationsA, 472 {"wght": 0, "wdth": 0.5}, 473 [0.5, 0, 0.5], 474 ), 475 (locationsA, {"wght": 0, "wdth": 1}, [0, 0, 1]), 476 (locationsA, {"wght": 1, "wdth": 1}, [-1, 1, 1]), 477 ( 478 locationsA, 479 {"wght": 0.5, "wdth": 0.5}, 480 [0, 0.5, 0.5], 481 ), 482 ( 483 locationsA, 484 {"wght": 0.75, "wdth": 0.75}, 485 [-0.5, 0.75, 0.75], 486 ), 487 ( 488 locationsB, 489 {"wght": 1, "wdth": 1}, 490 [0, 0, 0, 1], 491 ), 492 ( 493 locationsB, 494 {"wght": 0.5, "wdth": 0}, 495 [0.5, 0.5, 0, 0], 496 ), 497 ( 498 locationsB, 499 {"wght": 1, "wdth": 0.5}, 500 [0, 0.5, 0, 0.5], 501 ), 502 ( 503 locationsB, 504 {"wght": 0.5, "wdth": 0.5}, 505 [0.25, 0.25, 0.25, 0.25], 506 ), 507 ( 508 locationsC, 509 {"wght": 0.5, "wdth": 0}, 510 [0, 1, 0, 0, 0], 511 ), 512 ( 513 locationsC, 514 {"wght": 0.25, "wdth": 0}, 515 [0.5, 0.5, 0, 0, 0], 516 ), 517 ( 518 locationsC, 519 {"wght": 0.75, "wdth": 0}, 520 [0, 0.5, 0.5, 0, 0], 521 ), 522 ( 523 locationsC, 524 {"wght": 0.5, "wdth": 1}, 525 [-0.5, 1, -0.5, 0.5, 0.5], 526 ), 527 ( 528 locationsC, 529 {"wght": 0.75, "wdth": 1}, 530 [-0.25, 0.5, -0.25, 0.25, 0.75], 531 ), 532 ], 533 ) 534 def test_getMasterScalars(self, masterLocations, location, expected): 535 model = VariationModel(masterLocations) 536 assert model.getMasterScalars(location) == expected 537