• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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