1223 lines
40 KiB
Python
1223 lines
40 KiB
Python
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
|