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