colorLib: update builders to latest COLRv1 draft

LayerV1Record and Affine2x2 are gone.
LayerV1List now contains a list of Paint DAGs
Added 4 new Paint formats: PaintGlyph, PaintColorGlyph, PaintTransform
and PaintComposite
This commit is contained in:
Cosimo Lupo 2020-10-09 18:16:51 +01:00
parent 7f6a05b007
commit fdf6a5c1fc
No known key found for this signature in database
GPG Key ID: 179A8F0895A02F4F
2 changed files with 148 additions and 135 deletions

View File

@ -6,13 +6,26 @@ import collections
import copy
import enum
from functools import partial
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
from typing import (
Any,
Dict,
Iterable,
List,
Mapping,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
)
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 import otTables as ot
from fontTools.ttLib.tables.otTables import (
ExtendMode,
CompositeMode,
VariableValue,
VariableFloat,
VariableInt,
@ -21,11 +34,11 @@ from .errors import ColorLibError
# TODO move type aliases to colorLib.types?
T = TypeVar("T")
_Kwargs = Mapping[str, Any]
_PaintInput = Union[int, _Kwargs, ot.Paint]
_LayerTuple = Tuple[str, _PaintInput]
_LayersList = Sequence[_LayerTuple]
_ColorGlyphsDict = Dict[str, _LayersList]
_PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]]
_PaintInputList = Sequence[_PaintInput]
_ColorGlyphsDict = Dict[str, Union[_PaintInputList, ot.LayerV1List]]
_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
_Number = Union[int, float]
_ScalarInput = Union[_Number, VariableValue, Tuple[_Number, int]]
@ -33,10 +46,13 @@ _ColorStopTuple = Tuple[_ScalarInput, int]
_ColorStopInput = Union[_ColorStopTuple, _Kwargs, ot.ColorStop]
_ColorStopsList = Sequence[_ColorStopInput]
_ExtendInput = Union[int, str, ExtendMode]
_CompositeInput = Union[int, str, CompositeMode]
_ColorLineInput = Union[_Kwargs, ot.ColorLine]
_PointTuple = Tuple[_ScalarInput, _ScalarInput]
_AffineTuple = Tuple[_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput]
_AffineInput = Union[_AffineTuple, ot.Affine2x2]
_AffineTuple = Tuple[
_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput
]
_AffineInput = Union[_AffineTuple, ot.Affine2x3]
def populateCOLRv0(
@ -91,14 +107,13 @@ def buildCOLR(
"""Build COLR table from color layers mapping.
Args:
colorGlyphs: map of base glyph names to lists of (layer glyph names,
Paint) tuples. For COLRv0, a paint is simply the color palette index
(int); for COLRv1, paint can be either solid colors (with variable
opacity), linear gradients or radial gradients.
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.
version: the version of COLR table. If None, the version is determined
by the presence of gradients or variation data (varStore), which
require version 1; otherwise, if there are only simple colors, version
0 is used.
by the presence of COLRv1 paints or variation data (varStore), which
require version 1; otherwise, if all base glyphs use only simple color
layers, version 0 is used.
glyphMap: a map from glyph names to glyph indices, as returned from
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
varStore: Optional ItemVarationStore for deltas associated with v1 layer.
@ -113,10 +128,9 @@ def buildCOLR(
if version in (None, 0) and not varStore:
# split color glyphs into v0 and v1 and encode separately
colorGlyphsV0, colorGlyphsV1 = _splitSolidAndGradientGlyphs(colorGlyphs)
colorGlyphsV0, colorGlyphsV1 = _split_color_glyphs_by_version(colorGlyphs)
if version == 0 and colorGlyphsV1:
# TODO Derive "average" solid color from gradients?
raise ValueError("Can't encode gradients in COLRv0")
raise ValueError("Can't encode COLRv1 glyphs in COLRv0")
else:
# unless explicitly requested for v1 or have variations, in which case
# we encode all color glyph as v1
@ -277,29 +291,16 @@ def buildCPAL(
_DEFAULT_ALPHA = VariableFloat(1.0)
def _splitSolidAndGradientGlyphs(
def _split_color_glyphs_by_version(
colorGlyphs: _ColorGlyphsDict,
) -> Tuple[Dict[str, List[Tuple[str, int]]], Dict[str, List[Tuple[str, ot.Paint]]]]:
) -> Tuple[_ColorGlyphsV0Dict, _ColorGlyphsDict]:
colorGlyphsV0 = {}
colorGlyphsV1 = {}
for baseGlyph, layers in colorGlyphs.items():
newLayers = []
allSolidColors = True
for layerGlyph, paint in layers:
paint = _to_ot_paint(paint)
if (
paint.Format != 1
or paint.Color.Alpha.value != _DEFAULT_ALPHA.value
):
allSolidColors = False
newLayers.append((layerGlyph, paint))
if allSolidColors:
colorGlyphsV0[baseGlyph] = [
(layerGlyph, paint.Color.PaletteIndex)
for layerGlyph, paint in newLayers
]
if all(isinstance(l, tuple) and isinstance(l[1], int) for l in layers):
colorGlyphsV0[baseGlyph] = layers
else:
colorGlyphsV1[baseGlyph] = newLayers
colorGlyphsV1[baseGlyph] = layers
# sanity check
assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
@ -351,15 +352,23 @@ def buildColorStop(
return self
def _to_extend_mode(v: _ExtendInput) -> ExtendMode:
if isinstance(v, ExtendMode):
def _to_enum_value(v: Union[str, int, T], enumClass: Type[T]) -> T:
if isinstance(v, enumClass):
return v
elif isinstance(v, str):
try:
return getattr(ExtendMode, v.upper())
return getattr(enumClass, v.upper())
except AttributeError:
raise ValueError(f"{v!r} is not a valid ExtendMode")
return ExtendMode(v)
raise ValueError(f"{v!r} is not a valid {enumClass.__name__}")
return enumClass(v)
def _to_extend_mode(v: _ExtendInput) -> ExtendMode:
return _to_enum_value(v, ExtendMode)
def _to_composite_mode(v: _CompositeInput) -> CompositeMode:
return _to_enum_value(v, CompositeMode)
def buildColorLine(
@ -406,12 +415,17 @@ def buildLinearGradientPaint(
return self
def buildAffine2x2(
xx: _ScalarInput, xy: _ScalarInput, yx: _ScalarInput, yy: _ScalarInput
) -> ot.Affine2x2:
self = ot.Affine2x2()
def buildAffine2x3(
xx: _ScalarInput,
xy: _ScalarInput,
yx: _ScalarInput,
yy: _ScalarInput,
dx: _ScalarInput,
dy: _ScalarInput,
) -> ot.Affine2x3:
self = ot.Affine2x3()
locs = locals()
for attr in ("xx", "xy", "yx", "yy"):
for attr in ("xx", "xy", "yx", "yy", "dx", "dy"):
value = locs[attr]
setattr(self, attr, _to_variable_float(value))
return self
@ -423,7 +437,6 @@ def buildRadialGradientPaint(
c1: _PointTuple,
r0: _ScalarInput,
r1: _ScalarInput,
transform: Optional[_AffineInput] = None,
) -> ot.Paint:
self = ot.Paint()
@ -435,50 +448,86 @@ def buildRadialGradientPaint(
setattr(self, f"y{i}", _to_variable_int(y))
setattr(self, f"r{i}", _to_variable_int(r))
if transform is not None and not isinstance(transform, ot.Affine2x2):
transform = buildAffine2x2(*transform)
self.Transform = transform
return self
def _to_ot_paint(paint: _PaintInput) -> ot.Paint:
def buildPaintGlyph(glyph: str, paint: _PaintInput) -> ot.Paint:
self = ot.Paint()
self.Format = 4
self.Glyph = glyph
self.Paint = buildPaint(paint)
return self
def buildPaintColorGlyph(glyph: str) -> ot.Paint:
self = ot.Paint()
self.Format = 5
self.Glyph = glyph
return self
def buildPaintTransform(transform: _AffineInput, paint: _PaintInput) -> ot.Paint:
self = ot.Paint()
self.Format = 6
if not isinstance(transform, ot.Affine2x3):
transform = buildAffine2x3(*transform)
self.Transform = transform
self.Paint = buildPaint(paint)
return self
def buildPaintComposite(
mode: _CompositeInput, source: _PaintInput, backdrop: _PaintInput
):
self = ot.Paint()
self.Format = 7
self.SourcePaint = buildPaint(source)
self.CompositeMode = _to_composite_mode(mode)
self.BackdropPaint = buildPaint(backdrop)
return self
_PAINT_BUILDERS = {
1: buildSolidColorPaint,
2: buildLinearGradientPaint,
3: buildRadialGradientPaint,
4: buildPaintGlyph,
5: buildPaintColorGlyph,
6: buildPaintTransform,
7: buildPaintComposite,
}
def buildPaint(paint: _PaintInput) -> ot.Paint:
if isinstance(paint, ot.Paint):
return paint
elif isinstance(paint, int):
paletteIndex = paint
return buildSolidColorPaint(paletteIndex)
elif isinstance(paint, tuple):
layerGlyph, paint = paint
return buildPaintGlyph(layerGlyph, paint)
elif isinstance(paint, collections.abc.Mapping):
return buildPaint(**paint)
raise TypeError(f"expected int, Mapping or ot.Paint, found {type(paint.__name__)}")
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 buildLayerV1Record(layerGlyph: str, paint: _PaintInput) -> ot.LayerV1Record:
self = ot.LayerV1Record()
self.LayerGlyph = layerGlyph
self.Paint = _to_ot_paint(paint)
return self
def buildLayerV1List(
layers: Sequence[Union[_LayerTuple, ot.LayerV1Record]]
) -> ot.LayerV1List:
def buildLayerV1List(layers: _PaintInputList) -> ot.LayerV1List:
self = ot.LayerV1List()
self.LayerCount = len(layers)
records = []
for layer in layers:
if isinstance(layer, ot.LayerV1Record):
record = layer
else:
layerGlyph, paint = layer
record = buildLayerV1Record(layerGlyph, paint)
records.append(record)
self.LayerV1Record = records
self.Paint = [buildPaint(layer) for layer in layers]
return self
def buildBaseGlyphV1Record(
baseGlyph: str, layers: Union[_LayersList, ot.LayerV1List]
baseGlyph: str, layers: Union[_PaintInputList, ot.LayerV1List]
) -> ot.BaseGlyphV1List:
self = ot.BaseGlyphV1Record()
self.BaseGlyph = baseGlyph
@ -489,7 +538,7 @@ def buildBaseGlyphV1Record(
def buildBaseGlyphV1List(
colorGlyphs: Union[_ColorGlyphsDict, Dict[str, ot.LayerV1List]],
colorGlyphs: _ColorGlyphsDict,
glyphMap: Optional[Mapping[str, int]] = None,
) -> ot.BaseGlyphV1List:
if glyphMap is not None:
@ -506,17 +555,3 @@ def buildBaseGlyphV1List(
self.BaseGlyphCount = len(records)
self.BaseGlyphV1Record = records
return self
_PAINT_BUILDERS = {
1: buildSolidColorPaint,
2: buildLinearGradientPaint,
3: buildRadialGradientPaint,
}
def buildPaint(format: int, **kwargs) -> ot.Paint:
try:
return _PAINT_BUILDERS[format](**kwargs)
except KeyError:
raise NotImplementedError(format)

View File

@ -284,12 +284,14 @@ def test_buildColorLine():
] == stops
def test_buildAffine2x2():
matrix = builder.buildAffine2x2(1.5, 0, 0.5, 2.0)
def test_buildAffine2x3():
matrix = builder.buildAffine2x3(1.5, 0, 0.5, 2.0, 1.0, -3.0)
assert matrix.xx == builder.VariableFloat(1.5)
assert matrix.xy == builder.VariableFloat(0.0)
assert matrix.yx == builder.VariableFloat(0.5)
assert matrix.yy == builder.VariableFloat(2.0)
assert matrix.dx == builder.VariableFloat(1.0)
assert matrix.dy == builder.VariableFloat(-3.0)
def test_buildLinearGradientPaint():
@ -337,36 +339,24 @@ def test_buildRadialGradientPaint():
assert (gradient.x1, gradient.y1) == c1
assert gradient.r0 == r0
assert gradient.r1 == r1
assert gradient.Transform is None
gradient = builder.buildRadialGradientPaint({"stops": color_stops}, c0, c1, r0, r1)
assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
assert gradient.ColorLine.ColorStop == color_stops
matrix = builder.buildAffine2x2(2.0, 0.0, 0.0, 2.0)
gradient = builder.buildRadialGradientPaint(
color_line, c0, c1, r0, r1, transform=matrix
)
assert gradient.Transform == matrix
gradient = builder.buildRadialGradientPaint(
color_line, c0, c1, r0, r1, transform=(2.0, 0.0, 0.0, 2.0)
)
assert gradient.Transform == matrix
def test_buildLayerV1Record():
layer = builder.buildLayerV1Record("a", 2)
assert layer.LayerGlyph == "a"
def test_buildPaintGlyph():
layer = builder.buildPaintGlyph("a", 2)
assert layer.Glyph == "a"
assert layer.Paint.Format == 1
assert layer.Paint.Color.PaletteIndex == 2
layer = builder.buildLayerV1Record("a", builder.buildSolidColorPaint(3, 0.9))
layer = builder.buildPaintGlyph("a", builder.buildSolidColorPaint(3, 0.9))
assert layer.Paint.Format == 1
assert layer.Paint.Color.PaletteIndex == 3
assert layer.Paint.Color.Alpha.value == 0.9
layer = builder.buildLayerV1Record(
layer = builder.buildPaintGlyph(
"a",
builder.buildLinearGradientPaint(
{"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250)
@ -382,7 +372,7 @@ def test_buildLayerV1Record():
assert layer.Paint.x1.value == 150
assert layer.Paint.y1.value == 250
layer = builder.buildLayerV1Record(
layer = builder.buildPaintGlyph(
"a",
builder.buildRadialGradientPaint(
{
@ -414,13 +404,13 @@ def test_buildLayerV1Record():
assert layer.Paint.r1.value == 10
def test_buildLayerV1Record_from_dict():
layer = builder.buildLayerV1Record("a", {"format": 1, "paletteIndex": 0})
assert layer.LayerGlyph == "a"
def test_buildPaintGlyph_from_dict():
layer = builder.buildPaintGlyph("a", {"format": 1, "paletteIndex": 0})
assert layer.Glyph == "a"
assert layer.Paint.Format == 1
assert layer.Paint.Color.PaletteIndex == 0
layer = builder.buildLayerV1Record(
layer = builder.buildPaintGlyph(
"a",
{
"format": 2,
@ -432,7 +422,7 @@ def test_buildLayerV1Record_from_dict():
assert layer.Paint.Format == 2
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
layer = builder.buildLayerV1Record(
layer = builder.buildPaintGlyph(
"a",
{
"format": 3,
@ -477,12 +467,12 @@ def test_buildLayerV1List():
"r1": 10,
},
),
builder.buildLayerV1Record("e", builder.buildSolidColorPaint(8)),
builder.buildPaintGlyph("e", builder.buildSolidColorPaint(8)),
]
layers = builder.buildLayerV1List(layers)
assert layers.LayerCount == len(layers.LayerV1Record)
assert all(isinstance(l, ot.LayerV1Record) for l in layers.LayerV1Record)
assert layers.LayerCount == len(layers.Paint)
assert all(isinstance(l, ot.Paint) for l in layers.Paint)
def test_buildBaseGlyphV1Record():
@ -540,17 +530,17 @@ def test_buildBaseGlyphV1List():
assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g"
def test_splitSolidAndGradientGlyphs():
def test_split_color_glyphs_by_version():
colorGlyphs = {
"a": [
("b", 0),
("c", 1),
("d", {"format": 1, "paletteIndex": 2}),
("e", builder.buildSolidColorPaint(paletteIndex=3)),
("d", 2),
("e", 3),
]
}
colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs)
assert colorGlyphsV0 == {"a": [("b", 0), ("c", 1), ("d", 2), ("e", 3)]}
assert not colorGlyphsV1
@ -559,7 +549,7 @@ def test_splitSolidAndGradientGlyphs():
"a": [("b", builder.buildSolidColorPaint(paletteIndex=0, alpha=0.0))]
}
colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs)
assert not colorGlyphsV0
assert colorGlyphsV1 == colorGlyphs
@ -580,23 +570,13 @@ def test_splitSolidAndGradientGlyphs():
],
}
colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs)
assert colorGlyphsV0 == {"a": [("b", 0)]}
assert "a" not in colorGlyphsV1
assert "c" in colorGlyphsV1
assert len(colorGlyphsV1["c"]) == 2
layer_d = colorGlyphsV1["c"][0]
assert layer_d[0] == "d"
assert isinstance(layer_d[1], ot.Paint)
assert layer_d[1].Format == 1
layer_e = colorGlyphsV1["c"][1]
assert layer_e[0] == "e"
assert isinstance(layer_e[1], ot.Paint)
assert layer_e[1].Format == 2
class BuildCOLRTest(object):
def test_automatic_version_all_solid_color_glyphs(self):
@ -691,9 +671,7 @@ class BuildCOLRTest(object):
colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].LayerV1List, ot.LayerV1List
)
assert (
colr.table.BaseGlyphV1List.BaseGlyphV1Record[0]
.LayerV1List.LayerV1Record[0]
.LayerGlyph
colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].LayerV1List.Paint[0].Glyph
== "e"
)