Start hooking up revised PaintColrLayers
This commit is contained in:
parent
581416d77c
commit
f531038bf9
@ -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)
|
||||
|
@ -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'),
|
||||
|
@ -1331,7 +1331,7 @@ class Paint(getFormatSwitchingBaseTableClass("uint8")):
|
||||
PaintLinearGradient = 2
|
||||
PaintRadialGradient = 3
|
||||
PaintGlyph = 4
|
||||
PaintColrSlice = 5
|
||||
PaintColrGlyph = 5
|
||||
PaintTransform = 6
|
||||
PaintComposite = 7
|
||||
PaintColrLayers = 8
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user