| from fontTools.ttLib import newTable |
| from fontTools.ttLib.tables import otTables as ot |
| from fontTools.colorLib import builder |
| from fontTools.colorLib.geometry import round_start_circle_stable_containment, Circle |
| from fontTools.colorLib.builder import LayerV1ListBuilder, _build_n_ary_tree |
| 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": 11, |
| "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_buildPaintTranslate(): |
| layerBuilder = LayerV1ListBuilder() |
| paint = layerBuilder.buildPaintTranslate( |
| paint=layerBuilder.buildPaintGlyph( |
| "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0) |
| ), |
| dx=123, |
| dy=-345, |
| ) |
| |
| assert paint.Format == ot.Paint.Format.PaintTranslate |
| assert paint.Paint.Format == ot.Paint.Format.PaintGlyph |
| assert paint.dx.value == 123 |
| assert paint.dy.value == -345 |
| |
| |
| def test_buildPaintRotate(): |
| layerBuilder = LayerV1ListBuilder() |
| paint = layerBuilder.buildPaintRotate( |
| paint=layerBuilder.buildPaintGlyph( |
| "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0) |
| ), |
| angle=15, |
| centerX=127, |
| centerY=129, |
| ) |
| |
| assert paint.Format == ot.Paint.Format.PaintRotate |
| assert paint.Paint.Format == ot.Paint.Format.PaintGlyph |
| assert paint.angle.value == 15 |
| assert paint.centerX.value == 127 |
| assert paint.centerY.value == 129 |
| |
| |
| def test_buildPaintSkew(): |
| layerBuilder = LayerV1ListBuilder() |
| paint = layerBuilder.buildPaintSkew( |
| paint=layerBuilder.buildPaintGlyph( |
| "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0) |
| ), |
| xSkewAngle=15, |
| ySkewAngle=42, |
| centerX=127, |
| centerY=129, |
| ) |
| |
| assert paint.Format == ot.Paint.Format.PaintSkew |
| assert paint.Paint.Format == ot.Paint.Format.PaintGlyph |
| assert paint.xSkewAngle.value == 15 |
| assert paint.ySkewAngle.value == 42 |
| assert paint.centerX.value == 127 |
| assert paint.centerY.value == 129 |
| |
| |
| 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_buildColrV1_more_than_255_paints(): |
| num_paints = 364 |
| colorGlyphs = { |
| "a": [ |
| { |
| "format": 5, # PaintGlyph |
| "paint": 0, |
| "glyph": name, |
| } |
| for name in (f"glyph{i}" for i in range(num_paints)) |
| ], |
| } |
| layers, baseGlyphs = builder.buildColrV1(colorGlyphs) |
| paints = layers.Paint |
| |
| assert len(paints) == num_paints + 1 |
| |
| assert all(paints[i].Format == ot.Paint.Format.PaintGlyph for i in range(255)) |
| |
| assert paints[255].Format == ot.Paint.Format.PaintColrLayers |
| assert paints[255].FirstLayerIndex == 0 |
| assert paints[255].NumLayers == 255 |
| |
| assert all( |
| paints[i].Format == ot.Paint.Format.PaintGlyph |
| for i in range(256, num_paints + 1) |
| ) |
| |
| assert baseGlyphs.BaseGlyphCount == len(colorGlyphs) |
| assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a" |
| assert ( |
| baseGlyphs.BaseGlyphV1Record[0].Paint.Format == ot.Paint.Format.PaintColrLayers |
| ) |
| assert baseGlyphs.BaseGlyphV1Record[0].Paint.FirstLayerIndex == 255 |
| assert baseGlyphs.BaseGlyphV1Record[0].Paint.NumLayers == num_paints + 1 - 255 |
| |
| |
| 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 |
| |
| |
| class TrickyRadialGradientTest: |
| @staticmethod |
| def circle_inside_circle(c0, r0, c1, r1, rounded=False): |
| if rounded: |
| return Circle(c0, r0).round().inside(Circle(c1, r1).round()) |
| else: |
| return Circle(c0, r0).inside(Circle(c1, r1)) |
| |
| def round_start_circle(self, c0, r0, c1, r1, inside=True): |
| assert self.circle_inside_circle(c0, r0, c1, r1) is inside |
| assert self.circle_inside_circle(c0, r0, c1, r1, rounded=True) is not inside |
| r = round_start_circle_stable_containment(c0, r0, c1, r1) |
| assert ( |
| self.circle_inside_circle(r.centre, r.radius, c1, r1, rounded=True) |
| is inside |
| ) |
| return r.centre, r.radius |
| |
| def test_noto_emoji_mosquito_u1f99f(self): |
| # https://github.com/googlefonts/picosvg/issues/158 |
| c0 = (385.23508, 70.56727999999998) |
| r0 = 0 |
| c1 = (642.99108, 104.70327999999995) |
| r1 = 260.0072 |
| assert self.round_start_circle(c0, r0, c1, r1, inside=True) == ((386, 71), 0) |
| |
| @pytest.mark.parametrize( |
| "c0, r0, c1, r1, inside, expected", |
| [ |
| # inside before round, outside after round |
| ((1.4, 0), 0, (2.6, 0), 1.3, True, ((2, 0), 0)), |
| ((1, 0), 0.6, (2.8, 0), 2.45, True, ((2, 0), 1)), |
| ((6.49, 6.49), 0, (0.49, 0.49), 8.49, True, ((5, 5), 0)), |
| # outside before round, inside after round |
| ((0, 0), 0, (2, 0), 1.5, False, ((-1, 0), 0)), |
| ((0, -0.5), 0, (0, -2.5), 1.5, False, ((0, 1), 0)), |
| # the following ones require two nudges to round correctly |
| ((0.5, 0), 0, (9.4, 0), 8.8, False, ((-1, 0), 0)), |
| ((1.5, 1.5), 0, (0.49, 0.49), 1.49, True, ((0, 0), 0)), |
| # limit case when circle almost exactly overlap |
| ((0.5000001, 0), 0.5000001, (0.499999, 0), 0.4999999, True, ((0, 0), 0)), |
| # concentrical circles, r0 > r1 |
| ((0, 0), 1.49, (0, 0), 1, False, ((0, 0), 2)), |
| ], |
| ) |
| def test_nudge_start_circle_position(self, c0, r0, c1, r1, inside, expected): |
| assert self.round_start_circle(c0, r0, c1, r1, inside) == expected |
| |
| |
| @pytest.mark.parametrize( |
| "lst, n, expected", |
| [ |
| ([0], 2, [0]), |
| ([0, 1], 2, [0, 1]), |
| ([0, 1, 2], 2, [[0, 1], 2]), |
| ([0, 1, 2], 3, [0, 1, 2]), |
| ([0, 1, 2, 3], 2, [[0, 1], [2, 3]]), |
| ([0, 1, 2, 3], 3, [[0, 1, 2], 3]), |
| ([0, 1, 2, 3, 4], 3, [[0, 1, 2], 3, 4]), |
| ([0, 1, 2, 3, 4, 5], 3, [[0, 1, 2], [3, 4, 5]]), |
| (list(range(7)), 3, [[0, 1, 2], [3, 4, 5], 6]), |
| (list(range(8)), 3, [[0, 1, 2], [3, 4, 5], [6, 7]]), |
| (list(range(9)), 3, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]), |
| (list(range(10)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9]), |
| (list(range(11)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9, 10]), |
| (list(range(12)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11]]), |
| (list(range(13)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], 12]), |
| ( |
| list(range(14)), |
| 3, |
| [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], 12, 13]], |
| ), |
| ( |
| list(range(15)), |
| 3, |
| [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], [12, 13, 14]], |
| ), |
| ( |
| list(range(16)), |
| 3, |
| [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], [12, 13, 14], 15]], |
| ), |
| ( |
| list(range(23)), |
| 3, |
| [ |
| [[0, 1, 2], [3, 4, 5], [6, 7, 8]], |
| [[9, 10, 11], [12, 13, 14], [15, 16, 17]], |
| [[18, 19, 20], 21, 22], |
| ], |
| ), |
| ( |
| list(range(27)), |
| 3, |
| [ |
| [[0, 1, 2], [3, 4, 5], [6, 7, 8]], |
| [[9, 10, 11], [12, 13, 14], [15, 16, 17]], |
| [[18, 19, 20], [21, 22, 23], [24, 25, 26]], |
| ], |
| ), |
| ( |
| list(range(28)), |
| 3, |
| [ |
| [ |
| [[0, 1, 2], [3, 4, 5], [6, 7, 8]], |
| [[9, 10, 11], [12, 13, 14], [15, 16, 17]], |
| [[18, 19, 20], [21, 22, 23], [24, 25, 26]], |
| ], |
| 27, |
| ], |
| ), |
| (list(range(257)), 256, [list(range(256)), 256]), |
| (list(range(258)), 256, [list(range(256)), 256, 257]), |
| (list(range(512)), 256, [list(range(256)), list(range(256, 512))]), |
| (list(range(512 + 1)), 256, [list(range(256)), list(range(256, 512)), 512]), |
| ( |
| list(range(256 ** 2)), |
| 256, |
| [list(range(k * 256, k * 256 + 256)) for k in range(256)], |
| ), |
| ], |
| ) |
| def test_build_n_ary_tree(lst, n, expected): |
| assert _build_n_ary_tree(lst, n) == expected |