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 (
|
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)
|
||||||
|
@ -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'),
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user