Start hooking up revised PaintColrLayers

This commit is contained in:
rsheeter 2020-11-03 23:33:01 -08:00
parent 581416d77c
commit f531038bf9
4 changed files with 343 additions and 180 deletions

View File

@ -9,6 +9,7 @@ from functools import partial
from typing import (
Any,
Dict,
Generator,
Iterable,
List,
Mapping,
@ -23,6 +24,7 @@ from fontTools.misc.fixedTools import fixedToFloat
from fontTools.ttLib.tables import C_O_L_R_
from fontTools.ttLib.tables import C_P_A_L_
from fontTools.ttLib.tables import _n_a_m_e
from fontTools.ttLib.tables.otBase import BaseTable
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otTables import (
ExtendMode,
@ -111,8 +113,8 @@ def buildCOLR(
Args:
colorGlyphs: map of base glyph name to, either list of (layer glyph name,
color palette index) tuples for COLRv0; or list of Paints (dicts)
for COLRv1.
color palette index) tuples for COLRv0; or a single Paint (dict) or
list of Paint for COLRv1.
version: the version of COLR table. If None, the version is determined
by the presence of COLRv1 paints or variation data (varStore), which
require version 1; otherwise, if all base glyphs use only simple color
@ -148,7 +150,8 @@ def buildCOLR(
colr.BaseGlyphRecordArray = colr.LayerRecordArray = None
if colorGlyphsV1:
colr.BaseGlyphV1List = buildBaseGlyphV1List(colorGlyphsV1, glyphMap)
colr.LayerV1List, colr.BaseGlyphV1List = buildColrV1(colorGlyphsV1, glyphMap)
if version is None:
version = 1 if (varStore or colorGlyphsV1) else 0
@ -430,6 +433,111 @@ def _to_color_line(obj):
raise TypeError(obj)
_PAINT_BUILDERS = {
1: lambda _, kwargs: buildPaintSolid(**kwargs),
2: lambda _, kwargs: buildPaintLinearGradient(**kwargs),
3: lambda _, kwargs: buildPaintRadialGradient(**kwargs),
4: lambda builder, kwargs: buildPaintGlyph(builder, **kwargs),
5: lambda _, kwargs: buildPaintColrGlyph(**kwargs),
6: lambda builder, kwargs: buildPaintTransform(builder, **kwargs),
7: lambda builder, kwargs: buildPaintComposite(builder, **kwargs),
}
def _as_tuple(obj) -> Tuple[Any, ...]:
# start simple, who even cares about cyclic graphs or interesting field types
def _tuple_safe(value):
if isinstance(value, enum.Enum):
return value
elif hasattr(value, "__dict__"):
return tuple((k, _tuple_safe(v)) for k, v in value.__dict__.items())
elif isinstance(value, collections.abc.MutableSequence):
return tuple(_tuple_safe(e) for e in value)
return value
return tuple(_tuple_safe(obj))
def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
# TODO feels like something itertools might have already
for lbound in range(num_layers):
# TODO may want a max length to limit scope of search
# Reuse of very large #s of layers is relatively unlikely
# +2: we want sequences of at least 2
# otData handles single-record duplication
for ubound in range(lbound + 2, num_layers + 1):
yield (lbound, ubound)
class LayerCollector:
Slices: List[ot.Paint]
Layers: List[ot.Paint]
ReusePool: Mapping[Tuple[Any, ...], int]
def __init__(self):
self.Slices = []
self.Layers = []
self.ReusePool = {}
def buildColrLayers(self, paints: List[_PaintInput]) -> ot.Paint:
paint = ot.Paint()
paint.Format = int(ot.Paint.Format.PaintColrLayers)
self.Slices.append(paint)
paints = [self.build(p) for p in paints]
# Look for reuse, with preference to longer sequences
found_reuse = True
while found_reuse:
found_reuse = False
ranges = sorted(_reuse_ranges(len(paints)),
key=lambda t: (t[1] - t[0], t[1], t[0]),
reverse=True)
for lbound, ubound in ranges:
reuse_lbound = self.ReusePool.get(_as_tuple(paints[lbound:ubound]), -1)
if reuse_lbound == -1:
continue
found_reuse = True
new_slice = ot.Paint()
new_slice.Format = int(ot.Paint.Format.PaintColrLayers)
new_slice.NumLayers = ubound - lbound
new_slice.FirstLayerIndex = reuse_lbound
paints = paints[:lbound] + [new_slice] + paints[ubound:]
paint.NumLayers = len(paints)
paint.FirstLayerIndex = len(self.Layers)
self.Layers.extend(paints)
# Register our parts for reuse
for lbound, ubound in _reuse_ranges(len(paints)):
self.ReusePool[_as_tuple(paints[lbound:ubound])] = lbound + paint.FirstLayerIndex
return paint
def build(self, paint: _PaintInput) -> ot.Paint:
if isinstance(paint, ot.Paint):
return paint
elif isinstance(paint, int):
paletteIndex = paint
return buildPaintSolid(paletteIndex)
elif isinstance(paint, tuple):
layerGlyph, paint = paint
return buildPaintGlyph(self, layerGlyph, paint)
elif isinstance(paint, list):
# implicit PaintColrLayers
return self.buildColrLayers(paint)
elif isinstance(paint, collections.abc.Mapping):
kwargs = dict(paint)
fmt = kwargs.pop("format")
try:
return _PAINT_BUILDERS[fmt](self, kwargs)
except KeyError:
raise NotImplementedError(fmt)
raise TypeError(
f"expected int, Mapping or ot.Paint, found {type(paint).__name__}: {paint!r}"
)
def buildPaintLinearGradient(
colorLine: _ColorLineInput,
p0: _PointTuple,
@ -487,115 +595,50 @@ def buildPaintRadialGradient(
return self
def buildPaintGlyph(glyph: str, paint: _PaintInput) -> ot.Paint:
def buildPaintGlyph(layerCollector: LayerCollector, glyph: str, paint: _PaintInput) -> ot.Paint:
self = ot.Paint()
self.Format = int(ot.Paint.Format.PaintGlyph)
self.Glyph = glyph
self.Paint = buildPaint(paint)
self.Paint = layerCollector.build(paint)
return self
def buildPaintColrSlice(
glyph: str, firstLayerIndex: int = 0, lastLayerIndex: int = 255
def buildPaintColrGlyph(
glyph: str
) -> ot.Paint:
self = ot.Paint()
self.Format = int(ot.Paint.Format.PaintColrSlice)
self.Format = int(ot.Paint.Format.PaintColrGlyph)
self.Glyph = glyph
if firstLayerIndex > lastLayerIndex:
raise ValueError(
f"Expected first <= last index, found: {firstLayerIndex} > {lastLayerIndex}"
)
for prefix in ("first", "last"):
indexName = f"{prefix}LayerIndex"
index = locals()[indexName]
if index < 0 or index > 255:
raise OverflowError(f"{indexName} ({index}) out of range [0..255]")
self.FirstLayerIndex = firstLayerIndex
self.LastLayerIndex = lastLayerIndex
return self
def buildPaintTransform(transform: _AffineInput, paint: _PaintInput) -> ot.Paint:
def buildPaintTransform(layerCollector: LayerCollector, transform: _AffineInput, paint: _PaintInput) -> ot.Paint:
self = ot.Paint()
self.Format = int(ot.Paint.Format.PaintTransform)
if not isinstance(transform, ot.Affine2x3):
transform = buildAffine2x3(transform)
self.Transform = transform
self.Paint = buildPaint(paint)
self.Paint = layerCollector.build(paint)
return self
def buildPaintComposite(
mode: _CompositeInput, source: _PaintInput, backdrop: _PaintInput
layerCollector: LayerCollector, mode: _CompositeInput, source: _PaintInput, backdrop: _PaintInput
):
self = ot.Paint()
self.Format = int(ot.Paint.Format.PaintComposite)
self.SourcePaint = buildPaint(source)
self.SourcePaint = layerCollector.build(source)
self.CompositeMode = _to_composite_mode(mode)
self.BackdropPaint = buildPaint(backdrop)
return self
_PAINT_BUILDERS = {
1: buildPaintSolid,
2: buildPaintLinearGradient,
3: buildPaintRadialGradient,
4: buildPaintGlyph,
5: buildPaintColrSlice,
6: buildPaintTransform,
7: buildPaintComposite,
}
def buildPaint(paint: _PaintInput) -> ot.Paint:
if isinstance(paint, ot.Paint):
return paint
elif isinstance(paint, int):
paletteIndex = paint
return buildPaintSolid(paletteIndex)
elif isinstance(paint, tuple):
layerGlyph, paint = paint
return buildPaintGlyph(layerGlyph, paint)
elif isinstance(paint, collections.abc.Mapping):
kwargs = dict(paint)
fmt = kwargs.pop("format")
try:
return _PAINT_BUILDERS[fmt](**kwargs)
except KeyError:
raise NotImplementedError(fmt)
raise TypeError(
f"expected int, Mapping or ot.Paint, found {type(paint).__name__}: {paint!r}"
)
def buildLayerV1List(layers: _PaintInputList) -> ot.LayerV1List:
self = ot.LayerV1List()
layerCount = len(layers)
self.LayerCount = layerCount
self.Paint = [buildPaint(layer) for layer in layers]
return self
def buildPaintColrLayers(firstLayerIndex: int, numLayers: int) -> ot.Paint:
self = ot.Paint()
self.Format = int(ot.Paint.Format.PaintColrLayers)
if numLayers > MAX_PAINT_COLR_LAYER_COUNT:
raise OverflowError(
"PaintColrLayers.NumLayers: {numLayers} > {MAX_PAINT_COLR_LAYER_COUNT}"
)
self.NumLayers = numLayers
self.FirstLayerIndex = firstLayerIndex
self.BackdropPaint = layerCollector.build(backdrop)
return self
def buildBaseGlyphV1Record(
baseGlyph: str, layers: Union[_PaintInputList, ot.LayerV1List]
baseGlyph: str, layerCollector: LayerCollector, paint: _PaintInput
) -> ot.BaseGlyphV1List:
self = ot.BaseGlyphV1Record()
self.BaseGlyph = baseGlyph
if not isinstance(layers, ot.LayerV1List):
layers = buildLayerV1List(layers)
self.LayerV1List = layers
self.Paint = layerCollector.build(paint)
return self
@ -606,10 +649,10 @@ def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
return "\n".join(lines)
def buildBaseGlyphV1List(
def buildColrV1(
colorGlyphs: _ColorGlyphsDict,
glyphMap: Optional[Mapping[str, int]] = None,
) -> ot.BaseGlyphV1List:
) -> Tuple[ot.LayerV1List, ot.BaseGlyphV1List]:
if glyphMap is not None:
colorGlyphItems = sorted(
colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
@ -618,10 +661,12 @@ def buildBaseGlyphV1List(
colorGlyphItems = colorGlyphs.items()
errors = {}
records = []
for baseGlyph, layers in colorGlyphItems:
baseGlyphs = []
layerCollector = LayerCollector()
for baseGlyph, paint in colorGlyphItems:
try:
records.append(buildBaseGlyphV1Record(baseGlyph, layers))
baseGlyphs.append(buildBaseGlyphV1Record(baseGlyph, layerCollector, paint))
except (ColorLibError, OverflowError, ValueError, TypeError) as e:
errors[baseGlyph] = e
@ -631,7 +676,10 @@ def buildBaseGlyphV1List(
exc.errors = errors
raise exc from next(iter(errors.values()))
self = ot.BaseGlyphV1List()
self.BaseGlyphCount = len(records)
self.BaseGlyphV1Record = records
return self
layers = ot.LayerV1List()
layers.LayerCount = len(layerCollector.Layers)
layers.Paint = layerCollector.Layers
glyphs = ot.BaseGlyphV1List()
glyphs.BaseGlyphCount = len(baseGlyphs)
glyphs.BaseGlyphV1Record = baseGlyphs
return (layers, glyphs)

View File

@ -1647,8 +1647,6 @@ otData = [
('PaintFormat5', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
('GlyphID', 'Glyph', None, None, 'Virtual glyph ID for a BaseGlyphV1List base glyph.'),
('uint8', 'FirstLayerIndex', None, None, 'First layer index to reuse'),
('uint8', 'LastLayerIndex', None, None, 'Last layer index to reuse, inclusive'),
]),
('PaintFormat6', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 6'),

View File

@ -1331,7 +1331,7 @@ class Paint(getFormatSwitchingBaseTableClass("uint8")):
PaintLinearGradient = 2
PaintRadialGradient = 3
PaintGlyph = 4
PaintColrSlice = 5
PaintColrGlyph = 5
PaintTransform = 6
PaintComposite = 7
PaintColrLayers = 8

View File

@ -1,8 +1,10 @@
from fontTools.ttLib import newTable
from fontTools.ttLib.tables import otTables as ot
from fontTools.colorLib import builder
from fontTools.colorLib.builder import LayerCollector
from fontTools.colorLib.errors import ColorLibError
import pytest
from typing import List
def test_buildCOLR_v0():
@ -345,18 +347,22 @@ def test_buildPaintRadialGradient():
assert gradient.ColorLine.ColorStop == color_stops
def test_buildPaintGlyph():
layer = builder.buildPaintGlyph("a", 2)
def test_buildPaintGlyph_Solid():
collector = LayerCollector()
layer = builder.buildPaintGlyph(collector, "a", 2)
assert layer.Glyph == "a"
assert layer.Paint.Format == ot.Paint.Format.PaintSolid
assert layer.Paint.Color.PaletteIndex == 2
layer = builder.buildPaintGlyph("a", builder.buildPaintSolid(3, 0.9))
layer = builder.buildPaintGlyph(collector, "a", builder.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():
layer = builder.buildPaintGlyph(
LayerCollector(),
"a",
builder.buildPaintLinearGradient(
{"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250)
@ -372,7 +378,10 @@ def test_buildPaintGlyph():
assert layer.Paint.x1.value == 150
assert layer.Paint.y1.value == 250
def test_buildPaintGlyph_RadialGradient():
layer = builder.buildPaintGlyph(
LayerCollector(),
"a",
builder.buildPaintRadialGradient(
{
@ -404,13 +413,16 @@ def test_buildPaintGlyph():
assert layer.Paint.r1.value == 10
def test_buildPaintGlyph_from_dict():
layer = builder.buildPaintGlyph("a", {"format": 1, "paletteIndex": 0})
def test_buildPaintGlyph_Dict_Solid():
layer = builder.buildPaintGlyph(LayerCollector(), "a", {"format": 1, "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():
layer = builder.buildPaintGlyph(
LayerCollector(),
"a",
{
"format": 2,
@ -422,7 +434,10 @@ def test_buildPaintGlyph_from_dict():
assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
def test_buildPaintGlyph_Dict_RadialGradient():
layer = builder.buildPaintGlyph(
LayerCollector(),
"a",
{
"format": 3,
@ -437,29 +452,18 @@ def test_buildPaintGlyph_from_dict():
assert layer.Paint.r0.value == 4
def test_buildPaintColrSlice():
paint = builder.buildPaintColrSlice("a")
assert paint.Format == ot.Paint.Format.PaintColrSlice
def test_buildPaintColrGlyph():
paint = builder.buildPaintColrGlyph("a")
assert paint.Format == ot.Paint.Format.PaintColrGlyph
assert paint.Glyph == "a"
assert paint.FirstLayerIndex == 0
assert paint.LastLayerIndex == 255
paint = builder.buildPaintColrSlice("a", firstLayerIndex=1, lastLayerIndex=254)
assert paint.FirstLayerIndex == 1
assert paint.LastLayerIndex == 254
with pytest.raises(ValueError, match="Expected first <= last index"):
builder.buildPaintColrSlice("a", 255, 0)
with pytest.raises(OverflowError, match="firstLayerIndex .* out of range"):
builder.buildPaintColrSlice("a", -1, 255)
with pytest.raises(OverflowError, match="lastLayerIndex .* out of range"):
builder.buildPaintColrSlice("a", 0, 256)
def test_buildPaintTransform():
paint = builder.buildPaintTransform(
layerCollector=LayerCollector(),
transform=builder.buildAffine2x3((1, 2, 3, 4, 5, 6)),
paint=builder.buildPaintGlyph(
layerCollector=LayerCollector(),
glyph="a",
paint=builder.buildPaintSolid(paletteIndex=0, alpha=1.0),
),
@ -475,6 +479,7 @@ def test_buildPaintTransform():
assert paint.Paint.Format == ot.Paint.Format.PaintGlyph
paint = builder.buildPaintTransform(
LayerCollector(),
(1, 0, 0, 0.3333, 10, 10),
{
"format": 3,
@ -497,7 +502,9 @@ def test_buildPaintTransform():
def test_buildPaintComposite():
collector = LayerCollector()
composite = builder.buildPaintComposite(
layerCollector=collector,
mode=ot.CompositeMode.SRC_OVER,
source={
"format": 7,
@ -506,7 +513,7 @@ def test_buildPaintComposite():
"backdrop": {"format": 4, "glyph": "b", "paint": 1},
},
backdrop=builder.buildPaintGlyph(
"a", builder.buildPaintSolid(paletteIndex=0, alpha=1.0)
collector, "a", builder.buildPaintSolid(paletteIndex=0, alpha=1.0)
),
)
@ -530,56 +537,7 @@ def test_buildPaintComposite():
assert composite.BackdropPaint.Paint.Color.PaletteIndex == 0
def test_buildLayerV1List():
layers = [
("a", 1),
("b", {"format": 1, "paletteIndex": 2, "alpha": 0.5}),
(
"c",
{
"format": 2,
"colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "repeat"},
"p0": (100, 200),
"p1": (150, 250),
},
),
(
"d",
{
"format": 3,
"colorLine": {
"stops": [
{"offset": 0.0, "paletteIndex": 5},
{"offset": 0.5, "paletteIndex": 6, "alpha": 0.8},
{"offset": 1.0, "paletteIndex": 7},
]
},
"c0": (50, 50),
"c1": (75, 75),
"r0": 30,
"r1": 10,
},
),
builder.buildPaintGlyph("e", builder.buildPaintSolid(8)),
]
layers = builder.buildLayerV1List(layers)
assert layers.LayerCount == len(layers.Paint)
assert all(isinstance(l, ot.Paint) for l in layers.Paint)
def test_buildBaseGlyphV1Record():
baseGlyphRec = builder.buildBaseGlyphV1Record("a", [("b", 0), ("c", 1)])
assert baseGlyphRec.BaseGlyph == "a"
assert isinstance(baseGlyphRec.LayerV1List, ot.LayerV1List)
layers = builder.buildLayerV1List([("b", 0), ("c", 1)])
baseGlyphRec = builder.buildBaseGlyphV1Record("a", layers)
assert baseGlyphRec.BaseGlyph == "a"
assert baseGlyphRec.LayerV1List == layers
def test_buildBaseGlyphV1List():
def test_buildColrV1():
colorGlyphs = {
"a": [("b", 0), ("c", 1)],
"d": [
@ -596,7 +554,7 @@ def test_buildBaseGlyphV1List():
},
),
],
"g": builder.buildLayerV1List([("h", 5)]),
"g": [("h", 5)],
}
glyphMap = {
".notdef": 0,
@ -610,13 +568,14 @@ def test_buildBaseGlyphV1List():
"h": 8,
}
baseGlyphs = builder.buildBaseGlyphV1List(colorGlyphs, glyphMap)
# 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"
baseGlyphs = builder.buildBaseGlyphV1List(colorGlyphs)
layers, baseGlyphs = builder.buildColrV1(colorGlyphs)
assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a"
assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "d"
@ -669,6 +628,173 @@ def test_split_color_glyphs_by_version():
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 PaintColorLayers (format 8), no layerlist
colr = builder.buildCOLR(
{
"a": {
"format": 4, # PaintGlyph
"paint": {"format": 1, "paletteIndex": 2, "alpha": 0.8},
"glyph": "b",
},
"b": {
"format": 4, # PaintGlyph
"paint": {
"format": 2,
"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": 1, "paletteIndex": 2, "alpha": 0.8}
backdrop = {
"format": 4, # PaintGlyph
"paint": solid_paint,
"glyph": "back",
}
a_foreground = {
"format": 4, # PaintGlyph
"paint": solid_paint,
"glyph": "a_fore",
}
b_foreground = {
"format": 4, # 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": 1, "paletteIndex": 2, "alpha": 0.8}
backdrop = [
{
"format": 4, # PaintGlyph
"paint": solid_paint,
"glyph": "back1",
},
{
"format": 4, # PaintGlyph
"paint": solid_paint,
"glyph": "back2",
},
]
a_foreground = {
"format": 4, # PaintGlyph
"paint": solid_paint,
"glyph": "a_fore",
}
b_background = {
"format": 4, # PaintGlyph
"paint": solid_paint,
"glyph": "b_back",
}
b_foreground = {
"format": 4, # PaintGlyph
"paint": solid_paint,
"glyph": "b_fore",
}
c_background = {
"format": 4, # 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([baseGlyphs[0].Paint]), ["Layers[0:4]"]
assert _paint_names([baseGlyphs[0].Paint]), ["Layers[0:4]"]
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
class BuildCOLRTest(object):
def test_automatic_version_all_solid_color_glyphs(self):
colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]})
@ -714,10 +840,7 @@ class BuildCOLRTest(object):
],
}
)
assert colr.version == 1
assert not hasattr(colr, "ColorLayers")
assert hasattr(colr, "table")
assert isinstance(colr.table, ot.COLR)
assertIsColrV1(colr)
assert colr.table.BaseGlyphRecordCount == 0
assert colr.table.BaseGlyphRecordArray is None
assert colr.table.LayerRecordCount == 0
@ -741,10 +864,7 @@ class BuildCOLRTest(object):
],
}
)
assert colr.version == 1
assert not hasattr(colr, "ColorLayers")
assert hasattr(colr, "table")
assert isinstance(colr.table, ot.COLR)
assertIsColrV1(colr)
assert colr.table.VarStore is None
assert colr.table.BaseGlyphRecordCount == 1
@ -759,12 +879,9 @@ class BuildCOLRTest(object):
)
assert colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].BaseGlyph == "d"
assert isinstance(
colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].LayerV1List, ot.LayerV1List
)
assert (
colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].LayerV1List.Paint[0].Glyph
== "e"
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)