1from copy import deepcopy 2import string 3from fontTools.colorLib.builder import LayerListBuilder, buildCOLR, buildClipList 4from fontTools.misc.testTools import getXML 5from fontTools.varLib.merger import COLRVariationMerger 6from fontTools.varLib.models import VariationModel 7from fontTools.ttLib import TTFont 8from fontTools.ttLib.tables import otTables as ot 9from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter 10from io import BytesIO 11import pytest 12 13 14NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX 15 16 17def dump_xml(table, ttFont=None): 18 xml = getXML(table.toXML, ttFont) 19 print("[") 20 for line in xml: 21 print(f" {line!r},") 22 print("]") 23 return xml 24 25 26def compile_decompile(table, ttFont): 27 writer = OTTableWriter(tableTag="COLR") 28 # compile itself may modify a table, safer to copy it first 29 table = deepcopy(table) 30 table.compile(writer, ttFont) 31 data = writer.getAllData() 32 33 reader = OTTableReader(data, tableTag="COLR") 34 table2 = table.__class__() 35 table2.decompile(reader, ttFont) 36 37 return table2 38 39 40@pytest.fixture 41def ttFont(): 42 font = TTFont() 43 font.setGlyphOrder([".notdef"] + list(string.ascii_letters)) 44 return font 45 46 47def build_paint(data): 48 return LayerListBuilder().buildPaint(data) 49 50 51class COLRVariationMergerTest: 52 @pytest.mark.parametrize( 53 "paints, expected_xml, expected_varIdxes", 54 [ 55 pytest.param( 56 [ 57 { 58 "Format": int(ot.PaintFormat.PaintSolid), 59 "PaletteIndex": 0, 60 "Alpha": 1.0, 61 }, 62 { 63 "Format": int(ot.PaintFormat.PaintSolid), 64 "PaletteIndex": 0, 65 "Alpha": 1.0, 66 }, 67 ], 68 [ 69 '<Paint Format="2"><!-- PaintSolid -->', 70 ' <PaletteIndex value="0"/>', 71 ' <Alpha value="1.0"/>', 72 "</Paint>", 73 ], 74 [], 75 id="solid-same", 76 ), 77 pytest.param( 78 [ 79 { 80 "Format": int(ot.PaintFormat.PaintSolid), 81 "PaletteIndex": 0, 82 "Alpha": 1.0, 83 }, 84 { 85 "Format": int(ot.PaintFormat.PaintSolid), 86 "PaletteIndex": 0, 87 "Alpha": 0.5, 88 }, 89 ], 90 [ 91 '<Paint Format="3"><!-- PaintVarSolid -->', 92 ' <PaletteIndex value="0"/>', 93 ' <Alpha value="1.0"/>', 94 ' <VarIndexBase value="0"/>', 95 "</Paint>", 96 ], 97 [0], 98 id="solid-alpha", 99 ), 100 pytest.param( 101 [ 102 { 103 "Format": int(ot.PaintFormat.PaintLinearGradient), 104 "ColorLine": { 105 "Extend": int(ot.ExtendMode.PAD), 106 "ColorStop": [ 107 {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, 108 {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, 109 ], 110 }, 111 "x0": 0, 112 "y0": 0, 113 "x1": 1, 114 "y1": 1, 115 "x2": 2, 116 "y2": 2, 117 }, 118 { 119 "Format": int(ot.PaintFormat.PaintLinearGradient), 120 "ColorLine": { 121 "Extend": int(ot.ExtendMode.PAD), 122 "ColorStop": [ 123 {"StopOffset": 0.1, "PaletteIndex": 0, "Alpha": 1.0}, 124 {"StopOffset": 0.9, "PaletteIndex": 1, "Alpha": 1.0}, 125 ], 126 }, 127 "x0": 0, 128 "y0": 0, 129 "x1": 1, 130 "y1": 1, 131 "x2": 2, 132 "y2": 2, 133 }, 134 ], 135 [ 136 '<Paint Format="5"><!-- PaintVarLinearGradient -->', 137 " <ColorLine>", 138 ' <Extend value="pad"/>', 139 " <!-- StopCount=2 -->", 140 ' <ColorStop index="0">', 141 ' <StopOffset value="0.0"/>', 142 ' <PaletteIndex value="0"/>', 143 ' <Alpha value="1.0"/>', 144 ' <VarIndexBase value="0"/>', 145 " </ColorStop>", 146 ' <ColorStop index="1">', 147 ' <StopOffset value="1.0"/>', 148 ' <PaletteIndex value="1"/>', 149 ' <Alpha value="1.0"/>', 150 ' <VarIndexBase value="2"/>', 151 " </ColorStop>", 152 " </ColorLine>", 153 ' <x0 value="0"/>', 154 ' <y0 value="0"/>', 155 ' <x1 value="1"/>', 156 ' <y1 value="1"/>', 157 ' <x2 value="2"/>', 158 ' <y2 value="2"/>', 159 " <VarIndexBase/>", 160 "</Paint>", 161 ], 162 [0, NO_VARIATION_INDEX, 1, NO_VARIATION_INDEX], 163 id="linear_grad-stop-offsets", 164 ), 165 pytest.param( 166 [ 167 { 168 "Format": int(ot.PaintFormat.PaintLinearGradient), 169 "ColorLine": { 170 "Extend": int(ot.ExtendMode.PAD), 171 "ColorStop": [ 172 {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, 173 {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, 174 ], 175 }, 176 "x0": 0, 177 "y0": 0, 178 "x1": 1, 179 "y1": 1, 180 "x2": 2, 181 "y2": 2, 182 }, 183 { 184 "Format": int(ot.PaintFormat.PaintLinearGradient), 185 "ColorLine": { 186 "Extend": int(ot.ExtendMode.PAD), 187 "ColorStop": [ 188 {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 0.5}, 189 {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, 190 ], 191 }, 192 "x0": 0, 193 "y0": 0, 194 "x1": 1, 195 "y1": 1, 196 "x2": 2, 197 "y2": 2, 198 }, 199 ], 200 [ 201 '<Paint Format="5"><!-- PaintVarLinearGradient -->', 202 " <ColorLine>", 203 ' <Extend value="pad"/>', 204 " <!-- StopCount=2 -->", 205 ' <ColorStop index="0">', 206 ' <StopOffset value="0.0"/>', 207 ' <PaletteIndex value="0"/>', 208 ' <Alpha value="1.0"/>', 209 ' <VarIndexBase value="0"/>', 210 " </ColorStop>", 211 ' <ColorStop index="1">', 212 ' <StopOffset value="1.0"/>', 213 ' <PaletteIndex value="1"/>', 214 ' <Alpha value="1.0"/>', 215 " <VarIndexBase/>", 216 " </ColorStop>", 217 " </ColorLine>", 218 ' <x0 value="0"/>', 219 ' <y0 value="0"/>', 220 ' <x1 value="1"/>', 221 ' <y1 value="1"/>', 222 ' <x2 value="2"/>', 223 ' <y2 value="2"/>', 224 " <VarIndexBase/>", 225 "</Paint>", 226 ], 227 [NO_VARIATION_INDEX, 0], 228 id="linear_grad-stop[0].alpha", 229 ), 230 pytest.param( 231 [ 232 { 233 "Format": int(ot.PaintFormat.PaintLinearGradient), 234 "ColorLine": { 235 "Extend": int(ot.ExtendMode.PAD), 236 "ColorStop": [ 237 {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, 238 {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, 239 ], 240 }, 241 "x0": 0, 242 "y0": 0, 243 "x1": 1, 244 "y1": 1, 245 "x2": 2, 246 "y2": 2, 247 }, 248 { 249 "Format": int(ot.PaintFormat.PaintLinearGradient), 250 "ColorLine": { 251 "Extend": int(ot.ExtendMode.PAD), 252 "ColorStop": [ 253 {"StopOffset": -0.5, "PaletteIndex": 0, "Alpha": 1.0}, 254 {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, 255 ], 256 }, 257 "x0": 0, 258 "y0": 0, 259 "x1": 1, 260 "y1": 1, 261 "x2": 2, 262 "y2": -200, 263 }, 264 ], 265 [ 266 '<Paint Format="5"><!-- PaintVarLinearGradient -->', 267 " <ColorLine>", 268 ' <Extend value="pad"/>', 269 " <!-- StopCount=2 -->", 270 ' <ColorStop index="0">', 271 ' <StopOffset value="0.0"/>', 272 ' <PaletteIndex value="0"/>', 273 ' <Alpha value="1.0"/>', 274 ' <VarIndexBase value="0"/>', 275 " </ColorStop>", 276 ' <ColorStop index="1">', 277 ' <StopOffset value="1.0"/>', 278 ' <PaletteIndex value="1"/>', 279 ' <Alpha value="1.0"/>', 280 " <VarIndexBase/>", 281 " </ColorStop>", 282 " </ColorLine>", 283 ' <x0 value="0"/>', 284 ' <y0 value="0"/>', 285 ' <x1 value="1"/>', 286 ' <y1 value="1"/>', 287 ' <x2 value="2"/>', 288 ' <y2 value="2"/>', 289 ' <VarIndexBase value="1"/>', 290 "</Paint>", 291 ], 292 [ 293 0, 294 NO_VARIATION_INDEX, 295 NO_VARIATION_INDEX, 296 NO_VARIATION_INDEX, 297 NO_VARIATION_INDEX, 298 NO_VARIATION_INDEX, 299 1, 300 ], 301 id="linear_grad-stop[0].offset-y2", 302 ), 303 pytest.param( 304 [ 305 { 306 "Format": int(ot.PaintFormat.PaintRadialGradient), 307 "ColorLine": { 308 "Extend": int(ot.ExtendMode.PAD), 309 "ColorStop": [ 310 {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, 311 {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, 312 ], 313 }, 314 "x0": 0, 315 "y0": 0, 316 "r0": 0, 317 "x1": 1, 318 "y1": 1, 319 "r1": 1, 320 }, 321 { 322 "Format": int(ot.PaintFormat.PaintRadialGradient), 323 "ColorLine": { 324 "Extend": int(ot.ExtendMode.PAD), 325 "ColorStop": [ 326 {"StopOffset": 0.1, "PaletteIndex": 0, "Alpha": 0.6}, 327 {"StopOffset": 0.9, "PaletteIndex": 1, "Alpha": 0.7}, 328 ], 329 }, 330 "x0": -1, 331 "y0": -2, 332 "r0": 3, 333 "x1": -4, 334 "y1": -5, 335 "r1": 6, 336 }, 337 ], 338 [ 339 '<Paint Format="7"><!-- PaintVarRadialGradient -->', 340 " <ColorLine>", 341 ' <Extend value="pad"/>', 342 " <!-- StopCount=2 -->", 343 ' <ColorStop index="0">', 344 ' <StopOffset value="0.0"/>', 345 ' <PaletteIndex value="0"/>', 346 ' <Alpha value="1.0"/>', 347 ' <VarIndexBase value="0"/>', 348 " </ColorStop>", 349 ' <ColorStop index="1">', 350 ' <StopOffset value="1.0"/>', 351 ' <PaletteIndex value="1"/>', 352 ' <Alpha value="1.0"/>', 353 ' <VarIndexBase value="2"/>', 354 " </ColorStop>", 355 " </ColorLine>", 356 ' <x0 value="0"/>', 357 ' <y0 value="0"/>', 358 ' <r0 value="0"/>', 359 ' <x1 value="1"/>', 360 ' <y1 value="1"/>', 361 ' <r1 value="1"/>', 362 ' <VarIndexBase value="4"/>', 363 "</Paint>", 364 ], 365 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 366 id="radial_grad-all-different", 367 ), 368 pytest.param( 369 [ 370 { 371 "Format": int(ot.PaintFormat.PaintSweepGradient), 372 "ColorLine": { 373 "Extend": int(ot.ExtendMode.REPEAT), 374 "ColorStop": [ 375 {"StopOffset": 0.4, "PaletteIndex": 0, "Alpha": 1.0}, 376 {"StopOffset": 0.6, "PaletteIndex": 1, "Alpha": 1.0}, 377 ], 378 }, 379 "centerX": 0, 380 "centerY": 0, 381 "startAngle": 0, 382 "endAngle": 180.0, 383 }, 384 { 385 "Format": int(ot.PaintFormat.PaintSweepGradient), 386 "ColorLine": { 387 "Extend": int(ot.ExtendMode.REPEAT), 388 "ColorStop": [ 389 {"StopOffset": 0.4, "PaletteIndex": 0, "Alpha": 1.0}, 390 {"StopOffset": 0.6, "PaletteIndex": 1, "Alpha": 1.0}, 391 ], 392 }, 393 "centerX": 0, 394 "centerY": 0, 395 "startAngle": 90.0, 396 "endAngle": 180.0, 397 }, 398 ], 399 [ 400 '<Paint Format="9"><!-- PaintVarSweepGradient -->', 401 " <ColorLine>", 402 ' <Extend value="repeat"/>', 403 " <!-- StopCount=2 -->", 404 ' <ColorStop index="0">', 405 ' <StopOffset value="0.4"/>', 406 ' <PaletteIndex value="0"/>', 407 ' <Alpha value="1.0"/>', 408 " <VarIndexBase/>", 409 " </ColorStop>", 410 ' <ColorStop index="1">', 411 ' <StopOffset value="0.6"/>', 412 ' <PaletteIndex value="1"/>', 413 ' <Alpha value="1.0"/>', 414 " <VarIndexBase/>", 415 " </ColorStop>", 416 " </ColorLine>", 417 ' <centerX value="0"/>', 418 ' <centerY value="0"/>', 419 ' <startAngle value="0.0"/>', 420 ' <endAngle value="180.0"/>', 421 ' <VarIndexBase value="0"/>', 422 "</Paint>", 423 ], 424 [NO_VARIATION_INDEX, NO_VARIATION_INDEX, 0, NO_VARIATION_INDEX], 425 id="sweep_grad-startAngle", 426 ), 427 pytest.param( 428 [ 429 { 430 "Format": int(ot.PaintFormat.PaintSweepGradient), 431 "ColorLine": { 432 "Extend": int(ot.ExtendMode.PAD), 433 "ColorStop": [ 434 {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0}, 435 {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0}, 436 ], 437 }, 438 "centerX": 0, 439 "centerY": 0, 440 "startAngle": 0.0, 441 "endAngle": 180.0, 442 }, 443 { 444 "Format": int(ot.PaintFormat.PaintSweepGradient), 445 "ColorLine": { 446 "Extend": int(ot.ExtendMode.PAD), 447 "ColorStop": [ 448 {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 0.5}, 449 {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 0.5}, 450 ], 451 }, 452 "centerX": 0, 453 "centerY": 0, 454 "startAngle": 0.0, 455 "endAngle": 180.0, 456 }, 457 ], 458 [ 459 '<Paint Format="9"><!-- PaintVarSweepGradient -->', 460 " <ColorLine>", 461 ' <Extend value="pad"/>', 462 " <!-- StopCount=2 -->", 463 ' <ColorStop index="0">', 464 ' <StopOffset value="0.0"/>', 465 ' <PaletteIndex value="0"/>', 466 ' <Alpha value="1.0"/>', 467 ' <VarIndexBase value="0"/>', 468 " </ColorStop>", 469 ' <ColorStop index="1">', 470 ' <StopOffset value="1.0"/>', 471 ' <PaletteIndex value="1"/>', 472 ' <Alpha value="1.0"/>', 473 ' <VarIndexBase value="0"/>', 474 " </ColorStop>", 475 " </ColorLine>", 476 ' <centerX value="0"/>', 477 ' <centerY value="0"/>', 478 ' <startAngle value="0.0"/>', 479 ' <endAngle value="180.0"/>', 480 " <VarIndexBase/>", 481 "</Paint>", 482 ], 483 [NO_VARIATION_INDEX, 0], 484 id="sweep_grad-stops-alpha-reuse-varidxbase", 485 ), 486 pytest.param( 487 [ 488 { 489 "Format": int(ot.PaintFormat.PaintTransform), 490 "Paint": { 491 "Format": int(ot.PaintFormat.PaintRadialGradient), 492 "ColorLine": { 493 "Extend": int(ot.ExtendMode.PAD), 494 "ColorStop": [ 495 { 496 "StopOffset": 0.0, 497 "PaletteIndex": 0, 498 "Alpha": 1.0, 499 }, 500 { 501 "StopOffset": 1.0, 502 "PaletteIndex": 1, 503 "Alpha": 1.0, 504 }, 505 ], 506 }, 507 "x0": 0, 508 "y0": 0, 509 "r0": 0, 510 "x1": 1, 511 "y1": 1, 512 "r1": 1, 513 }, 514 "Transform": { 515 "xx": 1.0, 516 "xy": 0.0, 517 "yx": 0.0, 518 "yy": 1.0, 519 "dx": 0.0, 520 "dy": 0.0, 521 }, 522 }, 523 { 524 "Format": int(ot.PaintFormat.PaintTransform), 525 "Paint": { 526 "Format": int(ot.PaintFormat.PaintRadialGradient), 527 "ColorLine": { 528 "Extend": int(ot.ExtendMode.PAD), 529 "ColorStop": [ 530 { 531 "StopOffset": 0.0, 532 "PaletteIndex": 0, 533 "Alpha": 1.0, 534 }, 535 { 536 "StopOffset": 1.0, 537 "PaletteIndex": 1, 538 "Alpha": 1.0, 539 }, 540 ], 541 }, 542 "x0": 0, 543 "y0": 0, 544 "r0": 0, 545 "x1": 1, 546 "y1": 1, 547 "r1": 1, 548 }, 549 "Transform": { 550 "xx": 1.0, 551 "xy": 0.0, 552 "yx": 0.0, 553 "yy": 0.5, 554 "dx": 0.0, 555 "dy": -100.0, 556 }, 557 }, 558 ], 559 [ 560 '<Paint Format="13"><!-- PaintVarTransform -->', 561 ' <Paint Format="6"><!-- PaintRadialGradient -->', 562 " <ColorLine>", 563 ' <Extend value="pad"/>', 564 " <!-- StopCount=2 -->", 565 ' <ColorStop index="0">', 566 ' <StopOffset value="0.0"/>', 567 ' <PaletteIndex value="0"/>', 568 ' <Alpha value="1.0"/>', 569 " </ColorStop>", 570 ' <ColorStop index="1">', 571 ' <StopOffset value="1.0"/>', 572 ' <PaletteIndex value="1"/>', 573 ' <Alpha value="1.0"/>', 574 " </ColorStop>", 575 " </ColorLine>", 576 ' <x0 value="0"/>', 577 ' <y0 value="0"/>', 578 ' <r0 value="0"/>', 579 ' <x1 value="1"/>', 580 ' <y1 value="1"/>', 581 ' <r1 value="1"/>', 582 " </Paint>", 583 " <Transform>", 584 ' <xx value="1.0"/>', 585 ' <yx value="0.0"/>', 586 ' <xy value="0.0"/>', 587 ' <yy value="1.0"/>', 588 ' <dx value="0.0"/>', 589 ' <dy value="0.0"/>', 590 ' <VarIndexBase value="0"/>', 591 " </Transform>", 592 "</Paint>", 593 ], 594 [ 595 NO_VARIATION_INDEX, 596 NO_VARIATION_INDEX, 597 NO_VARIATION_INDEX, 598 0, 599 NO_VARIATION_INDEX, 600 1, 601 ], 602 id="transform-yy-dy", 603 ), 604 pytest.param( 605 [ 606 { 607 "Format": ot.PaintFormat.PaintTransform, 608 "Paint": { 609 "Format": ot.PaintFormat.PaintSweepGradient, 610 "ColorLine": { 611 "Extend": ot.ExtendMode.PAD, 612 "ColorStop": [ 613 {"StopOffset": 0.0, "PaletteIndex": 0}, 614 { 615 "StopOffset": 1.0, 616 "PaletteIndex": 1, 617 "Alpha": 1.0, 618 }, 619 ], 620 }, 621 "centerX": 0, 622 "centerY": 0, 623 "startAngle": 0, 624 "endAngle": 360, 625 }, 626 "Transform": (1.0, 0, 0, 1.0, 0, 0), 627 }, 628 { 629 "Format": ot.PaintFormat.PaintTransform, 630 "Paint": { 631 "Format": ot.PaintFormat.PaintSweepGradient, 632 "ColorLine": { 633 "Extend": ot.ExtendMode.PAD, 634 "ColorStop": [ 635 {"StopOffset": 0.0, "PaletteIndex": 0}, 636 { 637 "StopOffset": 1.0, 638 "PaletteIndex": 1, 639 "Alpha": 1.0, 640 }, 641 ], 642 }, 643 "centerX": 256, 644 "centerY": 0, 645 "startAngle": 0, 646 "endAngle": 360, 647 }, 648 # Transform.xx below produces the same VarStore delta as the 649 # above PaintSweepGradient's centerX because, when Fixed16.16 650 # is converted to integer, it becomes: 651 # floatToFixed(1.00390625, 16) == 256 652 # Because there is overlap between the varIdxes of the 653 # PaintVarTransform's Affine2x3 and the PaintSweepGradient's 654 # the VarIndexBase is reused (0 for both) 655 "Transform": (1.00390625, 0, 0, 1.0, 10, 0), 656 }, 657 ], 658 [ 659 '<Paint Format="13"><!-- PaintVarTransform -->', 660 ' <Paint Format="9"><!-- PaintVarSweepGradient -->', 661 " <ColorLine>", 662 ' <Extend value="pad"/>', 663 " <!-- StopCount=2 -->", 664 ' <ColorStop index="0">', 665 ' <StopOffset value="0.0"/>', 666 ' <PaletteIndex value="0"/>', 667 ' <Alpha value="1.0"/>', 668 " <VarIndexBase/>", 669 " </ColorStop>", 670 ' <ColorStop index="1">', 671 ' <StopOffset value="1.0"/>', 672 ' <PaletteIndex value="1"/>', 673 ' <Alpha value="1.0"/>', 674 " <VarIndexBase/>", 675 " </ColorStop>", 676 " </ColorLine>", 677 ' <centerX value="0"/>', 678 ' <centerY value="0"/>', 679 ' <startAngle value="0.0"/>', 680 ' <endAngle value="360.0"/>', 681 ' <VarIndexBase value="0"/>', 682 " </Paint>", 683 " <Transform>", 684 ' <xx value="1.0"/>', 685 ' <yx value="0.0"/>', 686 ' <xy value="0.0"/>', 687 ' <yy value="1.0"/>', 688 ' <dx value="0.0"/>', 689 ' <dy value="0.0"/>', 690 ' <VarIndexBase value="0"/>', 691 " </Transform>", 692 "</Paint>", 693 ], 694 [ 695 0, 696 NO_VARIATION_INDEX, 697 NO_VARIATION_INDEX, 698 NO_VARIATION_INDEX, 699 1, 700 NO_VARIATION_INDEX, 701 ], 702 id="transform-xx-sweep_grad-centerx-same-varidxbase", 703 ), 704 ], 705 ) 706 def test_merge_Paint(self, paints, ttFont, expected_xml, expected_varIdxes): 707 paints = [build_paint(p) for p in paints] 708 out = deepcopy(paints[0]) 709 710 model = VariationModel([{}, {"ZZZZ": 1.0}]) 711 merger = COLRVariationMerger(model, ["ZZZZ"], ttFont) 712 713 merger.mergeThings(out, paints) 714 715 assert compile_decompile(out, ttFont) == out 716 assert dump_xml(out, ttFont) == expected_xml 717 assert merger.varIdxes == expected_varIdxes 718 719 def test_merge_ClipList(self, ttFont): 720 clipLists = [ 721 buildClipList(clips) 722 for clips in [ 723 { 724 "A": (0, 0, 1000, 1000), 725 "B": (0, 0, 1000, 1000), 726 "C": (0, 0, 1000, 1000), 727 "D": (0, 0, 1000, 1000), 728 }, 729 { 730 # non-default masters' clip boxes can be 'sparse' 731 # (i.e. can omit explicit clip box for some glyphs) 732 # "A": (0, 0, 1000, 1000), 733 "B": (10, 0, 1000, 1000), 734 "C": (20, 20, 1020, 1020), 735 "D": (20, 20, 1020, 1020), 736 }, 737 ] 738 ] 739 out = deepcopy(clipLists[0]) 740 741 model = VariationModel([{}, {"ZZZZ": 1.0}]) 742 merger = COLRVariationMerger(model, ["ZZZZ"], ttFont) 743 744 merger.mergeThings(out, clipLists) 745 746 assert compile_decompile(out, ttFont) == out 747 assert dump_xml(out, ttFont) == [ 748 '<ClipList Format="1">', 749 " <Clip>", 750 ' <Glyph value="A"/>', 751 ' <ClipBox Format="1">', 752 ' <xMin value="0"/>', 753 ' <yMin value="0"/>', 754 ' <xMax value="1000"/>', 755 ' <yMax value="1000"/>', 756 " </ClipBox>", 757 " </Clip>", 758 " <Clip>", 759 ' <Glyph value="B"/>', 760 ' <ClipBox Format="2">', 761 ' <xMin value="0"/>', 762 ' <yMin value="0"/>', 763 ' <xMax value="1000"/>', 764 ' <yMax value="1000"/>', 765 ' <VarIndexBase value="0"/>', 766 " </ClipBox>", 767 " </Clip>", 768 " <Clip>", 769 ' <Glyph value="C"/>', 770 ' <Glyph value="D"/>', 771 ' <ClipBox Format="2">', 772 ' <xMin value="0"/>', 773 ' <yMin value="0"/>', 774 ' <xMax value="1000"/>', 775 ' <yMax value="1000"/>', 776 ' <VarIndexBase value="4"/>', 777 " </ClipBox>", 778 " </Clip>", 779 "</ClipList>", 780 ] 781 assert merger.varIdxes == [ 782 0, 783 NO_VARIATION_INDEX, 784 NO_VARIATION_INDEX, 785 NO_VARIATION_INDEX, 786 1, 787 1, 788 1, 789 1, 790 ] 791 792 @pytest.mark.parametrize( 793 "master_layer_reuse", 794 [ 795 pytest.param(False, id="no-reuse"), 796 pytest.param(True, id="with-reuse"), 797 ], 798 ) 799 @pytest.mark.parametrize( 800 "color_glyphs, output_layer_reuse, expected_xml, expected_varIdxes", 801 [ 802 pytest.param( 803 [ 804 { 805 "A": { 806 "Format": int(ot.PaintFormat.PaintColrLayers), 807 "Layers": [ 808 { 809 "Format": int(ot.PaintFormat.PaintGlyph), 810 "Paint": { 811 "Format": int(ot.PaintFormat.PaintSolid), 812 "PaletteIndex": 0, 813 "Alpha": 1.0, 814 }, 815 "Glyph": "B", 816 }, 817 { 818 "Format": int(ot.PaintFormat.PaintGlyph), 819 "Paint": { 820 "Format": int(ot.PaintFormat.PaintSolid), 821 "PaletteIndex": 1, 822 "Alpha": 1.0, 823 }, 824 "Glyph": "B", 825 }, 826 ], 827 }, 828 }, 829 { 830 "A": { 831 "Format": ot.PaintFormat.PaintColrLayers, 832 "Layers": [ 833 { 834 "Format": int(ot.PaintFormat.PaintGlyph), 835 "Paint": { 836 "Format": int(ot.PaintFormat.PaintSolid), 837 "PaletteIndex": 0, 838 "Alpha": 1.0, 839 }, 840 "Glyph": "B", 841 }, 842 { 843 "Format": int(ot.PaintFormat.PaintGlyph), 844 "Paint": { 845 "Format": int(ot.PaintFormat.PaintSolid), 846 "PaletteIndex": 1, 847 "Alpha": 1.0, 848 }, 849 "Glyph": "B", 850 }, 851 ], 852 }, 853 }, 854 ], 855 False, 856 [ 857 "<COLR>", 858 ' <Version value="1"/>', 859 " <!-- BaseGlyphRecordCount=0 -->", 860 " <!-- LayerRecordCount=0 -->", 861 " <BaseGlyphList>", 862 " <!-- BaseGlyphCount=1 -->", 863 ' <BaseGlyphPaintRecord index="0">', 864 ' <BaseGlyph value="A"/>', 865 ' <Paint Format="1"><!-- PaintColrLayers -->', 866 ' <NumLayers value="2"/>', 867 ' <FirstLayerIndex value="0"/>', 868 " </Paint>", 869 " </BaseGlyphPaintRecord>", 870 " </BaseGlyphList>", 871 " <LayerList>", 872 " <!-- LayerCount=2 -->", 873 ' <Paint index="0" Format="10"><!-- PaintGlyph -->', 874 ' <Paint Format="2"><!-- PaintSolid -->', 875 ' <PaletteIndex value="0"/>', 876 ' <Alpha value="1.0"/>', 877 " </Paint>", 878 ' <Glyph value="B"/>', 879 " </Paint>", 880 ' <Paint index="1" Format="10"><!-- PaintGlyph -->', 881 ' <Paint Format="2"><!-- PaintSolid -->', 882 ' <PaletteIndex value="1"/>', 883 ' <Alpha value="1.0"/>', 884 " </Paint>", 885 ' <Glyph value="B"/>', 886 " </Paint>", 887 " </LayerList>", 888 "</COLR>", 889 ], 890 [], 891 id="no-variation", 892 ), 893 pytest.param( 894 [ 895 { 896 "A": { 897 "Format": int(ot.PaintFormat.PaintColrLayers), 898 "Layers": [ 899 { 900 "Format": int(ot.PaintFormat.PaintGlyph), 901 "Paint": { 902 "Format": int(ot.PaintFormat.PaintSolid), 903 "PaletteIndex": 0, 904 "Alpha": 1.0, 905 }, 906 "Glyph": "B", 907 }, 908 { 909 "Format": int(ot.PaintFormat.PaintGlyph), 910 "Paint": { 911 "Format": int(ot.PaintFormat.PaintSolid), 912 "PaletteIndex": 1, 913 "Alpha": 1.0, 914 }, 915 "Glyph": "B", 916 }, 917 ], 918 }, 919 "C": { 920 "Format": int(ot.PaintFormat.PaintColrLayers), 921 "Layers": [ 922 { 923 "Format": int(ot.PaintFormat.PaintGlyph), 924 "Paint": { 925 "Format": int(ot.PaintFormat.PaintSolid), 926 "PaletteIndex": 2, 927 "Alpha": 1.0, 928 }, 929 "Glyph": "B", 930 }, 931 { 932 "Format": int(ot.PaintFormat.PaintGlyph), 933 "Paint": { 934 "Format": int(ot.PaintFormat.PaintSolid), 935 "PaletteIndex": 3, 936 "Alpha": 1.0, 937 }, 938 "Glyph": "B", 939 }, 940 ], 941 }, 942 }, 943 { 944 # NOTE: 'A' is missing from non-default master 945 "C": { 946 "Format": int(ot.PaintFormat.PaintColrLayers), 947 "Layers": [ 948 { 949 "Format": int(ot.PaintFormat.PaintGlyph), 950 "Paint": { 951 "Format": int(ot.PaintFormat.PaintSolid), 952 "PaletteIndex": 2, 953 "Alpha": 0.5, 954 }, 955 "Glyph": "B", 956 }, 957 { 958 "Format": int(ot.PaintFormat.PaintGlyph), 959 "Paint": { 960 "Format": int(ot.PaintFormat.PaintSolid), 961 "PaletteIndex": 3, 962 "Alpha": 0.5, 963 }, 964 "Glyph": "B", 965 }, 966 ], 967 }, 968 }, 969 ], 970 False, 971 [ 972 "<COLR>", 973 ' <Version value="1"/>', 974 " <!-- BaseGlyphRecordCount=0 -->", 975 " <!-- LayerRecordCount=0 -->", 976 " <BaseGlyphList>", 977 " <!-- BaseGlyphCount=2 -->", 978 ' <BaseGlyphPaintRecord index="0">', 979 ' <BaseGlyph value="A"/>', 980 ' <Paint Format="1"><!-- PaintColrLayers -->', 981 ' <NumLayers value="2"/>', 982 ' <FirstLayerIndex value="0"/>', 983 " </Paint>", 984 " </BaseGlyphPaintRecord>", 985 ' <BaseGlyphPaintRecord index="1">', 986 ' <BaseGlyph value="C"/>', 987 ' <Paint Format="1"><!-- PaintColrLayers -->', 988 ' <NumLayers value="2"/>', 989 ' <FirstLayerIndex value="2"/>', 990 " </Paint>", 991 " </BaseGlyphPaintRecord>", 992 " </BaseGlyphList>", 993 " <LayerList>", 994 " <!-- LayerCount=4 -->", 995 ' <Paint index="0" Format="10"><!-- PaintGlyph -->', 996 ' <Paint Format="2"><!-- PaintSolid -->', 997 ' <PaletteIndex value="0"/>', 998 ' <Alpha value="1.0"/>', 999 " </Paint>", 1000 ' <Glyph value="B"/>', 1001 " </Paint>", 1002 ' <Paint index="1" Format="10"><!-- PaintGlyph -->', 1003 ' <Paint Format="2"><!-- PaintSolid -->', 1004 ' <PaletteIndex value="1"/>', 1005 ' <Alpha value="1.0"/>', 1006 " </Paint>", 1007 ' <Glyph value="B"/>', 1008 " </Paint>", 1009 ' <Paint index="2" Format="10"><!-- PaintGlyph -->', 1010 ' <Paint Format="3"><!-- PaintVarSolid -->', 1011 ' <PaletteIndex value="2"/>', 1012 ' <Alpha value="1.0"/>', 1013 ' <VarIndexBase value="0"/>', 1014 " </Paint>", 1015 ' <Glyph value="B"/>', 1016 " </Paint>", 1017 ' <Paint index="3" Format="10"><!-- PaintGlyph -->', 1018 ' <Paint Format="3"><!-- PaintVarSolid -->', 1019 ' <PaletteIndex value="3"/>', 1020 ' <Alpha value="1.0"/>', 1021 ' <VarIndexBase value="0"/>', 1022 " </Paint>", 1023 ' <Glyph value="B"/>', 1024 " </Paint>", 1025 " </LayerList>", 1026 "</COLR>", 1027 ], 1028 [0], 1029 id="sparse-masters", 1030 ), 1031 pytest.param( 1032 [ 1033 { 1034 "A": { 1035 "Format": int(ot.PaintFormat.PaintColrLayers), 1036 "Layers": [ 1037 { 1038 "Format": int(ot.PaintFormat.PaintGlyph), 1039 "Paint": { 1040 "Format": int(ot.PaintFormat.PaintSolid), 1041 "PaletteIndex": 0, 1042 "Alpha": 1.0, 1043 }, 1044 "Glyph": "B", 1045 }, 1046 { 1047 "Format": int(ot.PaintFormat.PaintGlyph), 1048 "Paint": { 1049 "Format": int(ot.PaintFormat.PaintSolid), 1050 "PaletteIndex": 1, 1051 "Alpha": 1.0, 1052 }, 1053 "Glyph": "B", 1054 }, 1055 { 1056 "Format": int(ot.PaintFormat.PaintGlyph), 1057 "Paint": { 1058 "Format": int(ot.PaintFormat.PaintSolid), 1059 "PaletteIndex": 2, 1060 "Alpha": 1.0, 1061 }, 1062 "Glyph": "B", 1063 }, 1064 ], 1065 }, 1066 "C": { 1067 "Format": int(ot.PaintFormat.PaintColrLayers), 1068 "Layers": [ 1069 # 'C' reuses layers 1-3 from 'A' 1070 { 1071 "Format": int(ot.PaintFormat.PaintGlyph), 1072 "Paint": { 1073 "Format": int(ot.PaintFormat.PaintSolid), 1074 "PaletteIndex": 1, 1075 "Alpha": 1.0, 1076 }, 1077 "Glyph": "B", 1078 }, 1079 { 1080 "Format": int(ot.PaintFormat.PaintGlyph), 1081 "Paint": { 1082 "Format": int(ot.PaintFormat.PaintSolid), 1083 "PaletteIndex": 2, 1084 "Alpha": 1.0, 1085 }, 1086 "Glyph": "B", 1087 }, 1088 ], 1089 }, 1090 "D": { # identical to 'C' 1091 "Format": int(ot.PaintFormat.PaintColrLayers), 1092 "Layers": [ 1093 { 1094 "Format": int(ot.PaintFormat.PaintGlyph), 1095 "Paint": { 1096 "Format": int(ot.PaintFormat.PaintSolid), 1097 "PaletteIndex": 1, 1098 "Alpha": 1.0, 1099 }, 1100 "Glyph": "B", 1101 }, 1102 { 1103 "Format": int(ot.PaintFormat.PaintGlyph), 1104 "Paint": { 1105 "Format": int(ot.PaintFormat.PaintSolid), 1106 "PaletteIndex": 2, 1107 "Alpha": 1.0, 1108 }, 1109 "Glyph": "B", 1110 }, 1111 ], 1112 }, 1113 "E": { # superset of 'C' or 'D' 1114 "Format": int(ot.PaintFormat.PaintColrLayers), 1115 "Layers": [ 1116 { 1117 "Format": int(ot.PaintFormat.PaintGlyph), 1118 "Paint": { 1119 "Format": int(ot.PaintFormat.PaintSolid), 1120 "PaletteIndex": 1, 1121 "Alpha": 1.0, 1122 }, 1123 "Glyph": "B", 1124 }, 1125 { 1126 "Format": int(ot.PaintFormat.PaintGlyph), 1127 "Paint": { 1128 "Format": int(ot.PaintFormat.PaintSolid), 1129 "PaletteIndex": 2, 1130 "Alpha": 1.0, 1131 }, 1132 "Glyph": "B", 1133 }, 1134 { 1135 "Format": int(ot.PaintFormat.PaintGlyph), 1136 "Paint": { 1137 "Format": int(ot.PaintFormat.PaintSolid), 1138 "PaletteIndex": 3, 1139 "Alpha": 1.0, 1140 }, 1141 "Glyph": "B", 1142 }, 1143 ], 1144 }, 1145 }, 1146 { 1147 # NOTE: 'A' is missing from non-default master 1148 "C": { 1149 "Format": int(ot.PaintFormat.PaintColrLayers), 1150 "Layers": [ 1151 { 1152 "Format": int(ot.PaintFormat.PaintGlyph), 1153 "Paint": { 1154 "Format": int(ot.PaintFormat.PaintSolid), 1155 "PaletteIndex": 1, 1156 "Alpha": 0.5, 1157 }, 1158 "Glyph": "B", 1159 }, 1160 { 1161 "Format": int(ot.PaintFormat.PaintGlyph), 1162 "Paint": { 1163 "Format": int(ot.PaintFormat.PaintSolid), 1164 "PaletteIndex": 2, 1165 "Alpha": 0.5, 1166 }, 1167 "Glyph": "B", 1168 }, 1169 ], 1170 }, 1171 "D": { # same as 'C' 1172 "Format": int(ot.PaintFormat.PaintColrLayers), 1173 "Layers": [ 1174 { 1175 "Format": int(ot.PaintFormat.PaintGlyph), 1176 "Paint": { 1177 "Format": int(ot.PaintFormat.PaintSolid), 1178 "PaletteIndex": 1, 1179 "Alpha": 0.5, 1180 }, 1181 "Glyph": "B", 1182 }, 1183 { 1184 "Format": int(ot.PaintFormat.PaintGlyph), 1185 "Paint": { 1186 "Format": int(ot.PaintFormat.PaintSolid), 1187 "PaletteIndex": 2, 1188 "Alpha": 0.5, 1189 }, 1190 "Glyph": "B", 1191 }, 1192 ], 1193 }, 1194 "E": { # first two layers vary the same way as 'C' or 'D' 1195 "Format": int(ot.PaintFormat.PaintColrLayers), 1196 "Layers": [ 1197 { 1198 "Format": int(ot.PaintFormat.PaintGlyph), 1199 "Paint": { 1200 "Format": int(ot.PaintFormat.PaintSolid), 1201 "PaletteIndex": 1, 1202 "Alpha": 0.5, 1203 }, 1204 "Glyph": "B", 1205 }, 1206 { 1207 "Format": int(ot.PaintFormat.PaintGlyph), 1208 "Paint": { 1209 "Format": int(ot.PaintFormat.PaintSolid), 1210 "PaletteIndex": 2, 1211 "Alpha": 0.5, 1212 }, 1213 "Glyph": "B", 1214 }, 1215 { 1216 "Format": int(ot.PaintFormat.PaintGlyph), 1217 "Paint": { 1218 "Format": int(ot.PaintFormat.PaintSolid), 1219 "PaletteIndex": 3, 1220 "Alpha": 1.0, 1221 }, 1222 "Glyph": "B", 1223 }, 1224 ], 1225 }, 1226 }, 1227 ], 1228 True, # reuse 1229 [ 1230 "<COLR>", 1231 ' <Version value="1"/>', 1232 " <!-- BaseGlyphRecordCount=0 -->", 1233 " <!-- LayerRecordCount=0 -->", 1234 " <BaseGlyphList>", 1235 " <!-- BaseGlyphCount=4 -->", 1236 ' <BaseGlyphPaintRecord index="0">', 1237 ' <BaseGlyph value="A"/>', 1238 ' <Paint Format="1"><!-- PaintColrLayers -->', 1239 ' <NumLayers value="3"/>', 1240 ' <FirstLayerIndex value="0"/>', 1241 " </Paint>", 1242 " </BaseGlyphPaintRecord>", 1243 ' <BaseGlyphPaintRecord index="1">', 1244 ' <BaseGlyph value="C"/>', 1245 ' <Paint Format="1"><!-- PaintColrLayers -->', 1246 ' <NumLayers value="2"/>', 1247 ' <FirstLayerIndex value="3"/>', 1248 " </Paint>", 1249 " </BaseGlyphPaintRecord>", 1250 ' <BaseGlyphPaintRecord index="2">', 1251 ' <BaseGlyph value="D"/>', 1252 ' <Paint Format="1"><!-- PaintColrLayers -->', 1253 ' <NumLayers value="2"/>', 1254 ' <FirstLayerIndex value="3"/>', 1255 " </Paint>", 1256 " </BaseGlyphPaintRecord>", 1257 ' <BaseGlyphPaintRecord index="3">', 1258 ' <BaseGlyph value="E"/>', 1259 ' <Paint Format="1"><!-- PaintColrLayers -->', 1260 ' <NumLayers value="2"/>', 1261 ' <FirstLayerIndex value="5"/>', 1262 " </Paint>", 1263 " </BaseGlyphPaintRecord>", 1264 " </BaseGlyphList>", 1265 " <LayerList>", 1266 " <!-- LayerCount=7 -->", 1267 ' <Paint index="0" Format="10"><!-- PaintGlyph -->', 1268 ' <Paint Format="2"><!-- PaintSolid -->', 1269 ' <PaletteIndex value="0"/>', 1270 ' <Alpha value="1.0"/>', 1271 " </Paint>", 1272 ' <Glyph value="B"/>', 1273 " </Paint>", 1274 ' <Paint index="1" Format="10"><!-- PaintGlyph -->', 1275 ' <Paint Format="2"><!-- PaintSolid -->', 1276 ' <PaletteIndex value="1"/>', 1277 ' <Alpha value="1.0"/>', 1278 " </Paint>", 1279 ' <Glyph value="B"/>', 1280 " </Paint>", 1281 ' <Paint index="2" Format="10"><!-- PaintGlyph -->', 1282 ' <Paint Format="2"><!-- PaintSolid -->', 1283 ' <PaletteIndex value="2"/>', 1284 ' <Alpha value="1.0"/>', 1285 " </Paint>", 1286 ' <Glyph value="B"/>', 1287 " </Paint>", 1288 ' <Paint index="3" Format="10"><!-- PaintGlyph -->', 1289 ' <Paint Format="3"><!-- PaintVarSolid -->', 1290 ' <PaletteIndex value="1"/>', 1291 ' <Alpha value="1.0"/>', 1292 ' <VarIndexBase value="0"/>', 1293 " </Paint>", 1294 ' <Glyph value="B"/>', 1295 " </Paint>", 1296 ' <Paint index="4" Format="10"><!-- PaintGlyph -->', 1297 ' <Paint Format="3"><!-- PaintVarSolid -->', 1298 ' <PaletteIndex value="2"/>', 1299 ' <Alpha value="1.0"/>', 1300 ' <VarIndexBase value="0"/>', 1301 " </Paint>", 1302 ' <Glyph value="B"/>', 1303 " </Paint>", 1304 ' <Paint index="5" Format="1"><!-- PaintColrLayers -->', 1305 ' <NumLayers value="2"/>', 1306 ' <FirstLayerIndex value="3"/>', 1307 " </Paint>", 1308 ' <Paint index="6" Format="10"><!-- PaintGlyph -->', 1309 ' <Paint Format="2"><!-- PaintSolid -->', 1310 ' <PaletteIndex value="3"/>', 1311 ' <Alpha value="1.0"/>', 1312 " </Paint>", 1313 ' <Glyph value="B"/>', 1314 " </Paint>", 1315 " </LayerList>", 1316 "</COLR>", 1317 ], 1318 [0], 1319 id="sparse-masters-with-reuse", 1320 ), 1321 pytest.param( 1322 [ 1323 { 1324 "A": { 1325 "Format": int(ot.PaintFormat.PaintColrLayers), 1326 "Layers": [ 1327 { 1328 "Format": int(ot.PaintFormat.PaintGlyph), 1329 "Paint": { 1330 "Format": int(ot.PaintFormat.PaintSolid), 1331 "PaletteIndex": 0, 1332 "Alpha": 1.0, 1333 }, 1334 "Glyph": "B", 1335 }, 1336 { 1337 "Format": int(ot.PaintFormat.PaintGlyph), 1338 "Paint": { 1339 "Format": int(ot.PaintFormat.PaintSolid), 1340 "PaletteIndex": 1, 1341 "Alpha": 1.0, 1342 }, 1343 "Glyph": "B", 1344 }, 1345 { 1346 "Format": int(ot.PaintFormat.PaintGlyph), 1347 "Paint": { 1348 "Format": int(ot.PaintFormat.PaintSolid), 1349 "PaletteIndex": 2, 1350 "Alpha": 1.0, 1351 }, 1352 "Glyph": "B", 1353 }, 1354 ], 1355 }, 1356 "C": { # 'C' shares layer 1 and 2 with 'A' 1357 "Format": int(ot.PaintFormat.PaintColrLayers), 1358 "Layers": [ 1359 { 1360 "Format": int(ot.PaintFormat.PaintGlyph), 1361 "Paint": { 1362 "Format": int(ot.PaintFormat.PaintSolid), 1363 "PaletteIndex": 1, 1364 "Alpha": 1.0, 1365 }, 1366 "Glyph": "B", 1367 }, 1368 { 1369 "Format": int(ot.PaintFormat.PaintGlyph), 1370 "Paint": { 1371 "Format": int(ot.PaintFormat.PaintSolid), 1372 "PaletteIndex": 2, 1373 "Alpha": 1.0, 1374 }, 1375 "Glyph": "B", 1376 }, 1377 ], 1378 }, 1379 }, 1380 { 1381 "A": { 1382 "Format": int(ot.PaintFormat.PaintColrLayers), 1383 "Layers": [ 1384 { 1385 "Format": int(ot.PaintFormat.PaintGlyph), 1386 "Paint": { 1387 "Format": int(ot.PaintFormat.PaintSolid), 1388 "PaletteIndex": 0, 1389 "Alpha": 1.0, 1390 }, 1391 "Glyph": "B", 1392 }, 1393 { 1394 "Format": int(ot.PaintFormat.PaintGlyph), 1395 "Paint": { 1396 "Format": int(ot.PaintFormat.PaintSolid), 1397 "PaletteIndex": 1, 1398 "Alpha": 0.9, 1399 }, 1400 "Glyph": "B", 1401 }, 1402 { 1403 "Format": int(ot.PaintFormat.PaintGlyph), 1404 "Paint": { 1405 "Format": int(ot.PaintFormat.PaintSolid), 1406 "PaletteIndex": 2, 1407 "Alpha": 1.0, 1408 }, 1409 "Glyph": "B", 1410 }, 1411 ], 1412 }, 1413 "C": { 1414 "Format": int(ot.PaintFormat.PaintColrLayers), 1415 "Layers": [ 1416 { 1417 "Format": int(ot.PaintFormat.PaintGlyph), 1418 "Paint": { 1419 "Format": int(ot.PaintFormat.PaintSolid), 1420 "PaletteIndex": 1, 1421 "Alpha": 0.5, 1422 }, 1423 "Glyph": "B", 1424 }, 1425 { 1426 "Format": int(ot.PaintFormat.PaintGlyph), 1427 "Paint": { 1428 "Format": int(ot.PaintFormat.PaintSolid), 1429 "PaletteIndex": 2, 1430 "Alpha": 1.0, 1431 }, 1432 "Glyph": "B", 1433 }, 1434 ], 1435 }, 1436 }, 1437 ], 1438 True, 1439 [ 1440 # a different Alpha variation is applied to a shared layer between 1441 # 'A' and 'C' and thus they are no longer shared. 1442 "<COLR>", 1443 ' <Version value="1"/>', 1444 " <!-- BaseGlyphRecordCount=0 -->", 1445 " <!-- LayerRecordCount=0 -->", 1446 " <BaseGlyphList>", 1447 " <!-- BaseGlyphCount=2 -->", 1448 ' <BaseGlyphPaintRecord index="0">', 1449 ' <BaseGlyph value="A"/>', 1450 ' <Paint Format="1"><!-- PaintColrLayers -->', 1451 ' <NumLayers value="3"/>', 1452 ' <FirstLayerIndex value="0"/>', 1453 " </Paint>", 1454 " </BaseGlyphPaintRecord>", 1455 ' <BaseGlyphPaintRecord index="1">', 1456 ' <BaseGlyph value="C"/>', 1457 ' <Paint Format="1"><!-- PaintColrLayers -->', 1458 ' <NumLayers value="2"/>', 1459 ' <FirstLayerIndex value="3"/>', 1460 " </Paint>", 1461 " </BaseGlyphPaintRecord>", 1462 " </BaseGlyphList>", 1463 " <LayerList>", 1464 " <!-- LayerCount=5 -->", 1465 ' <Paint index="0" Format="10"><!-- PaintGlyph -->', 1466 ' <Paint Format="2"><!-- PaintSolid -->', 1467 ' <PaletteIndex value="0"/>', 1468 ' <Alpha value="1.0"/>', 1469 " </Paint>", 1470 ' <Glyph value="B"/>', 1471 " </Paint>", 1472 ' <Paint index="1" Format="10"><!-- PaintGlyph -->', 1473 ' <Paint Format="3"><!-- PaintVarSolid -->', 1474 ' <PaletteIndex value="1"/>', 1475 ' <Alpha value="1.0"/>', 1476 ' <VarIndexBase value="0"/>', 1477 " </Paint>", 1478 ' <Glyph value="B"/>', 1479 " </Paint>", 1480 ' <Paint index="2" Format="10"><!-- PaintGlyph -->', 1481 ' <Paint Format="2"><!-- PaintSolid -->', 1482 ' <PaletteIndex value="2"/>', 1483 ' <Alpha value="1.0"/>', 1484 " </Paint>", 1485 ' <Glyph value="B"/>', 1486 " </Paint>", 1487 ' <Paint index="3" Format="10"><!-- PaintGlyph -->', 1488 ' <Paint Format="3"><!-- PaintVarSolid -->', 1489 ' <PaletteIndex value="1"/>', 1490 ' <Alpha value="1.0"/>', 1491 ' <VarIndexBase value="1"/>', 1492 " </Paint>", 1493 ' <Glyph value="B"/>', 1494 " </Paint>", 1495 ' <Paint index="4" Format="10"><!-- PaintGlyph -->', 1496 ' <Paint Format="2"><!-- PaintSolid -->', 1497 ' <PaletteIndex value="2"/>', 1498 ' <Alpha value="1.0"/>', 1499 " </Paint>", 1500 ' <Glyph value="B"/>', 1501 " </Paint>", 1502 " </LayerList>", 1503 "</COLR>", 1504 ], 1505 [0, 1], 1506 id="shared-master-layers-different-variations", 1507 ), 1508 ], 1509 ) 1510 def test_merge_full_table( 1511 self, 1512 color_glyphs, 1513 ttFont, 1514 expected_xml, 1515 expected_varIdxes, 1516 master_layer_reuse, 1517 output_layer_reuse, 1518 ): 1519 master_ttfs = [deepcopy(ttFont) for _ in range(len(color_glyphs))] 1520 for ttf, glyphs in zip(master_ttfs, color_glyphs): 1521 # merge algorithm is expected to work the same even if the master COLRs 1522 # may differ as to the layer reuse, hence we try both ways 1523 ttf["COLR"] = buildCOLR(glyphs, allowLayerReuse=master_layer_reuse) 1524 vf = deepcopy(master_ttfs[0]) 1525 1526 model = VariationModel([{}, {"ZZZZ": 1.0}]) 1527 merger = COLRVariationMerger( 1528 model, ["ZZZZ"], vf, allowLayerReuse=output_layer_reuse 1529 ) 1530 1531 merger.mergeTables(vf, master_ttfs) 1532 1533 out = vf["COLR"].table 1534 1535 assert compile_decompile(out, vf) == out 1536 assert dump_xml(out, vf) == expected_xml 1537 assert merger.varIdxes == expected_varIdxes 1538 1539 @pytest.mark.parametrize( 1540 "color_glyphs, before_xml, expected_xml", 1541 [ 1542 pytest.param( 1543 { 1544 "A": { 1545 "Format": int(ot.PaintFormat.PaintColrLayers), 1546 "Layers": [ 1547 { 1548 "Format": int(ot.PaintFormat.PaintGlyph), 1549 "Paint": { 1550 "Format": int(ot.PaintFormat.PaintSolid), 1551 "PaletteIndex": 0, 1552 "Alpha": 1.0, 1553 }, 1554 "Glyph": "B", 1555 }, 1556 { 1557 "Format": int(ot.PaintFormat.PaintGlyph), 1558 "Paint": { 1559 "Format": int(ot.PaintFormat.PaintSolid), 1560 "PaletteIndex": 1, 1561 "Alpha": 1.0, 1562 }, 1563 "Glyph": "C", 1564 }, 1565 { 1566 "Format": int(ot.PaintFormat.PaintGlyph), 1567 "Paint": { 1568 "Format": int(ot.PaintFormat.PaintSolid), 1569 "PaletteIndex": 2, 1570 "Alpha": 1.0, 1571 }, 1572 "Glyph": "D", 1573 }, 1574 ], 1575 }, 1576 "E": { 1577 "Format": int(ot.PaintFormat.PaintColrLayers), 1578 "Layers": [ 1579 { 1580 "Format": int(ot.PaintFormat.PaintGlyph), 1581 "Paint": { 1582 "Format": int(ot.PaintFormat.PaintSolid), 1583 "PaletteIndex": 1, 1584 "Alpha": 1.0, 1585 }, 1586 "Glyph": "C", 1587 }, 1588 { 1589 "Format": int(ot.PaintFormat.PaintGlyph), 1590 "Paint": { 1591 "Format": int(ot.PaintFormat.PaintSolid), 1592 "PaletteIndex": 2, 1593 "Alpha": 1.0, 1594 }, 1595 "Glyph": "D", 1596 }, 1597 { 1598 "Format": int(ot.PaintFormat.PaintGlyph), 1599 "Paint": { 1600 "Format": int(ot.PaintFormat.PaintSolid), 1601 "PaletteIndex": 3, 1602 "Alpha": 1.0, 1603 }, 1604 "Glyph": "F", 1605 }, 1606 ], 1607 }, 1608 "G": { 1609 "Format": int(ot.PaintFormat.PaintColrGlyph), 1610 "Glyph": "E", 1611 }, 1612 }, 1613 [ 1614 "<COLR>", 1615 ' <Version value="1"/>', 1616 " <!-- BaseGlyphRecordCount=0 -->", 1617 " <!-- LayerRecordCount=0 -->", 1618 " <BaseGlyphList>", 1619 " <!-- BaseGlyphCount=3 -->", 1620 ' <BaseGlyphPaintRecord index="0">', 1621 ' <BaseGlyph value="A"/>', 1622 ' <Paint Format="1"><!-- PaintColrLayers -->', 1623 ' <NumLayers value="3"/>', 1624 ' <FirstLayerIndex value="0"/>', 1625 " </Paint>", 1626 " </BaseGlyphPaintRecord>", 1627 ' <BaseGlyphPaintRecord index="1">', 1628 ' <BaseGlyph value="E"/>', 1629 ' <Paint Format="1"><!-- PaintColrLayers -->', 1630 ' <NumLayers value="2"/>', 1631 ' <FirstLayerIndex value="3"/>', 1632 " </Paint>", 1633 " </BaseGlyphPaintRecord>", 1634 ' <BaseGlyphPaintRecord index="2">', 1635 ' <BaseGlyph value="G"/>', 1636 ' <Paint Format="11"><!-- PaintColrGlyph -->', 1637 ' <Glyph value="E"/>', 1638 " </Paint>", 1639 " </BaseGlyphPaintRecord>", 1640 " </BaseGlyphList>", 1641 " <LayerList>", 1642 " <!-- LayerCount=5 -->", 1643 ' <Paint index="0" Format="10"><!-- PaintGlyph -->', 1644 ' <Paint Format="2"><!-- PaintSolid -->', 1645 ' <PaletteIndex value="0"/>', 1646 ' <Alpha value="1.0"/>', 1647 " </Paint>", 1648 ' <Glyph value="B"/>', 1649 " </Paint>", 1650 ' <Paint index="1" Format="10"><!-- PaintGlyph -->', 1651 ' <Paint Format="2"><!-- PaintSolid -->', 1652 ' <PaletteIndex value="1"/>', 1653 ' <Alpha value="1.0"/>', 1654 " </Paint>", 1655 ' <Glyph value="C"/>', 1656 " </Paint>", 1657 ' <Paint index="2" Format="10"><!-- PaintGlyph -->', 1658 ' <Paint Format="2"><!-- PaintSolid -->', 1659 ' <PaletteIndex value="2"/>', 1660 ' <Alpha value="1.0"/>', 1661 " </Paint>", 1662 ' <Glyph value="D"/>', 1663 " </Paint>", 1664 ' <Paint index="3" Format="1"><!-- PaintColrLayers -->', 1665 ' <NumLayers value="2"/>', 1666 ' <FirstLayerIndex value="1"/>', 1667 " </Paint>", 1668 ' <Paint index="4" Format="10"><!-- PaintGlyph -->', 1669 ' <Paint Format="2"><!-- PaintSolid -->', 1670 ' <PaletteIndex value="3"/>', 1671 ' <Alpha value="1.0"/>', 1672 " </Paint>", 1673 ' <Glyph value="F"/>', 1674 " </Paint>", 1675 " </LayerList>", 1676 "</COLR>", 1677 ], 1678 [ 1679 "<COLR>", 1680 ' <Version value="1"/>', 1681 " <!-- BaseGlyphRecordCount=0 -->", 1682 " <!-- LayerRecordCount=0 -->", 1683 " <BaseGlyphList>", 1684 " <!-- BaseGlyphCount=3 -->", 1685 ' <BaseGlyphPaintRecord index="0">', 1686 ' <BaseGlyph value="A"/>', 1687 ' <Paint Format="1"><!-- PaintColrLayers -->', 1688 ' <NumLayers value="3"/>', 1689 ' <FirstLayerIndex value="0"/>', 1690 " </Paint>", 1691 " </BaseGlyphPaintRecord>", 1692 ' <BaseGlyphPaintRecord index="1">', 1693 ' <BaseGlyph value="E"/>', 1694 ' <Paint Format="1"><!-- PaintColrLayers -->', 1695 ' <NumLayers value="3"/>', 1696 ' <FirstLayerIndex value="3"/>', 1697 " </Paint>", 1698 " </BaseGlyphPaintRecord>", 1699 ' <BaseGlyphPaintRecord index="2">', 1700 ' <BaseGlyph value="G"/>', 1701 ' <Paint Format="11"><!-- PaintColrGlyph -->', 1702 ' <Glyph value="E"/>', 1703 " </Paint>", 1704 " </BaseGlyphPaintRecord>", 1705 " </BaseGlyphList>", 1706 " <LayerList>", 1707 " <!-- LayerCount=6 -->", 1708 ' <Paint index="0" Format="10"><!-- PaintGlyph -->', 1709 ' <Paint Format="2"><!-- PaintSolid -->', 1710 ' <PaletteIndex value="0"/>', 1711 ' <Alpha value="1.0"/>', 1712 " </Paint>", 1713 ' <Glyph value="B"/>', 1714 " </Paint>", 1715 ' <Paint index="1" Format="10"><!-- PaintGlyph -->', 1716 ' <Paint Format="2"><!-- PaintSolid -->', 1717 ' <PaletteIndex value="1"/>', 1718 ' <Alpha value="1.0"/>', 1719 " </Paint>", 1720 ' <Glyph value="C"/>', 1721 " </Paint>", 1722 ' <Paint index="2" Format="10"><!-- PaintGlyph -->', 1723 ' <Paint Format="2"><!-- PaintSolid -->', 1724 ' <PaletteIndex value="2"/>', 1725 ' <Alpha value="1.0"/>', 1726 " </Paint>", 1727 ' <Glyph value="D"/>', 1728 " </Paint>", 1729 ' <Paint index="3" Format="10"><!-- PaintGlyph -->', 1730 ' <Paint Format="2"><!-- PaintSolid -->', 1731 ' <PaletteIndex value="1"/>', 1732 ' <Alpha value="1.0"/>', 1733 " </Paint>", 1734 ' <Glyph value="C"/>', 1735 " </Paint>", 1736 ' <Paint index="4" Format="10"><!-- PaintGlyph -->', 1737 ' <Paint Format="2"><!-- PaintSolid -->', 1738 ' <PaletteIndex value="2"/>', 1739 ' <Alpha value="1.0"/>', 1740 " </Paint>", 1741 ' <Glyph value="D"/>', 1742 " </Paint>", 1743 ' <Paint index="5" Format="10"><!-- PaintGlyph -->', 1744 ' <Paint Format="2"><!-- PaintSolid -->', 1745 ' <PaletteIndex value="3"/>', 1746 ' <Alpha value="1.0"/>', 1747 " </Paint>", 1748 ' <Glyph value="F"/>', 1749 " </Paint>", 1750 " </LayerList>", 1751 "</COLR>", 1752 ], 1753 id="simple-reuse", 1754 ), 1755 pytest.param( 1756 { 1757 "A": { 1758 "Format": int(ot.PaintFormat.PaintGlyph), 1759 "Paint": { 1760 "Format": int(ot.PaintFormat.PaintSolid), 1761 "PaletteIndex": 0, 1762 "Alpha": 1.0, 1763 }, 1764 "Glyph": "B", 1765 }, 1766 }, 1767 [ 1768 "<COLR>", 1769 ' <Version value="1"/>', 1770 " <!-- BaseGlyphRecordCount=0 -->", 1771 " <!-- LayerRecordCount=0 -->", 1772 " <BaseGlyphList>", 1773 " <!-- BaseGlyphCount=1 -->", 1774 ' <BaseGlyphPaintRecord index="0">', 1775 ' <BaseGlyph value="A"/>', 1776 ' <Paint Format="10"><!-- PaintGlyph -->', 1777 ' <Paint Format="2"><!-- PaintSolid -->', 1778 ' <PaletteIndex value="0"/>', 1779 ' <Alpha value="1.0"/>', 1780 " </Paint>", 1781 ' <Glyph value="B"/>', 1782 " </Paint>", 1783 " </BaseGlyphPaintRecord>", 1784 " </BaseGlyphList>", 1785 "</COLR>", 1786 ], 1787 [ 1788 "<COLR>", 1789 ' <Version value="1"/>', 1790 " <!-- BaseGlyphRecordCount=0 -->", 1791 " <!-- LayerRecordCount=0 -->", 1792 " <BaseGlyphList>", 1793 " <!-- BaseGlyphCount=1 -->", 1794 ' <BaseGlyphPaintRecord index="0">', 1795 ' <BaseGlyph value="A"/>', 1796 ' <Paint Format="10"><!-- PaintGlyph -->', 1797 ' <Paint Format="2"><!-- PaintSolid -->', 1798 ' <PaletteIndex value="0"/>', 1799 ' <Alpha value="1.0"/>', 1800 " </Paint>", 1801 ' <Glyph value="B"/>', 1802 " </Paint>", 1803 " </BaseGlyphPaintRecord>", 1804 " </BaseGlyphList>", 1805 "</COLR>", 1806 ], 1807 id="no-layer-list", 1808 ), 1809 ], 1810 ) 1811 def test_expandPaintColrLayers( 1812 self, color_glyphs, ttFont, before_xml, expected_xml 1813 ): 1814 colr = buildCOLR(color_glyphs, allowLayerReuse=True) 1815 1816 assert dump_xml(colr.table, ttFont) == before_xml 1817 1818 before_layer_count = 0 1819 reuses_colr_layers = False 1820 if colr.table.LayerList: 1821 before_layer_count = len(colr.table.LayerList.Paint) 1822 reuses_colr_layers = any( 1823 p.Format == ot.PaintFormat.PaintColrLayers 1824 for p in colr.table.LayerList.Paint 1825 ) 1826 1827 COLRVariationMerger.expandPaintColrLayers(colr.table) 1828 1829 assert dump_xml(colr.table, ttFont) == expected_xml 1830 1831 after_layer_count = ( 1832 0 if not colr.table.LayerList else len(colr.table.LayerList.Paint) 1833 ) 1834 1835 if reuses_colr_layers: 1836 assert not any( 1837 p.Format == ot.PaintFormat.PaintColrLayers 1838 for p in colr.table.LayerList.Paint 1839 ) 1840 assert after_layer_count > before_layer_count 1841 else: 1842 assert after_layer_count == before_layer_count 1843 1844 if colr.table.LayerList: 1845 assert len({id(p) for p in colr.table.LayerList.Paint}) == after_layer_count 1846 1847 1848class SparsePositioningMergerTest: 1849 def test_zero_kern_at_default(self): 1850 # https://github.com/fonttools/fonttools/issues/3111 1851 1852 pytest.importorskip("ufo2ft") 1853 pytest.importorskip("ufoLib2") 1854 1855 from fontTools.designspaceLib import DesignSpaceDocument 1856 from ufo2ft import compileVariableTTF 1857 from ufoLib2 import Font 1858 1859 ds = DesignSpaceDocument() 1860 ds.addAxisDescriptor( 1861 name="wght", tag="wght", minimum=100, maximum=900, default=400 1862 ) 1863 ds.addSourceDescriptor(font=Font(), location=dict(wght=100)) 1864 ds.addSourceDescriptor(font=Font(), location=dict(wght=400)) 1865 ds.addSourceDescriptor(font=Font(), location=dict(wght=900)) 1866 1867 ds.sources[0].font.newGlyph("a").unicode = ord("a") 1868 ds.sources[0].font.newGlyph("b").unicode = ord("b") 1869 ds.sources[0].font.features.text = "feature kern { pos a b b' 100; } kern;" 1870 1871 ds.sources[1].font.newGlyph("a").unicode = ord("a") 1872 ds.sources[1].font.newGlyph("b").unicode = ord("b") 1873 ds.sources[1].font.features.text = "feature kern { pos a b b' 0; } kern;" 1874 1875 ds.sources[2].font.newGlyph("a").unicode = ord("a") 1876 ds.sources[2].font.newGlyph("b").unicode = ord("b") 1877 ds.sources[2].font.features.text = "feature kern { pos a b b' -100; } kern;" 1878 1879 font = compileVariableTTF(ds, inplace=True) 1880 b = BytesIO() 1881 font.save(b) 1882 1883 assert font["GDEF"].table.VarStore.VarData[0].Item[0] == [100, -100] 1884 1885 def test_sparse_cursive(self): 1886 # https://github.com/fonttools/fonttools/issues/3168 1887 1888 pytest.importorskip("ufo2ft") 1889 pytest.importorskip("ufoLib2") 1890 1891 from fontTools.designspaceLib import DesignSpaceDocument 1892 from ufo2ft import compileVariableTTF 1893 from ufoLib2 import Font 1894 1895 ds = DesignSpaceDocument() 1896 ds.addAxisDescriptor( 1897 name="wght", tag="wght", minimum=100, maximum=900, default=400 1898 ) 1899 ds.addSourceDescriptor(font=Font(), location=dict(wght=100)) 1900 ds.addSourceDescriptor(font=Font(), location=dict(wght=400)) 1901 ds.addSourceDescriptor(font=Font(), location=dict(wght=900)) 1902 1903 ds.sources[0].font.newGlyph("a").unicode = ord("a") 1904 ds.sources[0].font.newGlyph("b").unicode = ord("b") 1905 ds.sources[0].font.newGlyph("c").unicode = ord("c") 1906 ds.sources[ 1907 0 1908 ].font.features.text = """ 1909 feature curs { 1910 position cursive a <anchor 400 20> <anchor 0 -20>; 1911 position cursive c <anchor NULL> <anchor 0 -20>; 1912 } curs; 1913 """ 1914 1915 ds.sources[1].font.newGlyph("a").unicode = ord("a") 1916 ds.sources[1].font.newGlyph("b").unicode = ord("b") 1917 ds.sources[1].font.newGlyph("c").unicode = ord("c") 1918 ds.sources[ 1919 1 1920 ].font.features.text = """ 1921 feature curs { 1922 position cursive a <anchor 500 20> <anchor 0 -20>; 1923 position cursive b <anchor 50 22> <anchor 0 -10>; 1924 position cursive c <anchor NULL> <anchor 0 -20>; 1925 } curs; 1926 """ 1927 1928 ds.sources[2].font.newGlyph("a").unicode = ord("a") 1929 ds.sources[2].font.newGlyph("b").unicode = ord("b") 1930 ds.sources[2].font.newGlyph("c").unicode = ord("c") 1931 ds.sources[ 1932 2 1933 ].font.features.text = """ 1934 feature curs { 1935 position cursive b <anchor 100 40> <anchor 0 -30>; 1936 position cursive c <anchor NULL> <anchor 0 -20>; 1937 } curs; 1938 """ 1939 1940 font = compileVariableTTF(ds, inplace=True) 1941 b = BytesIO() 1942 font.save(b) 1943 1944 assert font["GDEF"].table.VarStore.VarData[0].Item[0] == [-100, 0] 1945