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 ( from typing import (
Any, Any,
Dict, Dict,
Generator,
Iterable, Iterable,
List, List,
Mapping, 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_O_L_R_
from fontTools.ttLib.tables import C_P_A_L_ from fontTools.ttLib.tables import C_P_A_L_
from fontTools.ttLib.tables import _n_a_m_e 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 import otTables as ot
from fontTools.ttLib.tables.otTables import ( from fontTools.ttLib.tables.otTables import (
ExtendMode, ExtendMode,
@ -111,8 +113,8 @@ def buildCOLR(
Args: Args:
colorGlyphs: map of base glyph name to, either list of (layer glyph name, colorGlyphs: map of base glyph name to, either list of (layer glyph name,
color palette index) tuples for COLRv0; or list of Paints (dicts) color palette index) tuples for COLRv0; or a single Paint (dict) or
for COLRv1. list of Paint for COLRv1.
version: the version of COLR table. If None, the version is determined version: the version of COLR table. If None, the version is determined
by the presence of COLRv1 paints or variation data (varStore), which by the presence of COLRv1 paints or variation data (varStore), which
require version 1; otherwise, if all base glyphs use only simple color require version 1; otherwise, if all base glyphs use only simple color
@ -148,7 +150,8 @@ def buildCOLR(
colr.BaseGlyphRecordArray = colr.LayerRecordArray = None colr.BaseGlyphRecordArray = colr.LayerRecordArray = None
if colorGlyphsV1: if colorGlyphsV1:
colr.BaseGlyphV1List = buildBaseGlyphV1List(colorGlyphsV1, glyphMap) colr.LayerV1List, colr.BaseGlyphV1List = buildColrV1(colorGlyphsV1, glyphMap)
if version is None: if version is None:
version = 1 if (varStore or colorGlyphsV1) else 0 version = 1 if (varStore or colorGlyphsV1) else 0
@ -430,6 +433,111 @@ def _to_color_line(obj):
raise TypeError(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( def buildPaintLinearGradient(
colorLine: _ColorLineInput, colorLine: _ColorLineInput,
p0: _PointTuple, p0: _PointTuple,
@ -487,115 +595,50 @@ def buildPaintRadialGradient(
return self return self
def buildPaintGlyph(glyph: str, paint: _PaintInput) -> ot.Paint: def buildPaintGlyph(layerCollector: LayerCollector, glyph: str, paint: _PaintInput) -> ot.Paint:
self = ot.Paint() self = ot.Paint()
self.Format = int(ot.Paint.Format.PaintGlyph) self.Format = int(ot.Paint.Format.PaintGlyph)
self.Glyph = glyph self.Glyph = glyph
self.Paint = buildPaint(paint) self.Paint = layerCollector.build(paint)
return self return self
def buildPaintColrSlice( def buildPaintColrGlyph(
glyph: str, firstLayerIndex: int = 0, lastLayerIndex: int = 255 glyph: str
) -> ot.Paint: ) -> ot.Paint:
self = ot.Paint() self = ot.Paint()
self.Format = int(ot.Paint.Format.PaintColrSlice) self.Format = int(ot.Paint.Format.PaintColrGlyph)
self.Glyph = glyph 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 return self
def buildPaintTransform(transform: _AffineInput, paint: _PaintInput) -> ot.Paint: def buildPaintTransform(layerCollector: LayerCollector, transform: _AffineInput, paint: _PaintInput) -> ot.Paint:
self = ot.Paint() self = ot.Paint()
self.Format = int(ot.Paint.Format.PaintTransform) self.Format = int(ot.Paint.Format.PaintTransform)
if not isinstance(transform, ot.Affine2x3): if not isinstance(transform, ot.Affine2x3):
transform = buildAffine2x3(transform) transform = buildAffine2x3(transform)
self.Transform = transform self.Transform = transform
self.Paint = buildPaint(paint) self.Paint = layerCollector.build(paint)
return self return self
def buildPaintComposite( def buildPaintComposite(
mode: _CompositeInput, source: _PaintInput, backdrop: _PaintInput layerCollector: LayerCollector, mode: _CompositeInput, source: _PaintInput, backdrop: _PaintInput
): ):
self = ot.Paint() self = ot.Paint()
self.Format = int(ot.Paint.Format.PaintComposite) self.Format = int(ot.Paint.Format.PaintComposite)
self.SourcePaint = buildPaint(source) self.SourcePaint = layerCollector.build(source)
self.CompositeMode = _to_composite_mode(mode) self.CompositeMode = _to_composite_mode(mode)
self.BackdropPaint = buildPaint(backdrop) self.BackdropPaint = layerCollector.build(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
return self return self
def buildBaseGlyphV1Record( def buildBaseGlyphV1Record(
baseGlyph: str, layers: Union[_PaintInputList, ot.LayerV1List] baseGlyph: str, layerCollector: LayerCollector, paint: _PaintInput
) -> ot.BaseGlyphV1List: ) -> ot.BaseGlyphV1List:
self = ot.BaseGlyphV1Record() self = ot.BaseGlyphV1Record()
self.BaseGlyph = baseGlyph self.BaseGlyph = baseGlyph
if not isinstance(layers, ot.LayerV1List): self.Paint = layerCollector.build(paint)
layers = buildLayerV1List(layers)
self.LayerV1List = layers
return self return self
@ -606,10 +649,10 @@ def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
return "\n".join(lines) return "\n".join(lines)
def buildBaseGlyphV1List( def buildColrV1(
colorGlyphs: _ColorGlyphsDict, colorGlyphs: _ColorGlyphsDict,
glyphMap: Optional[Mapping[str, int]] = None, glyphMap: Optional[Mapping[str, int]] = None,
) -> ot.BaseGlyphV1List: ) -> Tuple[ot.LayerV1List, ot.BaseGlyphV1List]:
if glyphMap is not None: if glyphMap is not None:
colorGlyphItems = sorted( colorGlyphItems = sorted(
colorGlyphs.items(), key=lambda item: glyphMap[item[0]] colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
@ -618,10 +661,12 @@ def buildBaseGlyphV1List(
colorGlyphItems = colorGlyphs.items() colorGlyphItems = colorGlyphs.items()
errors = {} errors = {}
records = [] baseGlyphs = []
for baseGlyph, layers in colorGlyphItems: layerCollector = LayerCollector()
for baseGlyph, paint in colorGlyphItems:
try: try:
records.append(buildBaseGlyphV1Record(baseGlyph, layers)) baseGlyphs.append(buildBaseGlyphV1Record(baseGlyph, layerCollector, paint))
except (ColorLibError, OverflowError, ValueError, TypeError) as e: except (ColorLibError, OverflowError, ValueError, TypeError) as e:
errors[baseGlyph] = e errors[baseGlyph] = e
@ -631,7 +676,10 @@ def buildBaseGlyphV1List(
exc.errors = errors exc.errors = errors
raise exc from next(iter(errors.values())) raise exc from next(iter(errors.values()))
self = ot.BaseGlyphV1List() layers = ot.LayerV1List()
self.BaseGlyphCount = len(records) layers.LayerCount = len(layerCollector.Layers)
self.BaseGlyphV1Record = records layers.Paint = layerCollector.Layers
return self glyphs = ot.BaseGlyphV1List()
glyphs.BaseGlyphCount = len(baseGlyphs)
glyphs.BaseGlyphV1Record = baseGlyphs
return (layers, glyphs)

View File

@ -1647,8 +1647,6 @@ otData = [
('PaintFormat5', [ ('PaintFormat5', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'), ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
('GlyphID', 'Glyph', None, None, 'Virtual glyph ID for a BaseGlyphV1List base glyph.'), ('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', [ ('PaintFormat6', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 6'), ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 6'),

View File

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

View File

@ -1,8 +1,10 @@
from fontTools.ttLib import newTable from fontTools.ttLib import newTable
from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables import otTables as ot
from fontTools.colorLib import builder from fontTools.colorLib import builder
from fontTools.colorLib.builder import LayerCollector
from fontTools.colorLib.errors import ColorLibError from fontTools.colorLib.errors import ColorLibError
import pytest import pytest
from typing import List
def test_buildCOLR_v0(): def test_buildCOLR_v0():
@ -345,18 +347,22 @@ def test_buildPaintRadialGradient():
assert gradient.ColorLine.ColorStop == color_stops assert gradient.ColorLine.ColorStop == color_stops
def test_buildPaintGlyph(): def test_buildPaintGlyph_Solid():
layer = builder.buildPaintGlyph("a", 2) collector = LayerCollector()
layer = builder.buildPaintGlyph(collector, "a", 2)
assert layer.Glyph == "a" assert layer.Glyph == "a"
assert layer.Paint.Format == ot.Paint.Format.PaintSolid assert layer.Paint.Format == ot.Paint.Format.PaintSolid
assert layer.Paint.Color.PaletteIndex == 2 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.Format == ot.Paint.Format.PaintSolid
assert layer.Paint.Color.PaletteIndex == 3 assert layer.Paint.Color.PaletteIndex == 3
assert layer.Paint.Color.Alpha.value == 0.9 assert layer.Paint.Color.Alpha.value == 0.9
def test_buildPaintGlyph_LinearGradient():
layer = builder.buildPaintGlyph( layer = builder.buildPaintGlyph(
LayerCollector(),
"a", "a",
builder.buildPaintLinearGradient( builder.buildPaintLinearGradient(
{"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250) {"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.x1.value == 150
assert layer.Paint.y1.value == 250 assert layer.Paint.y1.value == 250
def test_buildPaintGlyph_RadialGradient():
layer = builder.buildPaintGlyph( layer = builder.buildPaintGlyph(
LayerCollector(),
"a", "a",
builder.buildPaintRadialGradient( builder.buildPaintRadialGradient(
{ {
@ -404,13 +413,16 @@ def test_buildPaintGlyph():
assert layer.Paint.r1.value == 10 assert layer.Paint.r1.value == 10
def test_buildPaintGlyph_from_dict(): def test_buildPaintGlyph_Dict_Solid():
layer = builder.buildPaintGlyph("a", {"format": 1, "paletteIndex": 0}) layer = builder.buildPaintGlyph(LayerCollector(), "a", {"format": 1, "paletteIndex": 0})
assert layer.Glyph == "a" assert layer.Glyph == "a"
assert layer.Paint.Format == ot.Paint.Format.PaintSolid assert layer.Paint.Format == ot.Paint.Format.PaintSolid
assert layer.Paint.Color.PaletteIndex == 0 assert layer.Paint.Color.PaletteIndex == 0
def test_buildPaintGlyph_Dict_LinearGradient():
layer = builder.buildPaintGlyph( layer = builder.buildPaintGlyph(
LayerCollector(),
"a", "a",
{ {
"format": 2, "format": 2,
@ -422,7 +434,10 @@ def test_buildPaintGlyph_from_dict():
assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0 assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
def test_buildPaintGlyph_Dict_RadialGradient():
layer = builder.buildPaintGlyph( layer = builder.buildPaintGlyph(
LayerCollector(),
"a", "a",
{ {
"format": 3, "format": 3,
@ -437,29 +452,18 @@ def test_buildPaintGlyph_from_dict():
assert layer.Paint.r0.value == 4 assert layer.Paint.r0.value == 4
def test_buildPaintColrSlice(): def test_buildPaintColrGlyph():
paint = builder.buildPaintColrSlice("a") paint = builder.buildPaintColrGlyph("a")
assert paint.Format == ot.Paint.Format.PaintColrSlice assert paint.Format == ot.Paint.Format.PaintColrGlyph
assert paint.Glyph == "a" 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(): def test_buildPaintTransform():
paint = builder.buildPaintTransform( paint = builder.buildPaintTransform(
layerCollector=LayerCollector(),
transform=builder.buildAffine2x3((1, 2, 3, 4, 5, 6)), transform=builder.buildAffine2x3((1, 2, 3, 4, 5, 6)),
paint=builder.buildPaintGlyph( paint=builder.buildPaintGlyph(
layerCollector=LayerCollector(),
glyph="a", glyph="a",
paint=builder.buildPaintSolid(paletteIndex=0, alpha=1.0), paint=builder.buildPaintSolid(paletteIndex=0, alpha=1.0),
), ),
@ -475,6 +479,7 @@ def test_buildPaintTransform():
assert paint.Paint.Format == ot.Paint.Format.PaintGlyph assert paint.Paint.Format == ot.Paint.Format.PaintGlyph
paint = builder.buildPaintTransform( paint = builder.buildPaintTransform(
LayerCollector(),
(1, 0, 0, 0.3333, 10, 10), (1, 0, 0, 0.3333, 10, 10),
{ {
"format": 3, "format": 3,
@ -497,7 +502,9 @@ def test_buildPaintTransform():
def test_buildPaintComposite(): def test_buildPaintComposite():
collector = LayerCollector()
composite = builder.buildPaintComposite( composite = builder.buildPaintComposite(
layerCollector=collector,
mode=ot.CompositeMode.SRC_OVER, mode=ot.CompositeMode.SRC_OVER,
source={ source={
"format": 7, "format": 7,
@ -506,7 +513,7 @@ def test_buildPaintComposite():
"backdrop": {"format": 4, "glyph": "b", "paint": 1}, "backdrop": {"format": 4, "glyph": "b", "paint": 1},
}, },
backdrop=builder.buildPaintGlyph( 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 assert composite.BackdropPaint.Paint.Color.PaletteIndex == 0
def test_buildLayerV1List(): def test_buildColrV1():
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():
colorGlyphs = { colorGlyphs = {
"a": [("b", 0), ("c", 1)], "a": [("b", 0), ("c", 1)],
"d": [ "d": [
@ -596,7 +554,7 @@ def test_buildBaseGlyphV1List():
}, },
), ),
], ],
"g": builder.buildLayerV1List([("h", 5)]), "g": [("h", 5)],
} }
glyphMap = { glyphMap = {
".notdef": 0, ".notdef": 0,
@ -610,13 +568,14 @@ def test_buildBaseGlyphV1List():
"h": 8, "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.BaseGlyphCount == len(colorGlyphs)
assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "d" assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "d"
assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "a" assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "a"
assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g" assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g"
baseGlyphs = builder.buildBaseGlyphV1List(colorGlyphs) layers, baseGlyphs = builder.buildColrV1(colorGlyphs)
assert baseGlyphs.BaseGlyphCount == len(colorGlyphs) assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a" assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a"
assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "d" assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "d"
@ -669,6 +628,173 @@ def test_split_color_glyphs_by_version():
assert len(colorGlyphsV1["c"]) == 2 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): class BuildCOLRTest(object):
def test_automatic_version_all_solid_color_glyphs(self): def test_automatic_version_all_solid_color_glyphs(self):
colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}) colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]})
@ -714,10 +840,7 @@ class BuildCOLRTest(object):
], ],
} }
) )
assert colr.version == 1 assertIsColrV1(colr)
assert not hasattr(colr, "ColorLayers")
assert hasattr(colr, "table")
assert isinstance(colr.table, ot.COLR)
assert colr.table.BaseGlyphRecordCount == 0 assert colr.table.BaseGlyphRecordCount == 0
assert colr.table.BaseGlyphRecordArray is None assert colr.table.BaseGlyphRecordArray is None
assert colr.table.LayerRecordCount == 0 assert colr.table.LayerRecordCount == 0
@ -741,10 +864,7 @@ class BuildCOLRTest(object):
], ],
} }
) )
assert colr.version == 1 assertIsColrV1(colr)
assert not hasattr(colr, "ColorLayers")
assert hasattr(colr, "table")
assert isinstance(colr.table, ot.COLR)
assert colr.table.VarStore is None assert colr.table.VarStore is None
assert colr.table.BaseGlyphRecordCount == 1 assert colr.table.BaseGlyphRecordCount == 1
@ -759,12 +879,9 @@ class BuildCOLRTest(object):
) )
assert colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].BaseGlyph == "d" assert colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].BaseGlyph == "d"
assert isinstance( assert isinstance(
colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].LayerV1List, ot.LayerV1List colr.table.LayerV1List, ot.LayerV1List
)
assert (
colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].LayerV1List.Paint[0].Glyph
== "e"
) )
assert colr.table.LayerV1List.Paint[0].Glyph == "e"
def test_explicit_version_0(self): def test_explicit_version_0(self):
colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0) colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0)