• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from copy import deepcopy
2from fontTools.ttLib import newTable
3from fontTools.ttLib.tables import otTables as ot
4from fontTools.colorLib import builder
5from fontTools.colorLib.geometry import round_start_circle_stable_containment, Circle
6from fontTools.colorLib.builder import LayerListBuilder
7from fontTools.colorLib.table_builder import TableBuilder
8from fontTools.colorLib.errors import ColorLibError
9import pytest
10from typing import List
11
12
13def _build(cls, source):
14    return LayerListBuilder().tableBuilder.build(cls, source)
15
16
17def _buildPaint(source):
18    return LayerListBuilder().buildPaint(source)
19
20
21def test_buildCOLR_v0():
22    color_layer_lists = {
23        "a": [("a.color0", 0), ("a.color1", 1)],
24        "b": [("b.color1", 1), ("b.color0", 0)],
25    }
26
27    colr = builder.buildCOLR(color_layer_lists)
28
29    assert colr.tableTag == "COLR"
30    assert colr.version == 0
31    assert colr.ColorLayers["a"][0].name == "a.color0"
32    assert colr.ColorLayers["a"][0].colorID == 0
33    assert colr.ColorLayers["a"][1].name == "a.color1"
34    assert colr.ColorLayers["a"][1].colorID == 1
35    assert colr.ColorLayers["b"][0].name == "b.color1"
36    assert colr.ColorLayers["b"][0].colorID == 1
37    assert colr.ColorLayers["b"][1].name == "b.color0"
38    assert colr.ColorLayers["b"][1].colorID == 0
39
40
41def test_buildCOLR_v0_layer_as_list():
42    # when COLRv0 layers are encoded as plist in UFO lib, both python tuples and
43    # lists are encoded as plist array elements; but the latter are always decoded
44    # as python lists, thus after roundtripping a plist tuples become lists.
45    # Before FontTools 4.17.0 we were treating tuples and lists as equivalent;
46    # with 4.17.0, a paint of type list is used to identify a PaintColrLayers.
47    # This broke backward compatibility as ufo2ft is simply passing through the
48    # color layers as read from the UFO lib plist, and as such the latter use lists
49    # instead of tuples for COLRv0 layers (layerGlyph, paletteIndex) combo.
50    # We restore backward compat by accepting either tuples or lists (of length 2
51    # and only containing a str and an int) as individual top-level layers.
52    # https://github.com/googlefonts/ufo2ft/issues/426
53    color_layer_lists = {
54        "a": [["a.color0", 0], ["a.color1", 1]],
55        "b": [["b.color1", 1], ["b.color0", 0]],
56    }
57
58    colr = builder.buildCOLR(color_layer_lists)
59
60    assert colr.tableTag == "COLR"
61    assert colr.version == 0
62    assert colr.ColorLayers["a"][0].name == "a.color0"
63    assert colr.ColorLayers["a"][0].colorID == 0
64    assert colr.ColorLayers["a"][1].name == "a.color1"
65    assert colr.ColorLayers["a"][1].colorID == 1
66    assert colr.ColorLayers["b"][0].name == "b.color1"
67    assert colr.ColorLayers["b"][0].colorID == 1
68    assert colr.ColorLayers["b"][1].name == "b.color0"
69    assert colr.ColorLayers["b"][1].colorID == 0
70
71
72def test_buildCPAL_v0():
73    palettes = [
74        [(0.68, 0.20, 0.32, 1.0), (0.45, 0.68, 0.21, 1.0)],
75        [(0.68, 0.20, 0.32, 0.6), (0.45, 0.68, 0.21, 0.6)],
76        [(0.68, 0.20, 0.32, 0.3), (0.45, 0.68, 0.21, 0.3)],
77    ]
78
79    cpal = builder.buildCPAL(palettes)
80
81    assert cpal.tableTag == "CPAL"
82    assert cpal.version == 0
83    assert cpal.numPaletteEntries == 2
84
85    assert len(cpal.palettes) == 3
86    assert [tuple(c) for c in cpal.palettes[0]] == [
87        (82, 51, 173, 255),
88        (54, 173, 115, 255),
89    ]
90    assert [tuple(c) for c in cpal.palettes[1]] == [
91        (82, 51, 173, 153),
92        (54, 173, 115, 153),
93    ]
94    assert [tuple(c) for c in cpal.palettes[2]] == [
95        (82, 51, 173, 76),
96        (54, 173, 115, 76),
97    ]
98
99
100def test_buildCPAL_palettes_different_lengths():
101    with pytest.raises(ColorLibError, match="have different lengths"):
102        builder.buildCPAL([[(1, 1, 1, 1)], [(0, 0, 0, 1), (0.5, 0.5, 0.5, 1)]])
103
104
105def test_buildPaletteLabels():
106    name_table = newTable("name")
107    name_table.names = []
108
109    name_ids = builder.buildPaletteLabels(
110        [None, "hi", {"en": "hello", "de": "hallo"}], name_table
111    )
112
113    assert name_ids == [0xFFFF, 256, 257]
114
115    assert len(name_table.names) == 3
116    assert str(name_table.names[0]) == "hi"
117    assert name_table.names[0].nameID == 256
118
119    assert str(name_table.names[1]) == "hallo"
120    assert name_table.names[1].nameID == 257
121
122    assert str(name_table.names[2]) == "hello"
123    assert name_table.names[2].nameID == 257
124
125
126def test_build_CPAL_v1_types_no_labels():
127    palettes = [
128        [(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)],
129        [(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)],
130        [(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)],
131    ]
132    paletteTypes = [
133        builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND,
134        builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND,
135        builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND
136        | builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND,
137    ]
138
139    cpal = builder.buildCPAL(palettes, paletteTypes=paletteTypes)
140
141    assert cpal.tableTag == "CPAL"
142    assert cpal.version == 1
143    assert cpal.numPaletteEntries == 2
144    assert len(cpal.palettes) == 3
145
146    assert cpal.paletteTypes == paletteTypes
147    assert cpal.paletteLabels == [cpal.NO_NAME_ID] * len(palettes)
148    assert cpal.paletteEntryLabels == [cpal.NO_NAME_ID] * cpal.numPaletteEntries
149
150
151def test_build_CPAL_v1_labels():
152    palettes = [
153        [(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)],
154        [(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)],
155        [(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)],
156    ]
157    paletteLabels = ["First", {"en": "Second", "it": "Seconda"}, None]
158    paletteEntryLabels = ["Foo", "Bar"]
159
160    with pytest.raises(TypeError, match="nameTable is required"):
161        builder.buildCPAL(palettes, paletteLabels=paletteLabels)
162    with pytest.raises(TypeError, match="nameTable is required"):
163        builder.buildCPAL(palettes, paletteEntryLabels=paletteEntryLabels)
164
165    name_table = newTable("name")
166    name_table.names = []
167
168    cpal = builder.buildCPAL(
169        palettes,
170        paletteLabels=paletteLabels,
171        paletteEntryLabels=paletteEntryLabels,
172        nameTable=name_table,
173    )
174
175    assert cpal.tableTag == "CPAL"
176    assert cpal.version == 1
177    assert cpal.numPaletteEntries == 2
178    assert len(cpal.palettes) == 3
179
180    assert cpal.paletteTypes == [cpal.DEFAULT_PALETTE_TYPE] * len(palettes)
181    assert cpal.paletteLabels == [256, 257, cpal.NO_NAME_ID]
182    assert cpal.paletteEntryLabels == [258, 259]
183
184    assert name_table.getDebugName(256) == "First"
185    assert name_table.getDebugName(257) == "Second"
186    assert name_table.getDebugName(258) == "Foo"
187    assert name_table.getDebugName(259) == "Bar"
188
189
190def test_invalid_ColorPaletteType():
191    with pytest.raises(ValueError, match="not a valid ColorPaletteType"):
192        builder.ColorPaletteType(-1)
193    with pytest.raises(ValueError, match="not a valid ColorPaletteType"):
194        builder.ColorPaletteType(4)
195    with pytest.raises(ValueError, match="not a valid ColorPaletteType"):
196        builder.ColorPaletteType("abc")
197
198
199def test_buildCPAL_v1_invalid_args_length():
200    with pytest.raises(ColorLibError, match="Expected 2 paletteTypes, got 1"):
201        builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, 1, 1)]], paletteTypes=[1])
202
203    with pytest.raises(ColorLibError, match="Expected 2 paletteLabels, got 1"):
204        builder.buildCPAL(
205            [[(0, 0, 0, 0)], [(1, 1, 1, 1)]],
206            paletteLabels=["foo"],
207            nameTable=newTable("name"),
208        )
209
210    with pytest.raises(ColorLibError, match="Expected 1 paletteEntryLabels, got 0"):
211        cpal = builder.buildCPAL(
212            [[(0, 0, 0, 0)], [(1, 1, 1, 1)]],
213            paletteEntryLabels=[],
214            nameTable=newTable("name"),
215        )
216
217
218def test_buildCPAL_invalid_color():
219    with pytest.raises(
220        ColorLibError,
221        match=r"In palette\[0\]\[1\]: expected \(R, G, B, A\) tuple, got \(1, 1, 1\)",
222    ):
223        builder.buildCPAL([[(1, 1, 1, 1), (1, 1, 1)]])
224
225    with pytest.raises(
226        ColorLibError,
227        match=(
228            r"palette\[1\]\[0\] has invalid out-of-range "
229            r"\[0..1\] color: \(1, 1, -1, 2\)"
230        ),
231    ):
232        builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]])
233
234
235def test_buildPaintSolid():
236    p = _buildPaint((ot.PaintFormat.PaintSolid, 0))
237    assert p.Format == ot.PaintFormat.PaintSolid
238    assert p.PaletteIndex == 0
239    assert p.Alpha == 1.0
240
241
242def test_buildPaintSolid_Alpha():
243    p = _buildPaint((ot.PaintFormat.PaintSolid, 1, 0.5))
244    assert p.Format == ot.PaintFormat.PaintSolid
245    assert p.PaletteIndex == 1
246    assert p.Alpha == 0.5
247
248
249def test_buildPaintVarSolid():
250    p = _buildPaint((ot.PaintFormat.PaintVarSolid, 3, 0.5, 2))
251    assert p.Format == ot.PaintFormat.PaintVarSolid
252    assert p.PaletteIndex == 3
253    assert p.Alpha == 0.5
254    assert p.VarIndexBase == 2
255
256
257def test_buildVarColorStop_DefaultAlpha():
258    s = _build(ot.ColorStop, (0.1, 2))
259    assert s.StopOffset == 0.1
260    assert s.PaletteIndex == 2
261    assert s.Alpha == builder._DEFAULT_ALPHA
262
263
264def test_buildVarColorStop_DefaultAlpha():
265    s = _build(ot.VarColorStop, (0.1, 2))
266    assert s.StopOffset == 0.1
267    assert s.PaletteIndex == 2
268    assert s.Alpha == builder._DEFAULT_ALPHA
269
270
271def test_buildColorStop():
272    s = _build(ot.ColorStop, {"StopOffset": 0.2, "PaletteIndex": 3, "Alpha": 0.4})
273    assert s.StopOffset == 0.2
274    assert s.PaletteIndex == 3
275    assert s.Alpha == 0.4
276
277
278def test_buildColorStop_Variable():
279    s = _build(
280        ot.VarColorStop,
281        {
282            "StopOffset": 0.0,
283            "PaletteIndex": 0,
284            "Alpha": 0.3,
285            "VarIndexBase": 1,
286        },
287    )
288    assert s.StopOffset == 0.0
289    assert s.PaletteIndex == 0
290    assert s.Alpha == 0.3
291    assert s.VarIndexBase == 1
292
293
294def test_buildColorLine_StopList():
295    stops = [(0.0, 0), (0.5, 1), (1.0, 2)]
296
297    cline = _build(ot.ColorLine, {"ColorStop": stops})
298    assert cline.Extend == builder.ExtendMode.PAD
299    assert cline.StopCount == 3
300    assert [(cs.StopOffset, cs.PaletteIndex) for cs in cline.ColorStop] == stops
301
302    cline = _build(ot.ColorLine, {"Extend": "pad", "ColorStop": stops})
303    assert cline.Extend == builder.ExtendMode.PAD
304
305    cline = _build(
306        ot.ColorLine, {"ColorStop": stops, "Extend": builder.ExtendMode.REPEAT}
307    )
308    assert cline.Extend == builder.ExtendMode.REPEAT
309
310    cline = _build(
311        ot.ColorLine, {"ColorStop": stops, "Extend": builder.ExtendMode.REFLECT}
312    )
313    assert cline.Extend == builder.ExtendMode.REFLECT
314
315    cline = _build(
316        ot.ColorLine, {"ColorStop": [_build(ot.ColorStop, s) for s in stops]}
317    )
318    assert [(cs.StopOffset, cs.PaletteIndex) for cs in cline.ColorStop] == stops
319
320
321def test_buildVarColorLine_StopMap():
322    stops = [
323        {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 0.5, "VarIndexBase": 1},
324        {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 0.3, "VarIndexBase": 3},
325    ]
326    cline = _build(ot.VarColorLine, {"ColorStop": stops})
327    assert [
328        {
329            "StopOffset": cs.StopOffset,
330            "PaletteIndex": cs.PaletteIndex,
331            "Alpha": cs.Alpha,
332            "VarIndexBase": cs.VarIndexBase,
333        }
334        for cs in cline.ColorStop
335    ] == stops
336
337
338def checkBuildAffine2x3(cls, variable=False):
339    matrix = _build(cls, (1.5, 0, 0.5, 2.0, 1.0, -3.0))
340    assert matrix.xx == 1.5
341    assert matrix.yx == 0.0
342    assert matrix.xy == 0.5
343    assert matrix.yy == 2.0
344    assert matrix.dx == 1.0
345    assert matrix.dy == -3.0
346    if variable:
347        assert matrix.VarIndexBase == 0xFFFFFFFF
348
349
350def test_buildAffine2x3():
351    checkBuildAffine2x3(ot.Affine2x3)
352
353
354def test_buildVarAffine2x3():
355    checkBuildAffine2x3(ot.VarAffine2x3, variable=True)
356
357
358def _sample_stops(variable):
359    cls = ot.ColorStop if not variable else ot.VarColorStop
360    stop_sources = [
361        {"StopOffset": 0.0, "PaletteIndex": 0},
362        {"StopOffset": 0.5, "PaletteIndex": 1},
363        {"StopOffset": 1.0, "PaletteIndex": 2, "Alpha": 0.8},
364    ]
365    if variable:
366        for i, src in enumerate(stop_sources, start=123):
367            src["VarIndexBase"] = i
368    return [_build(cls, src) for src in stop_sources]
369
370
371def _is_var(fmt):
372    return fmt.name.startswith("PaintVar")
373
374
375def _is_around_center(fmt):
376    return fmt.name.endswith("AroundCenter")
377
378
379def _is_uniform_scale(fmt):
380    return "ScaleUniform" in fmt.name
381
382
383def checkBuildPaintLinearGradient(fmt):
384    variable = _is_var(fmt)
385    color_stops = _sample_stops(variable)
386
387    x0, y0, x1, y1, x2, y2 = (1, 2, 3, 4, 5, 6)
388    source = {
389        "Format": fmt,
390        "ColorLine": {"ColorStop": color_stops},
391        "x0": x0,
392        "y0": y0,
393        "x1": x1,
394        "y1": y1,
395        "x2": x2,
396        "y2": y2,
397    }
398    if variable:
399        source["VarIndexBase"] = 7
400    gradient = _buildPaint(source)
401    assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
402    assert gradient.ColorLine.ColorStop == color_stops
403
404    gradient = _buildPaint(gradient)
405    assert (gradient.x0, gradient.y0) == (1, 2)
406    assert (gradient.x1, gradient.y1) == (3, 4)
407    assert (gradient.x2, gradient.y2) == (5, 6)
408    if variable:
409        assert gradient.VarIndexBase == 7
410
411
412def test_buildPaintLinearGradient():
413    assert not _is_var(ot.PaintFormat.PaintLinearGradient)
414    checkBuildPaintLinearGradient(ot.PaintFormat.PaintLinearGradient)
415
416
417def test_buildPaintVarLinearGradient():
418    assert _is_var(ot.PaintFormat.PaintVarLinearGradient)
419    checkBuildPaintLinearGradient(ot.PaintFormat.PaintVarLinearGradient)
420
421
422def checkBuildPaintRadialGradient(fmt):
423    variable = _is_var(fmt)
424    color_stops = _sample_stops(variable)
425    line_cls = ot.VarColorLine if variable else ot.ColorLine
426
427    color_line = _build(
428        line_cls, {"ColorStop": color_stops, "Extend": builder.ExtendMode.REPEAT}
429    )
430    c0 = (100, 200)
431    c1 = (150, 250)
432    r0 = 10
433    r1 = 5
434    varIndexBase = 0
435
436    source = [fmt, color_line, *c0, r0, *c1, r1]
437    if variable:
438        source.append(varIndexBase)
439
440    gradient = _build(ot.Paint, tuple(source))
441    assert gradient.Format == fmt
442    assert gradient.ColorLine == color_line
443    assert (gradient.x0, gradient.y0) == c0
444    assert (gradient.x1, gradient.y1) == c1
445    assert gradient.r0 == r0
446    assert gradient.r1 == r1
447    if variable:
448        assert gradient.VarIndexBase == varIndexBase
449
450    source = {
451        "Format": fmt,
452        "ColorLine": {"ColorStop": color_stops},
453        "x0": c0[0],
454        "y0": c0[1],
455        "x1": c1[0],
456        "y1": c1[1],
457        "r0": r0,
458        "r1": r1,
459    }
460    if variable:
461        source["VarIndexBase"] = varIndexBase
462    gradient = _build(ot.Paint, source)
463    assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
464    assert gradient.ColorLine.ColorStop == color_stops
465    assert (gradient.x0, gradient.y0) == c0
466    assert (gradient.x1, gradient.y1) == c1
467    assert gradient.r0 == r0
468    assert gradient.r1 == r1
469    if variable:
470        assert gradient.VarIndexBase == varIndexBase
471
472
473def test_buildPaintRadialGradient():
474    assert not _is_var(ot.PaintFormat.PaintRadialGradient)
475    checkBuildPaintRadialGradient(ot.PaintFormat.PaintRadialGradient)
476
477
478def test_buildPaintVarRadialGradient():
479    assert _is_var(ot.PaintFormat.PaintVarRadialGradient)
480    checkBuildPaintRadialGradient(ot.PaintFormat.PaintVarRadialGradient)
481
482
483def checkPaintSweepGradient(fmt):
484    variable = _is_var(fmt)
485    source = {
486        "Format": fmt,
487        "ColorLine": {"ColorStop": _sample_stops(variable)},
488        "centerX": 127,
489        "centerY": 129,
490        "startAngle": 15,
491        "endAngle": 42,
492    }
493    if variable:
494        source["VarIndexBase"] = 666
495    paint = _buildPaint(source)
496
497    assert paint.Format == fmt
498    assert paint.centerX == 127
499    assert paint.centerY == 129
500    assert paint.startAngle == 15
501    assert paint.endAngle == 42
502    if variable:
503        assert paint.VarIndexBase == 666
504
505
506def test_buildPaintSweepGradient():
507    assert not _is_var(ot.PaintFormat.PaintSweepGradient)
508    checkPaintSweepGradient(ot.PaintFormat.PaintSweepGradient)
509
510
511def test_buildPaintVarSweepGradient():
512    assert _is_var(ot.PaintFormat.PaintVarSweepGradient)
513    checkPaintSweepGradient(ot.PaintFormat.PaintVarSweepGradient)
514
515
516def test_buildPaintGlyph_Solid():
517    layer = _build(
518        ot.Paint,
519        (
520            ot.PaintFormat.PaintGlyph,
521            (
522                ot.PaintFormat.PaintSolid,
523                2,
524            ),
525            "a",
526        ),
527    )
528    assert layer.Format == ot.PaintFormat.PaintGlyph
529    assert layer.Glyph == "a"
530    assert layer.Paint.Format == ot.PaintFormat.PaintSolid
531    assert layer.Paint.PaletteIndex == 2
532
533    layer = _build(
534        ot.Paint,
535        (
536            ot.PaintFormat.PaintGlyph,
537            (ot.PaintFormat.PaintSolid, 3, 0.9),
538            "a",
539        ),
540    )
541    assert layer.Paint.Format == ot.PaintFormat.PaintSolid
542    assert layer.Paint.PaletteIndex == 3
543    assert layer.Paint.Alpha == 0.9
544
545
546def test_buildPaintGlyph_VarLinearGradient():
547    layer = _build(
548        ot.Paint,
549        {
550            "Format": ot.PaintFormat.PaintGlyph,
551            "Glyph": "a",
552            "Paint": {
553                "Format": ot.PaintFormat.PaintVarLinearGradient,
554                "ColorLine": {"ColorStop": [(0.0, 3), (1.0, 4)]},
555                "x0": 100,
556                "y0": 200,
557                "x1": 150,
558                "y1": 250,
559            },
560        },
561    )
562
563    assert layer.Format == ot.PaintFormat.PaintGlyph
564    assert layer.Glyph == "a"
565    assert layer.Paint.Format == ot.PaintFormat.PaintVarLinearGradient
566    assert layer.Paint.ColorLine.ColorStop[0].StopOffset == 0.0
567    assert layer.Paint.ColorLine.ColorStop[0].PaletteIndex == 3
568    assert layer.Paint.ColorLine.ColorStop[1].StopOffset == 1.0
569    assert layer.Paint.ColorLine.ColorStop[1].PaletteIndex == 4
570    assert layer.Paint.x0 == 100
571    assert layer.Paint.y0 == 200
572    assert layer.Paint.x1 == 150
573    assert layer.Paint.y1 == 250
574
575
576def test_buildPaintGlyph_RadialGradient():
577    layer = _build(
578        ot.Paint,
579        (
580            int(ot.PaintFormat.PaintGlyph),
581            (
582                ot.PaintFormat.PaintRadialGradient,
583                (
584                    "pad",
585                    [
586                        (0.0, 5),
587                        {"StopOffset": 0.5, "PaletteIndex": 6, "Alpha": 0.8},
588                        (1.0, 7),
589                    ],
590                ),
591                50,
592                50,
593                30,
594                75,
595                75,
596                10,
597            ),
598            "a",
599        ),
600    )
601    assert layer.Format == ot.PaintFormat.PaintGlyph
602    assert layer.Paint.Format == ot.PaintFormat.PaintRadialGradient
603    assert layer.Paint.ColorLine.ColorStop[0].StopOffset == 0.0
604    assert layer.Paint.ColorLine.ColorStop[0].PaletteIndex == 5
605    assert layer.Paint.ColorLine.ColorStop[1].StopOffset == 0.5
606    assert layer.Paint.ColorLine.ColorStop[1].PaletteIndex == 6
607    assert layer.Paint.ColorLine.ColorStop[1].Alpha == 0.8
608    assert layer.Paint.ColorLine.ColorStop[2].StopOffset == 1.0
609    assert layer.Paint.ColorLine.ColorStop[2].PaletteIndex == 7
610    assert layer.Paint.x0 == 50
611    assert layer.Paint.y0 == 50
612    assert layer.Paint.r0 == 30
613    assert layer.Paint.x1 == 75
614    assert layer.Paint.y1 == 75
615    assert layer.Paint.r1 == 10
616
617
618def test_buildPaintGlyph_Dict_Solid():
619    layer = _build(
620        ot.Paint,
621        (
622            int(ot.PaintFormat.PaintGlyph),
623            (int(ot.PaintFormat.PaintSolid), 1),
624            "a",
625        ),
626    )
627    assert layer.Format == ot.PaintFormat.PaintGlyph
628    assert layer.Format == ot.PaintFormat.PaintGlyph
629    assert layer.Glyph == "a"
630    assert layer.Paint.Format == ot.PaintFormat.PaintSolid
631    assert layer.Paint.PaletteIndex == 1
632
633
634def test_buildPaintGlyph_Dict_VarLinearGradient():
635    layer = _build(
636        ot.Paint,
637        {
638            "Format": ot.PaintFormat.PaintGlyph,
639            "Glyph": "a",
640            "Paint": {
641                "Format": int(ot.PaintFormat.PaintVarLinearGradient),
642                "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]},
643                "x0": 0,
644                "y0": 0,
645                "x1": 10,
646                "y1": 10,
647            },
648        },
649    )
650    assert layer.Format == ot.PaintFormat.PaintGlyph
651    assert layer.Glyph == "a"
652    assert layer.Paint.Format == ot.PaintFormat.PaintVarLinearGradient
653    assert layer.Paint.ColorLine.ColorStop[0].StopOffset == 0.0
654
655
656def test_buildPaintGlyph_Dict_RadialGradient():
657    layer = _buildPaint(
658        {
659            "Glyph": "a",
660            "Paint": {
661                "Format": int(ot.PaintFormat.PaintRadialGradient),
662                "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]},
663                "x0": 0,
664                "y0": 0,
665                "r0": 4,
666                "x1": 10,
667                "y1": 10,
668                "r1": 0,
669            },
670            "Format": int(ot.PaintFormat.PaintGlyph),
671        },
672    )
673    assert layer.Paint.Format == ot.PaintFormat.PaintRadialGradient
674    assert layer.Paint.r0 == 4
675
676
677def test_buildPaintColrGlyph():
678    paint = _buildPaint((int(ot.PaintFormat.PaintColrGlyph), "a"))
679    assert paint.Format == ot.PaintFormat.PaintColrGlyph
680    assert paint.Glyph == "a"
681
682
683def checkBuildPaintTransform(fmt):
684    variable = _is_var(fmt)
685    if variable:
686        affine_cls = ot.VarAffine2x3
687    else:
688        affine_cls = ot.Affine2x3
689
690    affine_src = [1, 2, 3, 4, 5, 6]
691    if variable:
692        affine_src.append(7)
693
694    paint = _buildPaint(
695        (
696            int(fmt),
697            (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0, 1.0), "a"),
698            _build(affine_cls, tuple(affine_src)),
699        ),
700    )
701
702    assert paint.Format == fmt
703    assert paint.Paint.Format == ot.PaintFormat.PaintGlyph
704    assert paint.Paint.Paint.Format == ot.PaintFormat.PaintSolid
705
706    assert paint.Transform.xx == 1.0
707    assert paint.Transform.yx == 2.0
708    assert paint.Transform.xy == 3.0
709    assert paint.Transform.yy == 4.0
710    assert paint.Transform.dx == 5.0
711    assert paint.Transform.dy == 6.0
712    if variable:
713        assert paint.Transform.VarIndexBase == 7
714
715    affine_src = [1, 2, 3, 0.3333, 10, 10]
716    if variable:
717        affine_src.append(456)  # VarIndexBase
718    paint = _build(
719        ot.Paint,
720        {
721            "Format": fmt,
722            "Transform": tuple(affine_src),
723            "Paint": {
724                "Format": int(ot.PaintFormat.PaintRadialGradient),
725                "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]},
726                "x0": 100,
727                "y0": 101,
728                "x1": 102,
729                "y1": 103,
730                "r0": 0,
731                "r1": 50,
732            },
733        },
734    )
735
736    assert paint.Format == fmt
737    assert paint.Transform.xx == 1.0
738    assert paint.Transform.yx == 2.0
739    assert paint.Transform.xy == 3.0
740    assert paint.Transform.yy == 0.3333
741    assert paint.Transform.dx == 10
742    assert paint.Transform.dy == 10
743    if variable:
744        assert paint.Transform.VarIndexBase == 456
745    assert paint.Paint.Format == ot.PaintFormat.PaintRadialGradient
746
747
748def test_buildPaintTransform():
749    assert not _is_var(ot.PaintFormat.PaintTransform)
750    checkBuildPaintTransform(ot.PaintFormat.PaintTransform)
751
752
753def test_buildPaintVarTransform():
754    assert _is_var(ot.PaintFormat.PaintVarTransform)
755    checkBuildPaintTransform(ot.PaintFormat.PaintVarTransform)
756
757
758def test_buildPaintComposite():
759    composite = _build(
760        ot.Paint,
761        {
762            "Format": int(ot.PaintFormat.PaintComposite),
763            "CompositeMode": "src_over",
764            "SourcePaint": {
765                "Format": ot.PaintFormat.PaintComposite,
766                "CompositeMode": "src_over",
767                "SourcePaint": {
768                    "Format": int(ot.PaintFormat.PaintGlyph),
769                    "Glyph": "c",
770                    "Paint": (ot.PaintFormat.PaintSolid, 2),
771                },
772                "BackdropPaint": {
773                    "Format": int(ot.PaintFormat.PaintGlyph),
774                    "Glyph": "b",
775                    "Paint": (ot.PaintFormat.PaintSolid, 1),
776                },
777            },
778            "BackdropPaint": {
779                "Format": ot.PaintFormat.PaintGlyph,
780                "Glyph": "a",
781                "Paint": {
782                    "Format": ot.PaintFormat.PaintSolid,
783                    "PaletteIndex": 0,
784                    "Alpha": 0.5,
785                },
786            },
787        },
788    )
789
790    assert composite.Format == ot.PaintFormat.PaintComposite
791    assert composite.SourcePaint.Format == ot.PaintFormat.PaintComposite
792    assert composite.SourcePaint.SourcePaint.Format == ot.PaintFormat.PaintGlyph
793    assert composite.SourcePaint.SourcePaint.Glyph == "c"
794    assert composite.SourcePaint.SourcePaint.Paint.Format == ot.PaintFormat.PaintSolid
795    assert composite.SourcePaint.SourcePaint.Paint.PaletteIndex == 2
796    assert composite.SourcePaint.CompositeMode == ot.CompositeMode.SRC_OVER
797    assert composite.SourcePaint.BackdropPaint.Format == ot.PaintFormat.PaintGlyph
798    assert composite.SourcePaint.BackdropPaint.Glyph == "b"
799    assert composite.SourcePaint.BackdropPaint.Paint.Format == ot.PaintFormat.PaintSolid
800    assert composite.SourcePaint.BackdropPaint.Paint.PaletteIndex == 1
801    assert composite.CompositeMode == ot.CompositeMode.SRC_OVER
802    assert composite.BackdropPaint.Format == ot.PaintFormat.PaintGlyph
803    assert composite.BackdropPaint.Glyph == "a"
804    assert composite.BackdropPaint.Paint.Format == ot.PaintFormat.PaintSolid
805    assert composite.BackdropPaint.Paint.PaletteIndex == 0
806    assert composite.BackdropPaint.Paint.Alpha == 0.5
807
808
809def checkBuildPaintTranslate(fmt):
810    variable = _is_var(fmt)
811
812    source = {
813        "Format": fmt,
814        "Paint": (
815            ot.PaintFormat.PaintGlyph,
816            (ot.PaintFormat.PaintSolid, 0, 1.0),
817            "a",
818        ),
819        "dx": 123,
820        "dy": -345,
821    }
822    if variable:
823        source["VarIndexBase"] = 678
824
825    paint = _build(ot.Paint, source)
826
827    assert paint.Format == fmt
828    assert paint.Paint.Format == ot.PaintFormat.PaintGlyph
829    assert paint.dx == 123
830    assert paint.dy == -345
831    if variable:
832        assert paint.VarIndexBase == 678
833
834
835def test_buildPaintTranslate():
836    assert not _is_var(ot.PaintFormat.PaintTranslate)
837    checkBuildPaintTranslate(ot.PaintFormat.PaintTranslate)
838
839
840def test_buildPaintVarTranslate():
841    assert _is_var(ot.PaintFormat.PaintVarTranslate)
842    checkBuildPaintTranslate(ot.PaintFormat.PaintVarTranslate)
843
844
845def checkBuildPaintScale(fmt):
846    variable = _is_var(fmt)
847    around_center = _is_around_center(fmt)
848    uniform = _is_uniform_scale(fmt)
849
850    source = {
851        "Format": fmt,
852        "Paint": (
853            ot.PaintFormat.PaintGlyph,
854            (ot.PaintFormat.PaintSolid, 0, 1.0),
855            "a",
856        ),
857    }
858    if uniform:
859        source["scale"] = 1.5
860    else:
861        source["scaleX"] = 1.0
862        source["scaleY"] = 2.0
863    if around_center:
864        source["centerX"] = 127
865        source["centerY"] = 129
866    if variable:
867        source["VarIndexBase"] = 666
868
869    paint = _build(ot.Paint, source)
870
871    assert paint.Format == fmt
872    assert paint.Paint.Format == ot.PaintFormat.PaintGlyph
873    if uniform:
874        assert paint.scale == 1.5
875    else:
876        assert paint.scaleX == 1.0
877        assert paint.scaleY == 2.0
878    if around_center:
879        assert paint.centerX == 127
880        assert paint.centerY == 129
881    if variable:
882        assert paint.VarIndexBase == 666
883
884
885def test_buildPaintScale():
886    assert not _is_var(ot.PaintFormat.PaintScale)
887    assert not _is_uniform_scale(ot.PaintFormat.PaintScale)
888    assert not _is_around_center(ot.PaintFormat.PaintScale)
889    checkBuildPaintScale(ot.PaintFormat.PaintScale)
890
891
892def test_buildPaintVarScale():
893    assert _is_var(ot.PaintFormat.PaintVarScale)
894    assert not _is_uniform_scale(ot.PaintFormat.PaintVarScale)
895    assert not _is_around_center(ot.PaintFormat.PaintVarScale)
896    checkBuildPaintScale(ot.PaintFormat.PaintVarScale)
897
898
899def test_buildPaintScaleAroundCenter():
900    assert not _is_var(ot.PaintFormat.PaintScaleAroundCenter)
901    assert not _is_uniform_scale(ot.PaintFormat.PaintScaleAroundCenter)
902    assert _is_around_center(ot.PaintFormat.PaintScaleAroundCenter)
903    checkBuildPaintScale(ot.PaintFormat.PaintScaleAroundCenter)
904
905
906def test_buildPaintVarScaleAroundCenter():
907    assert _is_var(ot.PaintFormat.PaintVarScaleAroundCenter)
908    assert not _is_uniform_scale(ot.PaintFormat.PaintScaleAroundCenter)
909    assert _is_around_center(ot.PaintFormat.PaintVarScaleAroundCenter)
910    checkBuildPaintScale(ot.PaintFormat.PaintVarScaleAroundCenter)
911
912
913def test_buildPaintScaleUniform():
914    assert not _is_var(ot.PaintFormat.PaintScaleUniform)
915    assert _is_uniform_scale(ot.PaintFormat.PaintScaleUniform)
916    assert not _is_around_center(ot.PaintFormat.PaintScaleUniform)
917    checkBuildPaintScale(ot.PaintFormat.PaintScaleUniform)
918
919
920def test_buildPaintVarScaleUniform():
921    assert _is_var(ot.PaintFormat.PaintVarScaleUniform)
922    assert _is_uniform_scale(ot.PaintFormat.PaintVarScaleUniform)
923    assert not _is_around_center(ot.PaintFormat.PaintVarScaleUniform)
924    checkBuildPaintScale(ot.PaintFormat.PaintVarScaleUniform)
925
926
927def test_buildPaintScaleUniformAroundCenter():
928    assert not _is_var(ot.PaintFormat.PaintScaleUniformAroundCenter)
929    assert _is_uniform_scale(ot.PaintFormat.PaintScaleUniformAroundCenter)
930    assert _is_around_center(ot.PaintFormat.PaintScaleUniformAroundCenter)
931    checkBuildPaintScale(ot.PaintFormat.PaintScaleUniformAroundCenter)
932
933
934def test_buildPaintVarScaleUniformAroundCenter():
935    assert _is_var(ot.PaintFormat.PaintVarScaleUniformAroundCenter)
936    assert _is_uniform_scale(ot.PaintFormat.PaintVarScaleUniformAroundCenter)
937    assert _is_around_center(ot.PaintFormat.PaintVarScaleUniformAroundCenter)
938    checkBuildPaintScale(ot.PaintFormat.PaintVarScaleUniformAroundCenter)
939
940
941def checkBuildPaintRotate(fmt):
942    variable = _is_var(fmt)
943    around_center = _is_around_center(fmt)
944
945    source = {
946        "Format": fmt,
947        "Paint": (
948            ot.PaintFormat.PaintGlyph,
949            (ot.PaintFormat.PaintSolid, 0, 1.0),
950            "a",
951        ),
952        "angle": 15,
953    }
954    if around_center:
955        source["centerX"] = 127
956        source["centerY"] = 129
957
958    paint = _build(ot.Paint, source)
959
960    assert paint.Format == fmt
961    assert paint.Paint.Format == ot.PaintFormat.PaintGlyph
962    assert paint.angle == 15
963    if around_center:
964        assert paint.centerX == 127
965        assert paint.centerY == 129
966    if variable:
967        assert paint.VarIndexBase == 0xFFFFFFFF
968
969
970def test_buildPaintRotate():
971    assert not _is_var(ot.PaintFormat.PaintRotate)
972    assert not _is_around_center(ot.PaintFormat.PaintRotate)
973    checkBuildPaintRotate(ot.PaintFormat.PaintRotate)
974
975
976def test_buildPaintVarRotate():
977    assert _is_var(ot.PaintFormat.PaintVarRotate)
978    assert not _is_around_center(ot.PaintFormat.PaintVarRotate)
979    checkBuildPaintRotate(ot.PaintFormat.PaintVarRotate)
980
981
982def test_buildPaintRotateAroundCenter():
983    assert not _is_var(ot.PaintFormat.PaintRotateAroundCenter)
984    assert _is_around_center(ot.PaintFormat.PaintRotateAroundCenter)
985    checkBuildPaintRotate(ot.PaintFormat.PaintRotateAroundCenter)
986
987
988def test_buildPaintVarRotateAroundCenter():
989    assert _is_var(ot.PaintFormat.PaintVarRotateAroundCenter)
990    assert _is_around_center(ot.PaintFormat.PaintVarRotateAroundCenter)
991    checkBuildPaintRotate(ot.PaintFormat.PaintVarRotateAroundCenter)
992
993
994def checkBuildPaintSkew(fmt):
995    variable = _is_var(fmt)
996    around_center = _is_around_center(fmt)
997
998    source = {
999        "Format": fmt,
1000        "Paint": (
1001            ot.PaintFormat.PaintGlyph,
1002            (ot.PaintFormat.PaintSolid, 0, 1.0),
1003            "a",
1004        ),
1005        "xSkewAngle": 15,
1006        "ySkewAngle": 42,
1007    }
1008    if around_center:
1009        source["centerX"] = 127
1010        source["centerY"] = 129
1011    if variable:
1012        source["VarIndexBase"] = 0
1013
1014    paint = _build(ot.Paint, source)
1015
1016    assert paint.Format == fmt
1017    assert paint.Paint.Format == ot.PaintFormat.PaintGlyph
1018    assert paint.xSkewAngle == 15
1019    assert paint.ySkewAngle == 42
1020    if around_center:
1021        assert paint.centerX == 127
1022        assert paint.centerY == 129
1023    if variable:
1024        assert paint.VarIndexBase == 0
1025
1026
1027def test_buildPaintSkew():
1028    assert not _is_var(ot.PaintFormat.PaintSkew)
1029    assert not _is_around_center(ot.PaintFormat.PaintSkew)
1030    checkBuildPaintSkew(ot.PaintFormat.PaintSkew)
1031
1032
1033def test_buildPaintVarSkew():
1034    assert _is_var(ot.PaintFormat.PaintVarSkew)
1035    assert not _is_around_center(ot.PaintFormat.PaintVarSkew)
1036    checkBuildPaintSkew(ot.PaintFormat.PaintVarSkew)
1037
1038
1039def test_buildPaintSkewAroundCenter():
1040    assert not _is_var(ot.PaintFormat.PaintSkewAroundCenter)
1041    assert _is_around_center(ot.PaintFormat.PaintSkewAroundCenter)
1042    checkBuildPaintSkew(ot.PaintFormat.PaintSkewAroundCenter)
1043
1044
1045def test_buildPaintVarSkewAroundCenter():
1046    assert _is_var(ot.PaintFormat.PaintVarSkewAroundCenter)
1047    assert _is_around_center(ot.PaintFormat.PaintVarSkewAroundCenter)
1048    checkBuildPaintSkew(ot.PaintFormat.PaintVarSkewAroundCenter)
1049
1050
1051def test_buildColrV1():
1052    colorGlyphs = {
1053        "a": (
1054            ot.PaintFormat.PaintColrLayers,
1055            [
1056                (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0), "b"),
1057                (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintVarSolid, 1), "c"),
1058            ],
1059        ),
1060        "d": (
1061            ot.PaintFormat.PaintColrLayers,
1062            [
1063                (
1064                    ot.PaintFormat.PaintGlyph,
1065                    {
1066                        "Format": int(ot.PaintFormat.PaintSolid),
1067                        "PaletteIndex": 2,
1068                        "Alpha": 0.8,
1069                    },
1070                    "e",
1071                ),
1072                (
1073                    ot.PaintFormat.PaintGlyph,
1074                    {
1075                        "Format": int(ot.PaintFormat.PaintVarRadialGradient),
1076                        "ColorLine": {
1077                            "ColorStop": [(0.0, 3), (1.0, 4)],
1078                            "Extend": "reflect",
1079                        },
1080                        "x0": 0,
1081                        "y0": 0,
1082                        "x1": 0,
1083                        "y1": 0,
1084                        "r0": 10,
1085                        "r1": 0,
1086                    },
1087                    "f",
1088                ),
1089            ],
1090        ),
1091        "g": (
1092            ot.PaintFormat.PaintColrLayers,
1093            [(ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 5), "h")],
1094        ),
1095    }
1096    glyphMap = {
1097        ".notdef": 0,
1098        "a": 4,
1099        "b": 3,
1100        "c": 2,
1101        "d": 1,
1102        "e": 5,
1103        "f": 6,
1104        "g": 7,
1105        "h": 8,
1106    }
1107
1108    # TODO(anthrotype) should we split into two tests? - seems two distinct validations
1109    layers, baseGlyphs = builder.buildColrV1(colorGlyphs, glyphMap)
1110    assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
1111    assert baseGlyphs.BaseGlyphPaintRecord[0].BaseGlyph == "d"
1112    assert baseGlyphs.BaseGlyphPaintRecord[1].BaseGlyph == "a"
1113    assert baseGlyphs.BaseGlyphPaintRecord[2].BaseGlyph == "g"
1114
1115    layers, baseGlyphs = builder.buildColrV1(colorGlyphs)
1116    assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
1117    assert baseGlyphs.BaseGlyphPaintRecord[0].BaseGlyph == "a"
1118    assert baseGlyphs.BaseGlyphPaintRecord[1].BaseGlyph == "d"
1119    assert baseGlyphs.BaseGlyphPaintRecord[2].BaseGlyph == "g"
1120
1121
1122def test_buildColrV1_more_than_255_paints():
1123    num_paints = 364
1124    colorGlyphs = {
1125        "a": (
1126            ot.PaintFormat.PaintColrLayers,
1127            [
1128                {
1129                    "Format": int(ot.PaintFormat.PaintGlyph),
1130                    "Paint": (ot.PaintFormat.PaintSolid, 0),
1131                    "Glyph": name,
1132                }
1133                for name in (f"glyph{i}" for i in range(num_paints))
1134            ],
1135        ),
1136    }
1137    layers, baseGlyphs = builder.buildColrV1(colorGlyphs)
1138    paints = layers.Paint
1139
1140    assert len(paints) == num_paints + 1
1141
1142    assert all(paints[i].Format == ot.PaintFormat.PaintGlyph for i in range(255))
1143
1144    assert paints[255].Format == ot.PaintFormat.PaintColrLayers
1145    assert paints[255].FirstLayerIndex == 0
1146    assert paints[255].NumLayers == 255
1147
1148    assert all(
1149        paints[i].Format == ot.PaintFormat.PaintGlyph
1150        for i in range(256, num_paints + 1)
1151    )
1152
1153    assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
1154    assert baseGlyphs.BaseGlyphPaintRecord[0].BaseGlyph == "a"
1155    assert (
1156        baseGlyphs.BaseGlyphPaintRecord[0].Paint.Format
1157        == ot.PaintFormat.PaintColrLayers
1158    )
1159    assert baseGlyphs.BaseGlyphPaintRecord[0].Paint.FirstLayerIndex == 255
1160    assert baseGlyphs.BaseGlyphPaintRecord[0].Paint.NumLayers == num_paints + 1 - 255
1161
1162
1163def test_split_color_glyphs_by_version():
1164    layerBuilder = LayerListBuilder()
1165    colorGlyphs = {
1166        "a": [
1167            ("b", 0),
1168            ("c", 1),
1169            ("d", 2),
1170            ("e", 3),
1171        ]
1172    }
1173
1174    colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs)
1175
1176    assert colorGlyphsV0 == {"a": [("b", 0), ("c", 1), ("d", 2), ("e", 3)]}
1177    assert not colorGlyphsV1
1178
1179    colorGlyphs = {"a": (ot.PaintFormat.PaintGlyph, 0, "b")}
1180
1181    colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs)
1182
1183    assert not colorGlyphsV0
1184    assert colorGlyphsV1 == colorGlyphs
1185
1186    colorGlyphs = {
1187        "a": [("b", 0)],
1188        "c": [
1189            ("d", 1),
1190            (
1191                "e",
1192                {
1193                    "format": 3,
1194                    "colorLine": {"stops": [(0.0, 2), (1.0, 3)]},
1195                    "p0": (0, 0),
1196                    "p1": (10, 10),
1197                },
1198            ),
1199        ],
1200    }
1201
1202    colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs)
1203
1204    assert colorGlyphsV0 == {"a": [("b", 0)]}
1205    assert "a" not in colorGlyphsV1
1206    assert "c" in colorGlyphsV1
1207    assert len(colorGlyphsV1["c"]) == 2
1208
1209
1210def assertIsColrV1(colr):
1211    assert colr.version == 1
1212    assert not hasattr(colr, "ColorLayers")
1213    assert hasattr(colr, "table")
1214    assert isinstance(colr.table, ot.COLR)
1215
1216
1217def assertNoV0Content(colr):
1218    assert colr.table.BaseGlyphRecordCount == 0
1219    assert colr.table.BaseGlyphRecordArray is None
1220    assert colr.table.LayerRecordCount == 0
1221    assert colr.table.LayerRecordArray is None
1222
1223
1224def test_build_layerv1list_empty():
1225    # Nobody uses PaintColrLayers, no layerlist
1226    colr = builder.buildCOLR(
1227        {
1228            # BaseGlyph, tuple form
1229            "a": (
1230                int(ot.PaintFormat.PaintGlyph),
1231                (int(ot.PaintFormat.PaintSolid), 2, 0.8),
1232                "b",
1233            ),
1234            # BaseGlyph, map form
1235            "b": {
1236                "Format": int(ot.PaintFormat.PaintGlyph),
1237                "Paint": {
1238                    "Format": int(ot.PaintFormat.PaintLinearGradient),
1239                    "ColorLine": {
1240                        "ColorStop": [(0.0, 2), (1.0, 3)],
1241                        "Extend": "reflect",
1242                    },
1243                    "x0": 1,
1244                    "y0": 2,
1245                    "x1": 3,
1246                    "y1": 4,
1247                    "x2": 2,
1248                    "y2": 2,
1249                },
1250                "Glyph": "bb",
1251            },
1252        },
1253        version=1,
1254    )
1255
1256    assertIsColrV1(colr)
1257    assertNoV0Content(colr)
1258
1259    # 2 v1 glyphs, none in LayerList
1260    assert colr.table.BaseGlyphList.BaseGlyphCount == 2
1261    assert len(colr.table.BaseGlyphList.BaseGlyphPaintRecord) == 2
1262    assert colr.table.LayerList is None
1263
1264
1265def _paint_names(paints) -> List[str]:
1266    # prints a predictable string from a paint list to enable
1267    # semi-readable assertions on a LayerList order.
1268    result = []
1269    for paint in paints:
1270        if paint.Format == int(ot.PaintFormat.PaintGlyph):
1271            result.append(paint.Glyph)
1272        elif paint.Format == int(ot.PaintFormat.PaintColrLayers):
1273            result.append(
1274                f"Layers[{paint.FirstLayerIndex}:{paint.FirstLayerIndex+paint.NumLayers}]"
1275            )
1276    return result
1277
1278
1279def test_build_layerv1list_simple():
1280    # Two colr glyphs, each with two layers the first of which is common
1281    # All layers use the same solid paint
1282    solid_paint = {
1283        "Format": int(ot.PaintFormat.PaintSolid),
1284        "PaletteIndex": 2,
1285        "Alpha": 0.8,
1286    }
1287    backdrop = {
1288        "Format": int(ot.PaintFormat.PaintGlyph),
1289        "Paint": solid_paint,
1290        "Glyph": "back",
1291    }
1292    a_foreground = {
1293        "Format": int(ot.PaintFormat.PaintGlyph),
1294        "Paint": solid_paint,
1295        "Glyph": "a_fore",
1296    }
1297    b_foreground = {
1298        "Format": int(ot.PaintFormat.PaintGlyph),
1299        "Paint": solid_paint,
1300        "Glyph": "b_fore",
1301    }
1302
1303    # list => PaintColrLayers, contents should land in LayerList
1304    colr = builder.buildCOLR(
1305        {
1306            "a": (
1307                ot.PaintFormat.PaintColrLayers,
1308                [
1309                    backdrop,
1310                    a_foreground,
1311                ],
1312            ),
1313            "b": {
1314                "Format": ot.PaintFormat.PaintColrLayers,
1315                "Layers": [
1316                    backdrop,
1317                    b_foreground,
1318                ],
1319            },
1320        },
1321        version=1,
1322    )
1323
1324    assertIsColrV1(colr)
1325    assertNoV0Content(colr)
1326
1327    # 2 v1 glyphs, 4 paints in LayerList
1328    # A single shared backdrop isn't worth accessing by slice
1329    assert colr.table.BaseGlyphList.BaseGlyphCount == 2
1330    assert len(colr.table.BaseGlyphList.BaseGlyphPaintRecord) == 2
1331    assert colr.table.LayerList.LayerCount == 4
1332    assert _paint_names(colr.table.LayerList.Paint) == [
1333        "back",
1334        "a_fore",
1335        "back",
1336        "b_fore",
1337    ]
1338
1339
1340def test_build_layerv1list_with_sharing():
1341    # Three colr glyphs, each with two layers in common
1342    solid_paint = {
1343        "Format": int(ot.PaintFormat.PaintSolid),
1344        "PaletteIndex": 2,
1345        "Alpha": 0.8,
1346    }
1347    backdrop = [
1348        {
1349            "Format": int(ot.PaintFormat.PaintGlyph),
1350            "Paint": solid_paint,
1351            "Glyph": "back1",
1352        },
1353        {
1354            "Format": ot.PaintFormat.PaintGlyph,
1355            "Paint": solid_paint,
1356            "Glyph": "back2",
1357        },
1358    ]
1359    a_foreground = {
1360        "Format": ot.PaintFormat.PaintGlyph,
1361        "Paint": solid_paint,
1362        "Glyph": "a_fore",
1363    }
1364    b_background = {
1365        "Format": ot.PaintFormat.PaintGlyph,
1366        "Paint": solid_paint,
1367        "Glyph": "b_back",
1368    }
1369    b_foreground = {
1370        "Format": ot.PaintFormat.PaintGlyph,
1371        "Paint": solid_paint,
1372        "Glyph": "b_fore",
1373    }
1374    c_background = {
1375        "Format": ot.PaintFormat.PaintGlyph,
1376        "Paint": solid_paint,
1377        "Glyph": "c_back",
1378    }
1379
1380    # list => PaintColrLayers, which means contents should be in LayerList
1381    colr = builder.buildCOLR(
1382        {
1383            "a": (ot.PaintFormat.PaintColrLayers, backdrop + [a_foreground]),
1384            "b": (
1385                ot.PaintFormat.PaintColrLayers,
1386                [b_background] + backdrop + [b_foreground],
1387            ),
1388            "c": (ot.PaintFormat.PaintColrLayers, [c_background] + backdrop),
1389        },
1390        version=1,
1391    )
1392
1393    assertIsColrV1(colr)
1394    assertNoV0Content(colr)
1395
1396    # 2 v1 glyphs, 4 paints in LayerList
1397    # A single shared backdrop isn't worth accessing by slice
1398    baseGlyphs = colr.table.BaseGlyphList.BaseGlyphPaintRecord
1399    assert colr.table.BaseGlyphList.BaseGlyphCount == 3
1400    assert len(baseGlyphs) == 3
1401    assert _paint_names([b.Paint for b in baseGlyphs]) == [
1402        "Layers[0:3]",
1403        "Layers[3:6]",
1404        "Layers[6:8]",
1405    ]
1406    assert _paint_names(colr.table.LayerList.Paint) == [
1407        "back1",
1408        "back2",
1409        "a_fore",
1410        "b_back",
1411        "Layers[0:2]",
1412        "b_fore",
1413        "c_back",
1414        "Layers[0:2]",
1415    ]
1416    assert colr.table.LayerList.LayerCount == 8
1417
1418
1419def test_build_layerv1list_with_overlaps():
1420    paints = [
1421        {
1422            "Format": ot.PaintFormat.PaintGlyph,
1423            "Paint": {
1424                "Format": ot.PaintFormat.PaintSolid,
1425                "PaletteIndex": 2,
1426                "Alpha": 0.8,
1427            },
1428            "Glyph": c,
1429        }
1430        for c in "abcdefghi"
1431    ]
1432
1433    # list => PaintColrLayers, which means contents should be in LayerList
1434    colr = builder.buildCOLR(
1435        {
1436            "a": (ot.PaintFormat.PaintColrLayers, paints[0:4]),
1437            "b": (ot.PaintFormat.PaintColrLayers, paints[0:6]),
1438            "c": (ot.PaintFormat.PaintColrLayers, paints[2:8]),
1439        },
1440        version=1,
1441    )
1442
1443    assertIsColrV1(colr)
1444    assertNoV0Content(colr)
1445
1446    baseGlyphs = colr.table.BaseGlyphList.BaseGlyphPaintRecord
1447    # assert colr.table.BaseGlyphList.BaseGlyphCount == 2
1448
1449    assert _paint_names(colr.table.LayerList.Paint) == [
1450        "a",
1451        "b",
1452        "c",
1453        "d",
1454        "Layers[0:4]",
1455        "e",
1456        "f",
1457        "Layers[2:4]",
1458        "Layers[5:7]",
1459        "g",
1460        "h",
1461    ]
1462    assert _paint_names([b.Paint for b in baseGlyphs]) == [
1463        "Layers[0:4]",
1464        "Layers[4:7]",
1465        "Layers[7:11]",
1466    ]
1467    assert colr.table.LayerList.LayerCount == 11
1468
1469
1470def test_explicit_version_1():
1471    colr = builder.buildCOLR(
1472        {
1473            "a": (
1474                ot.PaintFormat.PaintColrLayers,
1475                [
1476                    (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0), "b"),
1477                    (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 1), "c"),
1478                ],
1479            )
1480        },
1481        version=1,
1482    )
1483    assert colr.version == 1
1484    assert not hasattr(colr, "ColorLayers")
1485    assert hasattr(colr, "table")
1486    assert isinstance(colr.table, ot.COLR)
1487    assert colr.table.VarStore is None
1488
1489
1490class BuildCOLRTest(object):
1491    def test_automatic_version_all_solid_color_glyphs(self):
1492        colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]})
1493        assert colr.version == 0
1494        assert hasattr(colr, "ColorLayers")
1495        assert colr.ColorLayers["a"][0].name == "b"
1496        assert colr.ColorLayers["a"][1].name == "c"
1497
1498    def test_automatic_version_no_solid_color_glyphs(self):
1499        colr = builder.buildCOLR(
1500            {
1501                "a": (
1502                    ot.PaintFormat.PaintColrLayers,
1503                    [
1504                        (
1505                            ot.PaintFormat.PaintGlyph,
1506                            {
1507                                "Format": int(ot.PaintFormat.PaintRadialGradient),
1508                                "ColorLine": {
1509                                    "ColorStop": [(0.0, 0), (1.0, 1)],
1510                                    "Extend": "repeat",
1511                                },
1512                                "x0": 1,
1513                                "y0": 0,
1514                                "x1": 10,
1515                                "y1": 0,
1516                                "r0": 4,
1517                                "r1": 2,
1518                            },
1519                            "b",
1520                        ),
1521                        (
1522                            ot.PaintFormat.PaintGlyph,
1523                            {
1524                                "Format": ot.PaintFormat.PaintSolid,
1525                                "PaletteIndex": 2,
1526                                "Alpha": 0.8,
1527                            },
1528                            "c",
1529                        ),
1530                    ],
1531                ),
1532                "d": (
1533                    ot.PaintFormat.PaintColrLayers,
1534                    [
1535                        {
1536                            "Format": ot.PaintFormat.PaintGlyph,
1537                            "Glyph": "e",
1538                            "Paint": {
1539                                "Format": ot.PaintFormat.PaintLinearGradient,
1540                                "ColorLine": {
1541                                    "ColorStop": [(0.0, 2), (1.0, 3)],
1542                                    "Extend": "reflect",
1543                                },
1544                                "x0": 1,
1545                                "y0": 2,
1546                                "x1": 3,
1547                                "y1": 4,
1548                                "x2": 2,
1549                                "y2": 2,
1550                            },
1551                        }
1552                    ],
1553                ),
1554            }
1555        )
1556        assertIsColrV1(colr)
1557        assert colr.table.BaseGlyphRecordCount == 0
1558        assert colr.table.BaseGlyphRecordArray is None
1559        assert colr.table.LayerRecordCount == 0
1560        assert colr.table.LayerRecordArray is None
1561
1562    def test_automatic_version_mixed_solid_and_gradient_glyphs(self):
1563        colr = builder.buildCOLR(
1564            {
1565                "a": [("b", 0), ("c", 1)],
1566                "d": (
1567                    ot.PaintFormat.PaintColrLayers,
1568                    [
1569                        (
1570                            ot.PaintFormat.PaintGlyph,
1571                            {
1572                                "Format": ot.PaintFormat.PaintLinearGradient,
1573                                "ColorLine": {"ColorStop": [(0.0, 2), (1.0, 3)]},
1574                                "x0": 1,
1575                                "y0": 2,
1576                                "x1": 3,
1577                                "y1": 4,
1578                                "x2": 2,
1579                                "y2": 2,
1580                            },
1581                            "e",
1582                        ),
1583                        (
1584                            ot.PaintFormat.PaintGlyph,
1585                            (ot.PaintFormat.PaintSolid, 2, 0.8),
1586                            "f",
1587                        ),
1588                    ],
1589                ),
1590            }
1591        )
1592        assertIsColrV1(colr)
1593        assert colr.table.VarStore is None
1594
1595        assert colr.table.BaseGlyphRecordCount == 1
1596        assert isinstance(colr.table.BaseGlyphRecordArray, ot.BaseGlyphRecordArray)
1597        assert colr.table.LayerRecordCount == 2
1598        assert isinstance(colr.table.LayerRecordArray, ot.LayerRecordArray)
1599
1600        assert isinstance(colr.table.BaseGlyphList, ot.BaseGlyphList)
1601        assert colr.table.BaseGlyphList.BaseGlyphCount == 1
1602        assert isinstance(
1603            colr.table.BaseGlyphList.BaseGlyphPaintRecord[0], ot.BaseGlyphPaintRecord
1604        )
1605        assert colr.table.BaseGlyphList.BaseGlyphPaintRecord[0].BaseGlyph == "d"
1606        assert isinstance(colr.table.LayerList, ot.LayerList)
1607        assert colr.table.LayerList.Paint[0].Glyph == "e"
1608
1609    def test_explicit_version_0(self):
1610        colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0)
1611        assert colr.version == 0
1612        assert hasattr(colr, "ColorLayers")
1613
1614    def test_explicit_version_1(self):
1615        colr = builder.buildCOLR(
1616            {
1617                "a": (
1618                    ot.PaintFormat.PaintColrLayers,
1619                    [
1620                        (
1621                            ot.PaintFormat.PaintGlyph,
1622                            (ot.PaintFormat.PaintSolid, 0),
1623                            "b",
1624                        ),
1625                        (
1626                            ot.PaintFormat.PaintGlyph,
1627                            (ot.PaintFormat.PaintSolid, 1),
1628                            "c",
1629                        ),
1630                    ],
1631                )
1632            },
1633            version=1,
1634        )
1635        assert colr.version == 1
1636        assert not hasattr(colr, "ColorLayers")
1637        assert hasattr(colr, "table")
1638        assert isinstance(colr.table, ot.COLR)
1639        assert colr.table.VarStore is None
1640
1641    def test_paint_one_colr_layers(self):
1642        # A set of one layers should flip to just that layer
1643        colr = builder.buildCOLR(
1644            {
1645                "a": (
1646                    ot.PaintFormat.PaintColrLayers,
1647                    [
1648                        (
1649                            ot.PaintFormat.PaintGlyph,
1650                            (ot.PaintFormat.PaintSolid, 0),
1651                            "b",
1652                        ),
1653                    ],
1654                )
1655            },
1656        )
1657
1658        assert colr.table.LayerList is None, "PaintColrLayers should be gone"
1659        assert colr.table.BaseGlyphList.BaseGlyphCount == 1
1660        paint = colr.table.BaseGlyphList.BaseGlyphPaintRecord[0].Paint
1661        assert paint.Format == ot.PaintFormat.PaintGlyph
1662        assert paint.Paint.Format == ot.PaintFormat.PaintSolid
1663
1664    def test_build_clip_list(self):
1665        colr = builder.buildCOLR(
1666            {
1667                "a": (
1668                    ot.PaintFormat.PaintGlyph,
1669                    (ot.PaintFormat.PaintSolid, 0),
1670                    "b",
1671                ),
1672                "c": (
1673                    ot.PaintFormat.PaintGlyph,
1674                    (ot.PaintFormat.PaintSolid, 1),
1675                    "d",
1676                ),
1677            },
1678            clipBoxes={
1679                "a": (0, 0, 1000, 1000, 0),  # optional 5th: varIndexBase
1680                "c": (-100.8, -200.4, 1100.1, 1200.5),  # floats get rounded
1681                "e": (0, 0, 10, 10),  # 'e' does _not_ get ignored despite being missing
1682            },
1683        )
1684
1685        assert colr.table.ClipList.Format == 1
1686        clipBoxes = colr.table.ClipList.clips
1687        assert [
1688            (baseGlyph, clipBox.as_tuple()) for baseGlyph, clipBox in clipBoxes.items()
1689        ] == [
1690            ("a", (0, 0, 1000, 1000, 0)),
1691            ("c", (-101, -201, 1101, 1201)),
1692            ("e", (0, 0, 10, 10)),
1693        ]
1694        assert clipBoxes["a"].Format == 2
1695        assert clipBoxes["c"].Format == 1
1696        assert clipBoxes["e"].Format == 1
1697
1698    def test_duplicate_base_glyphs(self):
1699        # If > 1 base glyphs refer to equivalent list of layers we expect them to share
1700        # the same PaintColrLayers.
1701        layers = {
1702            "Format": ot.PaintFormat.PaintColrLayers,
1703            "Layers": [
1704                (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0), "d"),
1705                (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 1), "e"),
1706            ],
1707        }
1708        # I copy the layers to ensure equality is by content, not by identity
1709        colr = builder.buildCOLR(
1710            {"a": layers, "b": deepcopy(layers), "c": deepcopy(layers)}
1711        ).table
1712
1713        baseGlyphs = colr.BaseGlyphList.BaseGlyphPaintRecord
1714        assert len(baseGlyphs) == 3
1715
1716        assert baseGlyphs[0].BaseGlyph == "a"
1717        assert baseGlyphs[1].BaseGlyph == "b"
1718        assert baseGlyphs[2].BaseGlyph == "c"
1719
1720        expected = {"Format": 1, "FirstLayerIndex": 0, "NumLayers": 2}
1721        assert baseGlyphs[0].Paint.__dict__ == expected
1722        assert baseGlyphs[1].Paint.__dict__ == expected
1723        assert baseGlyphs[2].Paint.__dict__ == expected
1724
1725
1726class TrickyRadialGradientTest:
1727    @staticmethod
1728    def circle_inside_circle(c0, r0, c1, r1, rounded=False):
1729        if rounded:
1730            return Circle(c0, r0).round().inside(Circle(c1, r1).round())
1731        else:
1732            return Circle(c0, r0).inside(Circle(c1, r1))
1733
1734    def round_start_circle(self, c0, r0, c1, r1, inside=True):
1735        assert self.circle_inside_circle(c0, r0, c1, r1) is inside
1736        assert self.circle_inside_circle(c0, r0, c1, r1, rounded=True) is not inside
1737        r = round_start_circle_stable_containment(c0, r0, c1, r1)
1738        assert (
1739            self.circle_inside_circle(r.centre, r.radius, c1, r1, rounded=True)
1740            is inside
1741        )
1742        return r.centre, r.radius
1743
1744    def test_noto_emoji_mosquito_u1f99f(self):
1745        # https://github.com/googlefonts/picosvg/issues/158
1746        c0 = (385.23508, 70.56727999999998)
1747        r0 = 0
1748        c1 = (642.99108, 104.70327999999995)
1749        r1 = 260.0072
1750        assert self.round_start_circle(c0, r0, c1, r1, inside=True) == ((386, 71), 0)
1751
1752    def test_noto_emoji_horns_sign_u1f918_1f3fc(self):
1753        # This radial gradient is taken from noto-emoji's 'SIGNS OF THE HORNS'
1754        # (1f918_1f3fc). We check that c0 is inside c1 both before and after rounding.
1755        c0 = (-437.6789059060543, -2116.9237094478003)
1756        r0 = 0.0
1757        c1 = (-488.7330118252256, -1876.5036857045086)
1758        r1 = 245.77147821915673
1759        assert self.circle_inside_circle(c0, r0, c1, r1)
1760        assert self.circle_inside_circle(c0, r0, c1, r1, rounded=True)
1761
1762    @pytest.mark.parametrize(
1763        "c0, r0, c1, r1, inside, expected",
1764        [
1765            # inside before round, outside after round
1766            ((1.4, 0), 0, (2.6, 0), 1.3, True, ((2, 0), 0)),
1767            ((1, 0), 0.6, (2.8, 0), 2.45, True, ((2, 0), 1)),
1768            ((6.49, 6.49), 0, (0.49, 0.49), 8.49, True, ((5, 5), 0)),
1769            # outside before round, inside after round
1770            ((0, 0), 0, (2, 0), 1.5, False, ((-1, 0), 0)),
1771            ((0, -0.5), 0, (0, -2.5), 1.5, False, ((0, 1), 0)),
1772            # the following ones require two nudges to round correctly
1773            ((0.5, 0), 0, (9.4, 0), 8.8, False, ((-1, 0), 0)),
1774            ((1.5, 1.5), 0, (0.49, 0.49), 1.49, True, ((0, 0), 0)),
1775            # limit case when circle almost exactly overlap
1776            ((0.5000001, 0), 0.5000001, (0.499999, 0), 0.4999999, True, ((0, 0), 0)),
1777            # concentrical circles, r0 > r1
1778            ((0, 0), 1.49, (0, 0), 1, False, ((0, 0), 2)),
1779        ],
1780    )
1781    def test_nudge_start_circle_position(self, c0, r0, c1, r1, inside, expected):
1782        assert self.round_start_circle(c0, r0, c1, r1, inside) == expected
1783