from fontTools.ttLib import newTable from fontTools.ttLib.tables import otTables as ot from fontTools.colorLib import builder from fontTools.colorLib.builder import LayerV1ListBuilder from fontTools.colorLib.errors import ColorLibError import pytest from typing import List def test_buildCOLR_v0(): color_layer_lists = { "a": [("a.color0", 0), ("a.color1", 1)], "b": [("b.color1", 1), ("b.color0", 0)], } colr = builder.buildCOLR(color_layer_lists) assert colr.tableTag == "COLR" assert colr.version == 0 assert colr.ColorLayers["a"][0].name == "a.color0" assert colr.ColorLayers["a"][0].colorID == 0 assert colr.ColorLayers["a"][1].name == "a.color1" assert colr.ColorLayers["a"][1].colorID == 1 assert colr.ColorLayers["b"][0].name == "b.color1" assert colr.ColorLayers["b"][0].colorID == 1 assert colr.ColorLayers["b"][1].name == "b.color0" assert colr.ColorLayers["b"][1].colorID == 0 def test_buildCOLR_v0_layer_as_list(): # when COLRv0 layers are encoded as plist in UFO lib, both python tuples and # lists are encoded as plist array elements; but the latter are always decoded # as python lists, thus after roundtripping a plist tuples become lists. # Before FontTools 4.17.0 we were treating tuples and lists as equivalent; # with 4.17.0, a paint of type list is used to identify a PaintColrLayers. # This broke backward compatibility as ufo2ft is simply passing through the # color layers as read from the UFO lib plist, and as such the latter use lists # instead of tuples for COLRv0 layers (layerGlyph, paletteIndex) combo. # We restore backward compat by accepting either tuples or lists (of length 2 # and only containing a str and an int) as individual top-level layers. # https://github.com/googlefonts/ufo2ft/issues/426 color_layer_lists = { "a": [["a.color0", 0], ["a.color1", 1]], "b": [["b.color1", 1], ["b.color0", 0]], } colr = builder.buildCOLR(color_layer_lists) assert colr.tableTag == "COLR" assert colr.version == 0 assert colr.ColorLayers["a"][0].name == "a.color0" assert colr.ColorLayers["a"][0].colorID == 0 assert colr.ColorLayers["a"][1].name == "a.color1" assert colr.ColorLayers["a"][1].colorID == 1 assert colr.ColorLayers["b"][0].name == "b.color1" assert colr.ColorLayers["b"][0].colorID == 1 assert colr.ColorLayers["b"][1].name == "b.color0" assert colr.ColorLayers["b"][1].colorID == 0 def test_buildCPAL_v0(): palettes = [ [(0.68, 0.20, 0.32, 1.0), (0.45, 0.68, 0.21, 1.0)], [(0.68, 0.20, 0.32, 0.6), (0.45, 0.68, 0.21, 0.6)], [(0.68, 0.20, 0.32, 0.3), (0.45, 0.68, 0.21, 0.3)], ] cpal = builder.buildCPAL(palettes) assert cpal.tableTag == "CPAL" assert cpal.version == 0 assert cpal.numPaletteEntries == 2 assert len(cpal.palettes) == 3 assert [tuple(c) for c in cpal.palettes[0]] == [ (82, 51, 173, 255), (54, 173, 115, 255), ] assert [tuple(c) for c in cpal.palettes[1]] == [ (82, 51, 173, 153), (54, 173, 115, 153), ] assert [tuple(c) for c in cpal.palettes[2]] == [ (82, 51, 173, 76), (54, 173, 115, 76), ] def test_buildCPAL_palettes_different_lengths(): with pytest.raises(ColorLibError, match="have different lengths"): builder.buildCPAL([[(1, 1, 1, 1)], [(0, 0, 0, 1), (0.5, 0.5, 0.5, 1)]]) def test_buildPaletteLabels(): name_table = newTable("name") name_table.names = [] name_ids = builder.buildPaletteLabels( [None, "hi", {"en": "hello", "de": "hallo"}], name_table ) assert name_ids == [0xFFFF, 256, 257] assert len(name_table.names) == 3 assert str(name_table.names[0]) == "hi" assert name_table.names[0].nameID == 256 assert str(name_table.names[1]) == "hallo" assert name_table.names[1].nameID == 257 assert str(name_table.names[2]) == "hello" assert name_table.names[2].nameID == 257 def test_build_CPAL_v1_types_no_labels(): palettes = [ [(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)], [(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)], [(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)], ] paletteTypes = [ builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND, builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND, builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND | builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND, ] cpal = builder.buildCPAL(palettes, paletteTypes=paletteTypes) assert cpal.tableTag == "CPAL" assert cpal.version == 1 assert cpal.numPaletteEntries == 2 assert len(cpal.palettes) == 3 assert cpal.paletteTypes == paletteTypes assert cpal.paletteLabels == [cpal.NO_NAME_ID] * len(palettes) assert cpal.paletteEntryLabels == [cpal.NO_NAME_ID] * cpal.numPaletteEntries def test_build_CPAL_v1_labels(): palettes = [ [(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)], [(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)], [(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)], ] paletteLabels = ["First", {"en": "Second", "it": "Seconda"}, None] paletteEntryLabels = ["Foo", "Bar"] with pytest.raises(TypeError, match="nameTable is required"): builder.buildCPAL(palettes, paletteLabels=paletteLabels) with pytest.raises(TypeError, match="nameTable is required"): builder.buildCPAL(palettes, paletteEntryLabels=paletteEntryLabels) name_table = newTable("name") name_table.names = [] cpal = builder.buildCPAL( palettes, paletteLabels=paletteLabels, paletteEntryLabels=paletteEntryLabels, nameTable=name_table, ) assert cpal.tableTag == "CPAL" assert cpal.version == 1 assert cpal.numPaletteEntries == 2 assert len(cpal.palettes) == 3 assert cpal.paletteTypes == [cpal.DEFAULT_PALETTE_TYPE] * len(palettes) assert cpal.paletteLabels == [256, 257, cpal.NO_NAME_ID] assert cpal.paletteEntryLabels == [258, 259] assert name_table.getDebugName(256) == "First" assert name_table.getDebugName(257) == "Second" assert name_table.getDebugName(258) == "Foo" assert name_table.getDebugName(259) == "Bar" def test_invalid_ColorPaletteType(): with pytest.raises(ValueError, match="not a valid ColorPaletteType"): builder.ColorPaletteType(-1) with pytest.raises(ValueError, match="not a valid ColorPaletteType"): builder.ColorPaletteType(4) with pytest.raises(ValueError, match="not a valid ColorPaletteType"): builder.ColorPaletteType("abc") def test_buildCPAL_v1_invalid_args_length(): with pytest.raises(ColorLibError, match="Expected 2 paletteTypes, got 1"): builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, 1, 1)]], paletteTypes=[1]) with pytest.raises(ColorLibError, match="Expected 2 paletteLabels, got 1"): builder.buildCPAL( [[(0, 0, 0, 0)], [(1, 1, 1, 1)]], paletteLabels=["foo"], nameTable=newTable("name"), ) with pytest.raises(ColorLibError, match="Expected 1 paletteEntryLabels, got 0"): cpal = builder.buildCPAL( [[(0, 0, 0, 0)], [(1, 1, 1, 1)]], paletteEntryLabels=[], nameTable=newTable("name"), ) def test_buildCPAL_invalid_color(): with pytest.raises( ColorLibError, match=r"In palette\[0\]\[1\]: expected \(R, G, B, A\) tuple, got \(1, 1, 1\)", ): builder.buildCPAL([[(1, 1, 1, 1), (1, 1, 1)]]) with pytest.raises( ColorLibError, match=( r"palette\[1\]\[0\] has invalid out-of-range " r"\[0..1\] color: \(1, 1, -1, 2\)" ), ): builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]]) def test_buildColorIndex(): c = builder.buildColorIndex(0) assert c.PaletteIndex == 0 assert c.Alpha.value == 1.0 assert c.Alpha.varIdx == 0 c = builder.buildColorIndex(1, alpha=0.5) assert c.PaletteIndex == 1 assert c.Alpha.value == 0.5 assert c.Alpha.varIdx == 0 c = builder.buildColorIndex(3, alpha=builder.VariableFloat(0.5, varIdx=2)) assert c.PaletteIndex == 3 assert c.Alpha.value == 0.5 assert c.Alpha.varIdx == 2 def test_buildPaintSolid(): p = LayerV1ListBuilder().buildPaintSolid(0) assert p.Format == ot.Paint.Format.PaintSolid assert p.Color.PaletteIndex == 0 assert p.Color.Alpha.value == 1.0 assert p.Color.Alpha.varIdx == 0 p = LayerV1ListBuilder().buildPaintSolid(1, alpha=0.5) assert p.Format == ot.Paint.Format.PaintSolid assert p.Color.PaletteIndex == 1 assert p.Color.Alpha.value == 0.5 assert p.Color.Alpha.varIdx == 0 p = LayerV1ListBuilder().buildPaintSolid( 3, alpha=builder.VariableFloat(0.5, varIdx=2) ) assert p.Format == ot.Paint.Format.PaintSolid assert p.Color.PaletteIndex == 3 assert p.Color.Alpha.value == 0.5 assert p.Color.Alpha.varIdx == 2 def test_buildColorStop(): s = builder.buildColorStop(0.1, 2) assert s.StopOffset == builder.VariableFloat(0.1) assert s.Color.PaletteIndex == 2 assert s.Color.Alpha == builder._DEFAULT_ALPHA s = builder.buildColorStop(offset=0.2, paletteIndex=3, alpha=0.4) assert s.StopOffset == builder.VariableFloat(0.2) assert s.Color == builder.buildColorIndex(3, alpha=0.4) s = builder.buildColorStop( offset=builder.VariableFloat(0.0, varIdx=1), paletteIndex=0, alpha=builder.VariableFloat(0.3, varIdx=2), ) assert s.StopOffset == builder.VariableFloat(0.0, varIdx=1) assert s.Color.PaletteIndex == 0 assert s.Color.Alpha == builder.VariableFloat(0.3, varIdx=2) def test_buildColorLine(): stops = [(0.0, 0), (0.5, 1), (1.0, 2)] cline = builder.buildColorLine(stops) assert cline.Extend == builder.ExtendMode.PAD assert cline.StopCount == 3 assert [ (cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop ] == stops cline = builder.buildColorLine(stops, extend="pad") assert cline.Extend == builder.ExtendMode.PAD cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REPEAT) assert cline.Extend == builder.ExtendMode.REPEAT cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REFLECT) assert cline.Extend == builder.ExtendMode.REFLECT cline = builder.buildColorLine([builder.buildColorStop(*s) for s in stops]) assert [ (cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop ] == stops stops = [ {"offset": (0.0, 1), "paletteIndex": 0, "alpha": (0.5, 2)}, {"offset": (1.0, 3), "paletteIndex": 1, "alpha": (0.3, 4)}, ] cline = builder.buildColorLine(stops) assert [ { "offset": cs.StopOffset, "paletteIndex": cs.Color.PaletteIndex, "alpha": cs.Color.Alpha, } for cs in cline.ColorStop ] == stops def test_buildAffine2x3(): matrix = builder.buildAffine2x3((1.5, 0, 0.5, 2.0, 1.0, -3.0)) assert matrix.xx == builder.VariableFloat(1.5) assert matrix.yx == builder.VariableFloat(0.0) assert matrix.xy == builder.VariableFloat(0.5) assert matrix.yy == builder.VariableFloat(2.0) assert matrix.dx == builder.VariableFloat(1.0) assert matrix.dy == builder.VariableFloat(-3.0) def test_buildPaintLinearGradient(): layerBuilder = LayerV1ListBuilder() color_stops = [ builder.buildColorStop(0.0, 0), builder.buildColorStop(0.5, 1), builder.buildColorStop(1.0, 2, alpha=0.8), ] color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT) p0 = (builder.VariableInt(100), builder.VariableInt(200)) p1 = (builder.VariableInt(150), builder.VariableInt(250)) gradient = layerBuilder.buildPaintLinearGradient(color_line, p0, p1) assert gradient.Format == 3 assert gradient.ColorLine == color_line assert (gradient.x0, gradient.y0) == p0 assert (gradient.x1, gradient.y1) == p1 assert (gradient.x2, gradient.y2) == p1 gradient = layerBuilder.buildPaintLinearGradient({"stops": color_stops}, p0, p1) assert gradient.ColorLine.Extend == builder.ExtendMode.PAD assert gradient.ColorLine.ColorStop == color_stops gradient = layerBuilder.buildPaintLinearGradient(color_line, p0, p1, p2=(150, 230)) assert (gradient.x2.value, gradient.y2.value) == (150, 230) assert (gradient.x2, gradient.y2) != (gradient.x1, gradient.y1) def test_buildPaintRadialGradient(): layerBuilder = LayerV1ListBuilder() color_stops = [ builder.buildColorStop(0.0, 0), builder.buildColorStop(0.5, 1), builder.buildColorStop(1.0, 2, alpha=0.8), ] color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT) c0 = (builder.VariableInt(100), builder.VariableInt(200)) c1 = (builder.VariableInt(150), builder.VariableInt(250)) r0 = builder.VariableInt(10) r1 = builder.VariableInt(5) gradient = layerBuilder.buildPaintRadialGradient(color_line, c0, c1, r0, r1) assert gradient.Format == ot.Paint.Format.PaintRadialGradient assert gradient.ColorLine == color_line assert (gradient.x0, gradient.y0) == c0 assert (gradient.x1, gradient.y1) == c1 assert gradient.r0 == r0 assert gradient.r1 == r1 gradient = layerBuilder.buildPaintRadialGradient( {"stops": color_stops}, c0, c1, r0, r1 ) assert gradient.ColorLine.Extend == builder.ExtendMode.PAD assert gradient.ColorLine.ColorStop == color_stops def test_buildPaintGlyph_Solid(): layerBuilder = LayerV1ListBuilder() layer = layerBuilder.buildPaintGlyph("a", 2) assert layer.Glyph == "a" assert layer.Paint.Format == ot.Paint.Format.PaintSolid assert layer.Paint.Color.PaletteIndex == 2 layer = layerBuilder.buildPaintGlyph("a", layerBuilder.buildPaintSolid(3, 0.9)) assert layer.Paint.Format == ot.Paint.Format.PaintSolid assert layer.Paint.Color.PaletteIndex == 3 assert layer.Paint.Color.Alpha.value == 0.9 def test_buildPaintGlyph_LinearGradient(): layerBuilder = LayerV1ListBuilder() layer = layerBuilder.buildPaintGlyph( "a", layerBuilder.buildPaintLinearGradient( {"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250) ), ) assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0 assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 3 assert layer.Paint.ColorLine.ColorStop[1].StopOffset.value == 1.0 assert layer.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 4 assert layer.Paint.x0.value == 100 assert layer.Paint.y0.value == 200 assert layer.Paint.x1.value == 150 assert layer.Paint.y1.value == 250 def test_buildPaintGlyph_RadialGradient(): layerBuilder = LayerV1ListBuilder() layer = layerBuilder.buildPaintGlyph( "a", layerBuilder.buildPaintRadialGradient( { "stops": [ (0.0, 5), {"offset": 0.5, "paletteIndex": 6, "alpha": 0.8}, (1.0, 7), ] }, (50, 50), (75, 75), 30, 10, ), ) assert layer.Paint.Format == ot.Paint.Format.PaintRadialGradient assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0 assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 5 assert layer.Paint.ColorLine.ColorStop[1].StopOffset.value == 0.5 assert layer.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 6 assert layer.Paint.ColorLine.ColorStop[1].Color.Alpha.value == 0.8 assert layer.Paint.ColorLine.ColorStop[2].StopOffset.value == 1.0 assert layer.Paint.ColorLine.ColorStop[2].Color.PaletteIndex == 7 assert layer.Paint.x0.value == 50 assert layer.Paint.y0.value == 50 assert layer.Paint.r0.value == 30 assert layer.Paint.x1.value == 75 assert layer.Paint.y1.value == 75 assert layer.Paint.r1.value == 10 def test_buildPaintGlyph_Dict_Solid(): layerBuilder = LayerV1ListBuilder() layer = layerBuilder.buildPaintGlyph("a", {"format": 2, "paletteIndex": 0}) assert layer.Glyph == "a" assert layer.Paint.Format == ot.Paint.Format.PaintSolid assert layer.Paint.Color.PaletteIndex == 0 def test_buildPaintGlyph_Dict_LinearGradient(): layerBuilder = LayerV1ListBuilder() layer = layerBuilder.buildPaintGlyph( "a", { "format": 3, "colorLine": {"stops": [(0.0, 0), (1.0, 1)]}, "p0": (0, 0), "p1": (10, 10), }, ) assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0 def test_buildPaintGlyph_Dict_RadialGradient(): layerBuilder = LayerV1ListBuilder() layer = layerBuilder.buildPaintGlyph( "a", { "format": 4, "colorLine": {"stops": [(0.0, 0), (1.0, 1)]}, "c0": (0, 0), "c1": (10, 10), "r0": 4, "r1": 0, }, ) assert layer.Paint.Format == ot.Paint.Format.PaintRadialGradient assert layer.Paint.r0.value == 4 def test_buildPaintColrGlyph(): paint = LayerV1ListBuilder().buildPaintColrGlyph("a") assert paint.Format == ot.Paint.Format.PaintColrGlyph assert paint.Glyph == "a" def test_buildPaintTransform(): layerBuilder = LayerV1ListBuilder() paint = layerBuilder.buildPaintTransform( transform=builder.buildAffine2x3((1, 2, 3, 4, 5, 6)), paint=layerBuilder.buildPaintGlyph( glyph="a", paint=layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0), ), ) assert paint.Format == ot.Paint.Format.PaintTransform assert paint.Paint.Format == ot.Paint.Format.PaintGlyph assert paint.Paint.Paint.Format == ot.Paint.Format.PaintSolid assert paint.Transform.xx.value == 1.0 assert paint.Transform.yx.value == 2.0 assert paint.Transform.xy.value == 3.0 assert paint.Transform.yy.value == 4.0 assert paint.Transform.dx.value == 5.0 assert paint.Transform.dy.value == 6.0 paint = layerBuilder.buildPaintTransform( (1, 0, 0, 0.3333, 10, 10), { "format": 4, "colorLine": {"stops": [(0.0, 0), (1.0, 1)]}, "c0": (100, 100), "c1": (100, 100), "r0": 0, "r1": 50, }, ) assert paint.Format == ot.Paint.Format.PaintTransform assert paint.Transform.xx.value == 1.0 assert paint.Transform.yx.value == 0.0 assert paint.Transform.xy.value == 0.0 assert paint.Transform.yy.value == 0.3333 assert paint.Transform.dx.value == 10 assert paint.Transform.dy.value == 10 assert paint.Paint.Format == ot.Paint.Format.PaintRadialGradient def test_buildPaintComposite(): layerBuilder = LayerV1ListBuilder() composite = layerBuilder.buildPaintComposite( mode=ot.CompositeMode.SRC_OVER, source={ "format": 8, "mode": "src_over", "source": {"format": 5, "glyph": "c", "paint": 2}, "backdrop": {"format": 5, "glyph": "b", "paint": 1}, }, backdrop=layerBuilder.buildPaintGlyph( "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0) ), ) assert composite.Format == ot.Paint.Format.PaintComposite assert composite.SourcePaint.Format == ot.Paint.Format.PaintComposite assert composite.SourcePaint.SourcePaint.Format == ot.Paint.Format.PaintGlyph assert composite.SourcePaint.SourcePaint.Glyph == "c" assert composite.SourcePaint.SourcePaint.Paint.Format == ot.Paint.Format.PaintSolid assert composite.SourcePaint.SourcePaint.Paint.Color.PaletteIndex == 2 assert composite.SourcePaint.CompositeMode == ot.CompositeMode.SRC_OVER assert composite.SourcePaint.BackdropPaint.Format == ot.Paint.Format.PaintGlyph assert composite.SourcePaint.BackdropPaint.Glyph == "b" assert ( composite.SourcePaint.BackdropPaint.Paint.Format == ot.Paint.Format.PaintSolid ) assert composite.SourcePaint.BackdropPaint.Paint.Color.PaletteIndex == 1 assert composite.CompositeMode == ot.CompositeMode.SRC_OVER assert composite.BackdropPaint.Format == ot.Paint.Format.PaintGlyph assert composite.BackdropPaint.Glyph == "a" assert composite.BackdropPaint.Paint.Format == ot.Paint.Format.PaintSolid assert composite.BackdropPaint.Paint.Color.PaletteIndex == 0 def test_buildColrV1(): colorGlyphs = { "a": [("b", 0), ("c", 1)], "d": [ ("e", {"format": 2, "paletteIndex": 2, "alpha": 0.8}), ( "f", { "format": 4, "colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "reflect"}, "c0": (0, 0), "c1": (0, 0), "r0": 10, "r1": 0, }, ), ], "g": [("h", 5)], } glyphMap = { ".notdef": 0, "a": 4, "b": 3, "c": 2, "d": 1, "e": 5, "f": 6, "g": 7, "h": 8, } # TODO(anthrotype) should we split into two tests? - seems two distinct validations layers, baseGlyphs = builder.buildColrV1(colorGlyphs, glyphMap) assert baseGlyphs.BaseGlyphCount == len(colorGlyphs) assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "d" assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "a" assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g" layers, baseGlyphs = builder.buildColrV1(colorGlyphs) assert baseGlyphs.BaseGlyphCount == len(colorGlyphs) assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a" assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "d" assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g" def test_split_color_glyphs_by_version(): layerBuilder = LayerV1ListBuilder() colorGlyphs = { "a": [ ("b", 0), ("c", 1), ("d", 2), ("e", 3), ] } colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs) assert colorGlyphsV0 == {"a": [("b", 0), ("c", 1), ("d", 2), ("e", 3)]} assert not colorGlyphsV1 colorGlyphs = { "a": [("b", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=0.0))] } colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs) assert not colorGlyphsV0 assert colorGlyphsV1 == colorGlyphs colorGlyphs = { "a": [("b", 0)], "c": [ ("d", 1), ( "e", { "format": 3, "colorLine": {"stops": [(0.0, 2), (1.0, 3)]}, "p0": (0, 0), "p1": (10, 10), }, ), ], } colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs) assert colorGlyphsV0 == {"a": [("b", 0)]} assert "a" not in colorGlyphsV1 assert "c" in colorGlyphsV1 assert len(colorGlyphsV1["c"]) == 2 def assertIsColrV1(colr): assert colr.version == 1 assert not hasattr(colr, "ColorLayers") assert hasattr(colr, "table") assert isinstance(colr.table, ot.COLR) def assertNoV0Content(colr): assert colr.table.BaseGlyphRecordCount == 0 assert colr.table.BaseGlyphRecordArray is None assert colr.table.LayerRecordCount == 0 assert colr.table.LayerRecordArray is None def test_build_layerv1list_empty(): # Nobody uses PaintColrLayers (format 8), no layerlist colr = builder.buildCOLR( { "a": { "format": 5, # PaintGlyph "paint": {"format": 2, "paletteIndex": 2, "alpha": 0.8}, "glyph": "b", }, # A list of 1 shouldn't become a PaintColrLayers "b": [ { "format": 5, # PaintGlyph "paint": { "format": 3, "colorLine": { "stops": [(0.0, 2), (1.0, 3)], "extend": "reflect", }, "p0": (1, 2), "p1": (3, 4), "p2": (2, 2), }, "glyph": "bb", } ], } ) assertIsColrV1(colr) assertNoV0Content(colr) # 2 v1 glyphs, none in LayerV1List assert colr.table.BaseGlyphV1List.BaseGlyphCount == 2 assert len(colr.table.BaseGlyphV1List.BaseGlyphV1Record) == 2 assert colr.table.LayerV1List.LayerCount == 0 assert len(colr.table.LayerV1List.Paint) == 0 def _paint_names(paints) -> List[str]: # prints a predictable string from a paint list to enable # semi-readable assertions on a LayerV1List order. result = [] for paint in paints: if paint.Format == int(ot.Paint.Format.PaintGlyph): result.append(paint.Glyph) elif paint.Format == int(ot.Paint.Format.PaintColrLayers): result.append( f"Layers[{paint.FirstLayerIndex}:{paint.FirstLayerIndex+paint.NumLayers}]" ) return result def test_build_layerv1list_simple(): # Two colr glyphs, each with two layers the first of which is common # All layers use the same solid paint solid_paint = {"format": 2, "paletteIndex": 2, "alpha": 0.8} backdrop = { "format": 5, # PaintGlyph "paint": solid_paint, "glyph": "back", } a_foreground = { "format": 5, # PaintGlyph "paint": solid_paint, "glyph": "a_fore", } b_foreground = { "format": 5, # PaintGlyph "paint": solid_paint, "glyph": "b_fore", } # list => PaintColrLayers, which means contents should be in LayerV1List colr = builder.buildCOLR( { "a": [ backdrop, a_foreground, ], "b": [ backdrop, b_foreground, ], } ) assertIsColrV1(colr) assertNoV0Content(colr) # 2 v1 glyphs, 4 paints in LayerV1List # A single shared backdrop isn't worth accessing by slice assert colr.table.BaseGlyphV1List.BaseGlyphCount == 2 assert len(colr.table.BaseGlyphV1List.BaseGlyphV1Record) == 2 assert colr.table.LayerV1List.LayerCount == 4 assert _paint_names(colr.table.LayerV1List.Paint) == [ "back", "a_fore", "back", "b_fore", ] def test_build_layerv1list_with_sharing(): # Three colr glyphs, each with two layers in common solid_paint = {"format": 2, "paletteIndex": 2, "alpha": 0.8} backdrop = [ { "format": 5, # PaintGlyph "paint": solid_paint, "glyph": "back1", }, { "format": 5, # PaintGlyph "paint": solid_paint, "glyph": "back2", }, ] a_foreground = { "format": 5, # PaintGlyph "paint": solid_paint, "glyph": "a_fore", } b_background = { "format": 5, # PaintGlyph "paint": solid_paint, "glyph": "b_back", } b_foreground = { "format": 5, # PaintGlyph "paint": solid_paint, "glyph": "b_fore", } c_background = { "format": 5, # PaintGlyph "paint": solid_paint, "glyph": "c_back", } # list => PaintColrLayers, which means contents should be in LayerV1List colr = builder.buildCOLR( { "a": backdrop + [a_foreground], "b": [b_background] + backdrop + [b_foreground], "c": [c_background] + backdrop, } ) assertIsColrV1(colr) assertNoV0Content(colr) # 2 v1 glyphs, 4 paints in LayerV1List # A single shared backdrop isn't worth accessing by slice baseGlyphs = colr.table.BaseGlyphV1List.BaseGlyphV1Record assert colr.table.BaseGlyphV1List.BaseGlyphCount == 3 assert len(baseGlyphs) == 3 assert _paint_names([b.Paint for b in baseGlyphs]) == [ "Layers[0:3]", "Layers[3:6]", "Layers[6:8]", ] assert _paint_names(colr.table.LayerV1List.Paint) == [ "back1", "back2", "a_fore", "b_back", "Layers[0:2]", "b_fore", "c_back", "Layers[0:2]", ] assert colr.table.LayerV1List.LayerCount == 8 def test_build_layerv1list_with_overlaps(): paints = [ { "format": 5, # PaintGlyph "paint": {"format": 2, "paletteIndex": 2, "alpha": 0.8}, "glyph": c, } for c in "abcdefghi" ] # list => PaintColrLayers, which means contents should be in LayerV1List colr = builder.buildCOLR( { "a": paints[0:4], "b": paints[0:6], "c": paints[2:8], } ) assertIsColrV1(colr) assertNoV0Content(colr) baseGlyphs = colr.table.BaseGlyphV1List.BaseGlyphV1Record # assert colr.table.BaseGlyphV1List.BaseGlyphCount == 2 assert _paint_names(colr.table.LayerV1List.Paint) == [ "a", "b", "c", "d", "Layers[0:4]", "e", "f", "Layers[2:4]", "Layers[5:7]", "g", "h", ] assert _paint_names([b.Paint for b in baseGlyphs]) == [ "Layers[0:4]", "Layers[4:7]", "Layers[7:11]", ] assert colr.table.LayerV1List.LayerCount == 11 class BuildCOLRTest(object): def test_automatic_version_all_solid_color_glyphs(self): colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}) assert colr.version == 0 assert hasattr(colr, "ColorLayers") assert colr.ColorLayers["a"][0].name == "b" assert colr.ColorLayers["a"][1].name == "c" def test_automatic_version_no_solid_color_glyphs(self): colr = builder.buildCOLR( { "a": [ ( "b", { "format": 4, "colorLine": { "stops": [(0.0, 0), (1.0, 1)], "extend": "repeat", }, "c0": (1, 0), "c1": (10, 0), "r0": 4, "r1": 2, }, ), ("c", {"format": 2, "paletteIndex": 2, "alpha": 0.8}), ], "d": [ ( "e", { "format": 3, "colorLine": { "stops": [(0.0, 2), (1.0, 3)], "extend": "reflect", }, "p0": (1, 2), "p1": (3, 4), "p2": (2, 2), }, ), ], } ) assertIsColrV1(colr) assert colr.table.BaseGlyphRecordCount == 0 assert colr.table.BaseGlyphRecordArray is None assert colr.table.LayerRecordCount == 0 assert colr.table.LayerRecordArray is None def test_automatic_version_mixed_solid_and_gradient_glyphs(self): colr = builder.buildCOLR( { "a": [("b", 0), ("c", 1)], "d": [ ( "e", { "format": 3, "colorLine": {"stops": [(0.0, 2), (1.0, 3)]}, "p0": (1, 2), "p1": (3, 4), "p2": (2, 2), }, ), ("f", {"format": 2, "paletteIndex": 2, "alpha": 0.8}), ], } ) assertIsColrV1(colr) assert colr.table.VarStore is None assert colr.table.BaseGlyphRecordCount == 1 assert isinstance(colr.table.BaseGlyphRecordArray, ot.BaseGlyphRecordArray) assert colr.table.LayerRecordCount == 2 assert isinstance(colr.table.LayerRecordArray, ot.LayerRecordArray) assert isinstance(colr.table.BaseGlyphV1List, ot.BaseGlyphV1List) assert colr.table.BaseGlyphV1List.BaseGlyphCount == 1 assert isinstance( colr.table.BaseGlyphV1List.BaseGlyphV1Record[0], ot.BaseGlyphV1Record ) assert colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].BaseGlyph == "d" assert isinstance(colr.table.LayerV1List, ot.LayerV1List) assert colr.table.LayerV1List.Paint[0].Glyph == "e" def test_explicit_version_0(self): colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0) assert colr.version == 0 assert hasattr(colr, "ColorLayers") def test_explicit_version_1(self): colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=1) assert colr.version == 1 assert not hasattr(colr, "ColorLayers") assert hasattr(colr, "table") assert isinstance(colr.table, ot.COLR) assert colr.table.VarStore is None