colorLib: allow to build Paint, ColorLine, Color from dict

This commit is contained in:
Cosimo Lupo 2020-03-11 13:27:59 +00:00
parent 0149f40588
commit 5629d5d8d9
No known key found for this signature in database
GPG Key ID: 20D4A261E4A0E642
2 changed files with 190 additions and 72 deletions

View File

@ -1,7 +1,8 @@
import collections
import copy
import enum
from functools import partial
from typing import Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, 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
@ -16,17 +17,22 @@ from .errors import ColorLibError
# TODO move type aliases to colorLib.types?
_LayerTuple = Tuple[str, Union[int, ot.Paint]]
_Kwargs = Mapping[str, Any]
_PaintInput = Union[int, _Kwargs, ot.Paint]
_LayerTuple = Tuple[str, _PaintInput]
_LayersList = Sequence[_LayerTuple]
_ColorGlyphsDict = Dict[str, _LayersList]
_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
_Number = Union[int, float]
_VariableScalar = Union[_Number, VariableValue, Tuple[_Number, int]]
_ColorTuple = Tuple[int, VariableValue]
_ColorStopTuple = Tuple[_VariableScalar, Union[int, _ColorTuple, ot.Color]]
_ScalarInput = Union[_Number, VariableValue, Tuple[_Number, int]]
_ColorInput = Union[int, _Kwargs, ot.Color]
_ColorStopTuple = Tuple[_ScalarInput, _ColorInput]
_ColorStopsList = Sequence[Union[_ColorStopTuple, ot.ColorStop]]
_PointTuple = Tuple[_VariableScalar, _VariableScalar]
_AffineTuple = Tuple[_VariableScalar, _VariableScalar, _VariableScalar, _VariableScalar]
_ColorLineInput = Union[_Kwargs, ot.ColorLine]
_PointTuple = Tuple[_ScalarInput, _ScalarInput]
_PointInput = Union[_PointTuple, ot.Point]
_AffineTuple = Tuple[_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput]
_AffineInput = Union[_AffineTuple, ot.Affine2x2]
def populateCOLRv0(
@ -43,9 +49,12 @@ def populateCOLRv0(
glyphMap: a map from glyph names to glyph indices, as returned from
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
"""
colorGlyphItems = colorGlyphsV0.items()
if glyphMap:
colorGlyphItems = sorted(colorGlyphItems, key=lambda item: glyphMap[item[0]])
if glyphMap is not None:
colorGlyphItems = sorted(
colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
)
else:
colorGlyphItems = colorGlyphsV0.items()
baseGlyphRecords = []
layerRecords = []
for baseGlyph, layers in colorGlyphItems:
@ -301,7 +310,7 @@ def _splitSolidAndGradientGlyphs(
_DEFAULT_TRANSPARENCY = VariableFloat(0.0)
def _to_variable_value(value: _VariableScalar, cls=VariableValue) -> VariableValue:
def _to_variable_value(value: _ScalarInput, cls=VariableValue) -> VariableValue:
if isinstance(value, cls):
return value
try:
@ -317,7 +326,7 @@ _to_variable_int = partial(_to_variable_value, cls=VariableInt)
def buildColor(
paletteIndex: int, transparency: _VariableScalar = _DEFAULT_TRANSPARENCY
paletteIndex: int, transparency: _ScalarInput = _DEFAULT_TRANSPARENCY
) -> ot.Color:
self = ot.Color()
self.PaletteIndex = int(paletteIndex)
@ -326,7 +335,7 @@ def buildColor(
def buildSolidColorPaint(
paletteIndex: int, transparency: _VariableScalar = _DEFAULT_TRANSPARENCY
paletteIndex: int, transparency: _ScalarInput = _DEFAULT_TRANSPARENCY
) -> ot.Paint:
self = ot.Paint()
self.Format = 1
@ -334,37 +343,46 @@ def buildSolidColorPaint(
return self
def buildColorStop(
offset: _VariableScalar, color: Union[int, _ColorTuple, ot.Color]
) -> ot.ColorStop:
def buildColorStop(offset: _ScalarInput, color: _ColorInput) -> ot.ColorStop:
self = ot.ColorStop()
self.StopOffset = _to_variable_float(offset)
if isinstance(color, int):
color = buildColor(paletteIndex=color)
elif not isinstance(color, ot.Color):
color = buildColor(*color)
color = buildColor(**color)
self.Color = color
return self
def _to_extend_mode(v):
if isinstance(v, ExtendMode):
return v
elif isinstance(v, str):
try:
return getattr(ExtendMode, v.upper())
except AttributeError:
raise ValueError(f"{v!r} is not a valid ExtendMode")
return ExtendMode(v)
def buildColorLine(
colorStops: _ColorStopsList, extend: ExtendMode = ExtendMode.PAD
stops: _ColorStopsList, extend: ExtendMode = ExtendMode.PAD
) -> ot.ColorLine:
self = ot.ColorLine()
self.Extend = ExtendMode(extend)
self.StopCount = len(colorStops)
self.Extend = _to_extend_mode(extend)
self.StopCount = len(stops)
self.ColorStop = [
stop
if isinstance(stop, ot.ColorStop)
else buildColorStop(offset=stop[0], color=stop[1])
for stop in colorStops
for stop in stops
]
return self
def buildPoint(x: _VariableScalar, y: _VariableScalar) -> ot.Point:
def buildPoint(x: _ScalarInput, y: _ScalarInput) -> ot.Point:
self = ot.Point()
# positions are encoded as Int16 so round to int
self.x = _to_variable_int(x)
@ -372,7 +390,7 @@ def buildPoint(x: _VariableScalar, y: _VariableScalar) -> ot.Point:
return self
def _to_variable_point(pt: Union[_PointTuple, ot.Point]) -> ot.Point:
def _to_variable_point(pt: _PointInput) -> ot.Point:
if isinstance(pt, ot.Point):
return pt
if isinstance(pt, tuple):
@ -380,18 +398,23 @@ def _to_variable_point(pt: Union[_PointTuple, ot.Point]) -> ot.Point:
raise TypeError(pt)
def _to_color_line(obj):
if isinstance(obj, ot.ColorLine):
return obj
elif isinstance(obj, collections.abc.Mapping):
return buildColorLine(**obj)
raise TypeError(obj)
def buildLinearGradientPaint(
colorLine: Union[_ColorStopsList, ot.ColorLine],
p0: Union[_PointTuple, ot.Point],
p1: Union[_PointTuple, ot.Point],
p2: Optional[Union[_PointTuple, ot.Point]] = None,
colorLine: _ColorLineInput,
p0: _PointInput,
p1: _PointInput,
p2: Optional[_PointInput] = None,
) -> ot.Paint:
self = ot.Paint()
self.Format = 2
if not isinstance(colorLine, ot.ColorLine):
colorLine = buildColorLine(colorStops=colorLine)
self.ColorLine = colorLine
self.ColorLine = _to_color_line(colorLine)
if p2 is None:
p2 = copy.copy(p1)
@ -402,7 +425,7 @@ def buildLinearGradientPaint(
def buildAffine2x2(
xx: _VariableScalar, xy: _VariableScalar, yx: _VariableScalar, yy: _VariableScalar
xx: _ScalarInput, xy: _ScalarInput, yx: _ScalarInput, yy: _ScalarInput
) -> ot.Affine2x2:
self = ot.Affine2x2()
locs = locals()
@ -413,20 +436,17 @@ def buildAffine2x2(
def buildRadialGradientPaint(
colorLine: Union[_ColorStopsList, ot.ColorLine],
c0: Union[_PointTuple, ot.Point],
c1: Union[_PointTuple, ot.Point],
r0: _VariableScalar,
r1: _VariableScalar,
affine: Optional[Union[_AffineTuple, ot.Affine2x2]] = None,
colorLine: _ColorLineInput,
c0: _PointInput,
c1: _PointInput,
r0: _ScalarInput,
r1: _ScalarInput,
affine: Optional[_AffineInput] = None,
) -> ot.Paint:
self = ot.Paint()
self.Format = 3
if not isinstance(colorLine, ot.ColorLine):
colorLine = buildColorLine(colorStops=colorLine)
self.ColorLine = colorLine
self.ColorLine = _to_color_line(colorLine)
for i, pt in [(0, c0), (1, c1)]:
setattr(self, f"c{i}", _to_variable_point(pt))
@ -442,15 +462,21 @@ def buildRadialGradientPaint(
return self
def buildLayerV1Record(
layerGlyph: str, paint: Union[int, ot.Paint]
) -> ot.LayerV1Record:
def _to_ot_paint(paint: _PaintInput) -> ot.Paint:
if isinstance(paint, ot.Paint):
return paint
elif isinstance(paint, int):
paletteIndex = paint
return buildSolidColorPaint(paletteIndex)
elif isinstance(paint, collections.abc.Mapping):
return buildPaint(**paint)
raise TypeError(f"expected int, Mapping or ot.Paint, found {type(paint.__name__)}")
def buildLayerV1Record(layerGlyph: str, paint: _PaintInput) -> ot.LayerV1Record:
self = ot.LayerV1Record()
self.LayerGlyph = layerGlyph
if isinstance(paint, int):
paletteIndex = paint
paint = buildSolidColorPaint(paletteIndex)
self.Paint = paint
self.Paint = _to_ot_paint(paint)
return self
@ -486,9 +512,12 @@ def buildBaseGlyphV1Array(
colorGlyphs: Union[_ColorGlyphsDict, Dict[str, ot.LayerV1Array]],
glyphMap: Optional[Mapping[str, int]] = None,
) -> ot.BaseGlyphV1Array:
colorGlyphItems = colorGlyphs.items()
if glyphMap:
colorGlyphItems = sorted(colorGlyphItems, key=lambda item: glyphMap[item[0]])
if glyphMap is not None:
colorGlyphItems = sorted(
colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
)
else:
colorGlyphItems = colorGlyphs.items()
records = [
buildBaseGlyphV1Record(baseGlyph, layers)
for baseGlyph, layers in colorGlyphItems
@ -497,3 +526,17 @@ def buildBaseGlyphV1Array(
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

@ -258,6 +258,9 @@ def test_buildColorLine():
(cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop
] == stops
cline = builder.buildColorLine(stops, extend="pad")
assert cline.Extend == builder.ExtendMode.PAD
cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REPEAT)
assert cline.Extend == builder.ExtendMode.REPEAT
@ -271,10 +274,19 @@ def test_buildColorLine():
(cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop
] == stops
stops = [((0.0, 1), (0, (0.5, 2))), ((1.0, 3), (1, (0.3, 4)))]
stops = [
((0.0, 1), {"paletteIndex": 0, "transparency": (0.5, 2)}),
((1.0, 3), {"paletteIndex": 1, "transparency": (0.3, 4)}),
]
cline = builder.buildColorLine(stops)
assert [
(tuple(cs.StopOffset), (cs.Color.PaletteIndex, tuple(cs.Color.Transparency)))
(
cs.StopOffset,
{
"paletteIndex": cs.Color.PaletteIndex,
"transparency": cs.Color.Transparency,
},
)
for cs in cline.ColorStop
] == stops
@ -327,7 +339,7 @@ def test_buildLinearGradientPaint():
assert gradient.p2 == gradient.p1
assert gradient.p2 is not gradient.p1
gradient = builder.buildLinearGradientPaint(color_stops, p0, p1)
gradient = builder.buildLinearGradientPaint({"stops": color_stops}, p0, p1)
assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
assert gradient.ColorLine.ColorStop == color_stops
@ -357,18 +369,18 @@ def test_buildRadialGradientPaint():
assert gradient.r1 == r1
assert gradient.Affine is None
gradient = builder.buildRadialGradientPaint(color_stops, c0, c1, r0, r1)
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_stops, c0, c1, r0, r1, affine=matrix
color_line, c0, c1, r0, r1, affine=matrix
)
assert gradient.Affine == matrix
gradient = builder.buildRadialGradientPaint(
color_stops, c0, c1, r0, r1, affine=(2.0, 0.0, 0.0, 2.0)
color_line, c0, c1, r0, r1, affine=(2.0, 0.0, 0.0, 2.0)
)
assert gradient.Affine == matrix
@ -386,7 +398,9 @@ def test_buildLayerV1Record():
layer = builder.buildLayerV1Record(
"a",
builder.buildLinearGradientPaint([(0.0, 3), (1.0, 4)], (100, 200), (150, 250)),
builder.buildLinearGradientPaint(
{"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250)
),
)
assert layer.Paint.Format == 2
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
@ -401,7 +415,17 @@ def test_buildLayerV1Record():
layer = builder.buildLayerV1Record(
"a",
builder.buildRadialGradientPaint(
[(0.0, 5), (0.5, (6, 0.8)), (1.0, 7)], (50, 50), (75, 75), 30, 10
{
"stops": [
(0.0, 5),
(0.5, {"paletteIndex": 6, "transparency": 0.8}),
(1.0, 7),
]
},
(50, 50),
(75, 75),
30,
10,
),
)
assert layer.Paint.Format == 3
@ -420,25 +444,71 @@ 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"
assert layer.Paint.Format == 1
assert layer.Paint.Color.PaletteIndex == 0
layer = builder.buildLayerV1Record(
"a",
{
"format": 2,
"colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
"p0": (0, 0),
"p1": (10, 10),
},
)
assert layer.Paint.Format == 2
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
layer = builder.buildLayerV1Record(
"a",
{
"format": 3,
"colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
"c0": (0, 0),
"c1": (10, 10),
"r0": 4,
"r1": 0,
},
)
assert layer.Paint.Format == 3
assert layer.Paint.r0.value == 4
def test_buildLayerV1Array():
layers = [
("a", 1),
("b", builder.buildSolidColorPaint(2, 0.5)),
("b", {"format": 1, "paletteIndex": 2, "transparency": 0.5}),
(
"c",
builder.buildLinearGradientPaint(
[(0.0, 3), (1.0, 4)], (100, 200), (150, 250)
),
{
"format": 2,
"colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "repeat"},
"p0": (100, 200),
"p1": (150, 250),
},
),
(
"d",
builder.buildRadialGradientPaint(
[(0.0, 5), (0.5, (6, 0.8)), (1.0, 7)], (50, 50), (75, 75), 30, 10
),
{
"format": 3,
"colorLine": {
"stops": [
(0.0, 5),
(0.5, {"paletteIndex": 6, "transparency": 0.8}),
(1.0, 7),
]
},
"c0": (50, 50),
"c1": (75, 75),
"r0": 30,
"r1": 10,
},
),
builder.buildLayerV1Record("e", builder.buildSolidColorPaint(8)),
]
layersArray = builder.buildLayerV1Array(layers)
assert layersArray.LayerCount == len(layersArray.LayerV1Record)
@ -460,12 +530,17 @@ def test_buildBaseGlyphV1Array():
colorGlyphs = {
"a": [("b", 0), ("c", 1)],
"d": [
("e", builder.buildSolidColorPaint(2, transparency=0.8)),
("e", {"format": 1, "paletteIndex": 2, "transparency": 0.8}),
(
"f",
builder.buildRadialGradientPaint(
[(0.0, 3), (1.0, 4)], (0, 0), (0, 0), 10, 0
),
{
"format": 3,
"colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "reflect"},
"c0": (0, 0),
"c1": (0, 0),
"r0": 10,
"r1": 0,
},
),
],
"g": builder.buildLayerV1Array([("h", 5)]),