From fdf6a5c1fc7646b193903450924709de965a5085 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 9 Oct 2020 18:16:51 +0100 Subject: [PATCH] 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 --- Lib/fontTools/colorLib/builder.py | 211 +++++++++++++++++------------- Tests/colorLib/builder_test.py | 72 ++++------ 2 files changed, 148 insertions(+), 135 deletions(-) diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py index 48ade4482..dd38d2956 100644 --- a/Lib/fontTools/colorLib/builder.py +++ b/Lib/fontTools/colorLib/builder.py @@ -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) diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py index e7ea6b614..dd7ccb9c9 100644 --- a/Tests/colorLib/builder_test.py +++ b/Tests/colorLib/builder_test.py @@ -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" )