From cf4a4087be7628ed433778b94897d13374695df6 Mon Sep 17 00:00:00 2001 From: rsheeter Date: Thu, 4 Feb 2021 21:57:20 -0800 Subject: [PATCH] Generic build fns --- Lib/fontTools/colorLib/builder.py | 467 ++++--------- Lib/fontTools/colorLib/table_builder.py | 194 ++++++ Lib/fontTools/ttLib/tables/otData.py | 9 + Tests/colorLib/builder_test.py | 881 +++++++++++++++--------- Tests/colorLib/unbuilder_test.py | 277 ++++---- 5 files changed, 1048 insertions(+), 780 deletions(-) create mode 100644 Lib/fontTools/colorLib/table_builder.py diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py index 840f8bca6..586935c3e 100644 --- a/Lib/fontTools/colorLib/builder.py +++ b/Lib/fontTools/colorLib/builder.py @@ -25,7 +25,6 @@ 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, @@ -36,6 +35,11 @@ from fontTools.ttLib.tables.otTables import ( ) from .errors import ColorLibError from .geometry import round_start_circle_stable_containment +from .table_builder import ( + convertTupleClass, + BuildCallback, + TableBuilder, +) # TODO move type aliases to colorLib.types? @@ -45,21 +49,63 @@ _PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]] _PaintInputList = Sequence[_PaintInput] _ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]] _ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]] -_Number = Union[int, float] -_ScalarInput = Union[_Number, VariableValue, Tuple[_Number, int]] -_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, _ScalarInput, _ScalarInput -] -_AffineInput = Union[_AffineTuple, ot.Affine2x3] + MAX_PAINT_COLR_LAYER_COUNT = 255 +_DEFAULT_ALPHA = VariableFloat(1.0) + + +def _beforeBuildPaintRadialGradient(paint, source): + # normalize input types (which may or may not specify a varIdx) + x0 = convertTupleClass(VariableFloat, source.get("x0", 0.0)) + y0 = convertTupleClass(VariableFloat, source.get("y0", 0.0)) + r0 = convertTupleClass(VariableFloat, source.get("r0", 0.0)) + x1 = convertTupleClass(VariableFloat, source.get("x1", 0.0)) + y1 = convertTupleClass(VariableFloat, source.get("y1", 0.0)) + r1 = convertTupleClass(VariableFloat, source.get("r1", 0.0)) + + # TODO apparently no builder_test confirms this works (?) + + # avoid abrupt change after rounding when c0 is near c1's perimeter + c = round_start_circle_stable_containment( + (x0.value, y0.value), r0.value, (x1.value, y1.value), r1.value + ) + x0, y0 = x0._replace(value=c.centre[0]), y0._replace(value=c.centre[1]) + r0 = r0._replace(value=c.radius) + + # update source to ensure paint is built with corrected values + source["x0"] = x0 + source["y0"] = y0 + source["r0"] = r0 + source["x1"] = x1 + source["y1"] = y1 + source["r1"] = r1 + + return paint, source + + +def _defaultColorIndex(): + colorIndex = ot.ColorIndex() + colorIndex.Alpha = _DEFAULT_ALPHA + return colorIndex + + +def _defaultColorLine(): + colorLine = ot.ColorLine() + colorLine.Extend = ExtendMode.PAD + return colorLine + + +def _buildPaintCallbacks(): + return { + ( + BuildCallback.BEFORE_BUILD, + ot.Paint, + ot.PaintFormat.PaintRadialGradient, + ): _beforeBuildPaintRadialGradient, + (BuildCallback.CREATE_DEFAULT, ot.ColorIndex): _defaultColorIndex, + (BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine, + } def populateCOLRv0( @@ -112,7 +158,6 @@ def buildCOLR( varStore: Optional[ot.VarStore] = None, ) -> C_O_L_R_.table_C_O_L_R_: """Build COLR table from color layers mapping. - Args: colorGlyphs: map of base glyph name to, either list of (layer glyph name, color palette index) tuples for COLRv0; or a single Paint (dict) or @@ -124,7 +169,6 @@ def buildCOLR( 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. - Return: A new COLR table. """ @@ -295,8 +339,6 @@ def buildCPAL( # COLR v1 tables # See draft proposal at: https://github.com/googlefonts/colr-gradients-spec -_DEFAULT_ALPHA = VariableFloat(1.0) - def _is_colrv0_layer(layer: Any) -> bool: # Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which @@ -328,116 +370,6 @@ def _split_color_glyphs_by_version( return colorGlyphsV0, colorGlyphsV1 -def _to_variable_value( - value: _ScalarInput, - cls: Type[VariableValue] = VariableFloat, - minValue: Optional[_Number] = None, - maxValue: Optional[_Number] = None, -) -> VariableValue: - if not isinstance(value, cls): - try: - it = iter(value) - except TypeError: # not iterable - value = cls(value) - else: - value = cls._make(it) - if minValue is not None and value.value < minValue: - raise OverflowError(f"{cls.__name__}: {value.value} < {minValue}") - if maxValue is not None and value.value > maxValue: - raise OverflowError(f"{cls.__name__}: {value.value} < {maxValue}") - return value - - -_to_variable_f16dot16_float = partial( - _to_variable_value, - cls=VariableFloat, - minValue=-(2 ** 15), - maxValue=fixedToFloat(2 ** 31 - 1, 16), -) -_to_variable_f2dot14_float = partial( - _to_variable_value, - cls=VariableFloat, - minValue=-2.0, - maxValue=fixedToFloat(2 ** 15 - 1, 14), -) -_to_variable_int16 = partial( - _to_variable_value, - cls=VariableInt, - minValue=-(2 ** 15), - maxValue=2 ** 15 - 1, -) -_to_variable_uint16 = partial( - _to_variable_value, - cls=VariableInt, - minValue=0, - maxValue=2 ** 16, -) - - -def buildColorIndex( - paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA -) -> ot.ColorIndex: - self = ot.ColorIndex() - self.PaletteIndex = int(paletteIndex) - self.Alpha = _to_variable_f2dot14_float(alpha) - return self - - -def buildColorStop( - offset: _ScalarInput, - paletteIndex: int, - alpha: _ScalarInput = _DEFAULT_ALPHA, -) -> ot.ColorStop: - self = ot.ColorStop() - self.StopOffset = _to_variable_f2dot14_float(offset) - self.Color = buildColorIndex(paletteIndex, alpha) - return self - - -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(enumClass, v.upper()) - except AttributeError: - 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( - stops: _ColorStopsList, extend: _ExtendInput = ExtendMode.PAD -) -> ot.ColorLine: - self = ot.ColorLine() - self.Extend = _to_extend_mode(extend) - self.StopCount = len(stops) - self.ColorStop = [ - stop - if isinstance(stop, ot.ColorStop) - else buildColorStop(**stop) - if isinstance(stop, collections.abc.Mapping) - else buildColorStop(*stop) - for stop in stops - ] - return self - - -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 _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): @@ -463,6 +395,17 @@ class LayerV1ListBuilder: self.tuples = {} self.keepAlive = [] + # We need to intercept construction of PaintColrLayers + callbacks = _buildPaintCallbacks() + callbacks[ + ( + BuildCallback.BEFORE_BUILD, + ot.Paint, + ot.PaintFormat.PaintColrLayers, + ) + ] = self._beforeBuildPaintColrLayers + self.tableBuilder = TableBuilder(callbacks) + def _paint_tuple(self, paint: ot.Paint): # start simple, who even cares about cyclic graphs or interesting field types def _tuple_safe(value): @@ -488,186 +431,37 @@ class LayerV1ListBuilder: def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]: return tuple(self._paint_tuple(p) for p in paints) - def buildPaintSolid( - self, paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA - ) -> ot.Paint: - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintSolid) - ot_paint.Color = buildColorIndex(paletteIndex, alpha) - return ot_paint + # COLR layers is unusual in that it modifies shared state + # so we need a callback into an object + def _beforeBuildPaintColrLayers(self, dest, source): + paint = ot.Paint() + paint.Format = int(ot.PaintFormat.PaintColrLayers) + self.slices.append(paint) - def buildPaintLinearGradient( - self, - colorLine: _ColorLineInput, - p0: _PointTuple, - p1: _PointTuple, - p2: Optional[_PointTuple] = None, - ) -> ot.Paint: - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintLinearGradient) - ot_paint.ColorLine = _to_color_line(colorLine) + # Sketchy gymnastics: a sequence input will have dropped it's layers + # into NumLayers; get it back + if isinstance(source.get("NumLayers", None), collections.abc.Sequence): + layers = source["NumLayers"] + else: + layers = source["Layers"] - if p2 is None: - p2 = copy.copy(p1) - for i, (x, y) in enumerate((p0, p1, p2)): - setattr(ot_paint, f"x{i}", _to_variable_int16(x)) - setattr(ot_paint, f"y{i}", _to_variable_int16(y)) - - return ot_paint - - def buildPaintRadialGradient( - self, - colorLine: _ColorLineInput, - c0: _PointTuple, - c1: _PointTuple, - r0: _ScalarInput, - r1: _ScalarInput, - ) -> ot.Paint: - - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintRadialGradient) - ot_paint.ColorLine = _to_color_line(colorLine) - - # normalize input types (which may or may not specify a varIdx) - x0, y0 = _to_variable_value(c0[0]), _to_variable_value(c0[1]) - r0 = _to_variable_value(r0) - x1, y1 = _to_variable_value(c1[0]), _to_variable_value(c1[1]) - r1 = _to_variable_value(r1) - - # avoid abrupt change after rounding when c0 is near c1's perimeter - c = round_start_circle_stable_containment( - (x0.value, y0.value), r0.value, (x1.value, y1.value), r1.value - ) - x0, y0 = x0._replace(value=c.centre[0]), y0._replace(value=c.centre[1]) - r0 = r0._replace(value=c.radius) - - for i, (x, y, r) in enumerate(((x0, y0, r0), (x1, y1, r1))): - # rounding happens here as floats are converted to integers - setattr(ot_paint, f"x{i}", _to_variable_int16(x)) - setattr(ot_paint, f"y{i}", _to_variable_int16(y)) - setattr(ot_paint, f"r{i}", _to_variable_uint16(r)) - - return ot_paint - - def buildPaintSweepGradient( - self, - colorLine: _ColorLineInput, - centerX: _ScalarInput, - centerY: _ScalarInput, - startAngle: _ScalarInput, - endAngle: _ScalarInput, - ) -> ot.Paint: - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintSweepGradient) - ot_paint.ColorLine = _to_color_line(colorLine) - ot_paint.centerX = _to_variable_int16(centerX) - ot_paint.centerY = _to_variable_int16(centerY) - ot_paint.startAngle = _to_variable_f16dot16_float(startAngle) - ot_paint.endAngle = _to_variable_f16dot16_float(endAngle) - return ot_paint - - def buildPaintGlyph(self, glyph: str, paint: _PaintInput) -> ot.Paint: - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintGlyph) - ot_paint.Glyph = glyph - ot_paint.Paint = self.buildPaint(paint) - return ot_paint - - def buildPaintColrGlyph(self, glyph: str) -> ot.Paint: - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintColrGlyph) - ot_paint.Glyph = glyph - return ot_paint - - def buildPaintTransform( - self, transform: _AffineInput, paint: _PaintInput - ) -> ot.Paint: - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintTransform) - if not isinstance(transform, ot.Affine2x3): - transform = buildAffine2x3(transform) - ot_paint.Transform = transform - ot_paint.Paint = self.buildPaint(paint) - return ot_paint - - def buildPaintTranslate( - self, paint: _PaintInput, dx: _ScalarInput, dy: _ScalarInput - ): - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintTranslate) - ot_paint.Paint = self.buildPaint(paint) - ot_paint.dx = _to_variable_f16dot16_float(dx) - ot_paint.dy = _to_variable_f16dot16_float(dy) - return ot_paint - - def buildPaintRotate( - self, - paint: _PaintInput, - angle: _ScalarInput, - centerX: _ScalarInput, - centerY: _ScalarInput, - ) -> ot.Paint: - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintRotate) - ot_paint.Paint = self.buildPaint(paint) - ot_paint.angle = _to_variable_f16dot16_float(angle) - ot_paint.centerX = _to_variable_f16dot16_float(centerX) - ot_paint.centerY = _to_variable_f16dot16_float(centerY) - return ot_paint - - def buildPaintSkew( - self, - paint: _PaintInput, - xSkewAngle: _ScalarInput, - ySkewAngle: _ScalarInput, - centerX: _ScalarInput, - centerY: _ScalarInput, - ) -> ot.Paint: - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintSkew) - ot_paint.Paint = self.buildPaint(paint) - ot_paint.xSkewAngle = _to_variable_f16dot16_float(xSkewAngle) - ot_paint.ySkewAngle = _to_variable_f16dot16_float(ySkewAngle) - ot_paint.centerX = _to_variable_f16dot16_float(centerX) - ot_paint.centerY = _to_variable_f16dot16_float(centerY) - return ot_paint - - def buildPaintComposite( - self, - mode: _CompositeInput, - source: _PaintInput, - backdrop: _PaintInput, - ): - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintComposite) - ot_paint.SourcePaint = self.buildPaint(source) - ot_paint.CompositeMode = _to_composite_mode(mode) - ot_paint.BackdropPaint = self.buildPaint(backdrop) - return ot_paint - - def buildColrLayers(self, paints: List[_PaintInput]) -> ot.Paint: - ot_paint = ot.Paint() - ot_paint.Format = int(ot.PaintFormat.PaintColrLayers) - self.slices.append(ot_paint) - - paints = [ - self.buildPaint(p) - for p in _build_n_ary_tree(paints, n=MAX_PAINT_COLR_LAYER_COUNT) - ] + # Convert maps seqs or whatever into typed objects + layers = [self.buildPaint(l) for l in layers] # Look for reuse, with preference to longer sequences + # This may make the layer list smaller found_reuse = True while found_reuse: found_reuse = False ranges = sorted( - _reuse_ranges(len(paints)), + _reuse_ranges(len(layers)), key=lambda t: (t[1] - t[0], t[1], t[0]), reverse=True, ) for lbound, ubound in ranges: reuse_lbound = self.reusePool.get( - self._as_tuple(paints[lbound:ubound]), -1 + self._as_tuple(layers[lbound:ubound]), -1 ) if reuse_lbound == -1: continue @@ -675,47 +469,43 @@ class LayerV1ListBuilder: new_slice.Format = int(ot.PaintFormat.PaintColrLayers) new_slice.NumLayers = ubound - lbound new_slice.FirstLayerIndex = reuse_lbound - paints = paints[:lbound] + [new_slice] + paints[ubound:] + layers = layers[:lbound] + [new_slice] + layers[ubound:] found_reuse = True break - ot_paint.NumLayers = len(paints) - ot_paint.FirstLayerIndex = len(self.layers) - self.layers.extend(paints) + # The layer list is now final; if it's too big we need to tree it + layers = _build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT) + + # We now have a tree of sequences with Paint leaves. + # Convert the sequences into PaintColrLayers. + def listToColrLayers(layer): + if isinstance(layer, collections.abc.Sequence): + return self.buildPaint( + { + "Format": ot.PaintFormat.PaintColrLayers, + "Layers": [listToColrLayers(l) for l in layer], + } + ) + return layer + + layers = [listToColrLayers(l) for l in layers] + + paint.NumLayers = len(layers) + paint.FirstLayerIndex = len(self.layers) + self.layers.extend(layers) # Register our parts for reuse - for lbound, ubound in _reuse_ranges(len(paints)): - self.reusePool[self._as_tuple(paints[lbound:ubound])] = ( - lbound + ot_paint.FirstLayerIndex + # TODO what if we made ourselves a lovely little tree + for lbound, ubound in _reuse_ranges(len(layers)): + self.reusePool[self._as_tuple(layers[lbound:ubound])] = ( + lbound + paint.FirstLayerIndex ) - return ot_paint + # we've fully built dest; empty source prevents generalized build from kicking in + return paint, {} def buildPaint(self, paint: _PaintInput) -> ot.Paint: - if isinstance(paint, ot.Paint): - return paint - elif isinstance(paint, int): - paletteIndex = paint - return self.buildPaintSolid(paletteIndex) - elif isinstance(paint, tuple): - layerGlyph, paint = paint - return self.buildPaintGlyph(layerGlyph, paint) - elif isinstance(paint, list): - # implicit PaintColrLayers for a list of > 1 - if len(paint) == 0: - raise ValueError("An empty list is hard to paint") - elif len(paint) == 1: - return self.buildPaint(paint[0]) - else: - return self.buildColrLayers(paint) - elif isinstance(paint, collections.abc.Mapping): - kwargs = dict(paint) - fmt = kwargs.pop("format") - try: - return LayerV1ListBuilder._buildFunctions[fmt](self, **kwargs) - except KeyError: - raise NotImplementedError(fmt) - raise TypeError(f"Not sure what to do with {type(paint).__name__}: {paint!r}") + return self.tableBuilder.build(ot.Paint, paint) def build(self) -> ot.LayerV1List: layers = ot.LayerV1List() @@ -724,31 +514,6 @@ class LayerV1ListBuilder: return layers -LayerV1ListBuilder._buildFunctions = { - pf.value: getattr(LayerV1ListBuilder, "build" + pf.name) - for pf in ot.PaintFormat - if pf != ot.PaintFormat.PaintColrLayers -} - - -def buildAffine2x3(transform: _AffineTuple) -> ot.Affine2x3: - if len(transform) != 6: - raise ValueError(f"Expected 6-tuple of floats, found: {transform!r}") - self = ot.Affine2x3() - # COLRv1 Affine2x3 uses the same column-major order to serialize a 2D - # Affine Transformation as the one used by fontTools.misc.transform. - # However, for historical reasons, the labels 'xy' and 'yx' are swapped. - # Their fundamental meaning is the same though. - # COLRv1 Affine2x3 follows the names found in FreeType and Cairo. - # In all case, the second element in the 6-tuple correspond to the - # y-part of the x basis vector, and the third to the x-part of the y - # basis vector. - # See https://github.com/googlefonts/colr-gradients-spec/pull/85 - for i, attr in enumerate(("xx", "yx", "xy", "yy", "dx", "dy")): - setattr(self, attr, _to_variable_f16dot16_float(transform[i])) - return self - - def buildBaseGlyphV1Record( baseGlyph: str, layerBuilder: LayerV1ListBuilder, paint: _PaintInput ) -> ot.BaseGlyphV1List: diff --git a/Lib/fontTools/colorLib/table_builder.py b/Lib/fontTools/colorLib/table_builder.py new file mode 100644 index 000000000..b80229754 --- /dev/null +++ b/Lib/fontTools/colorLib/table_builder.py @@ -0,0 +1,194 @@ +""" +colorLib.table_builder: Generic helper for filling in BaseTable derivatives from tuples and maps and such. + +""" + +import collections +import enum +from fontTools.ttLib.tables.otBase import ( + BaseTable, + FormatSwitchingBaseTable, + UInt8FormatSwitchingBaseTable, +) +from fontTools.ttLib.tables.otConverters import ( + ComputedInt, + GlyphID, + Struct, + Short, + UInt8, + UShort, + VarInt16, + VarUInt16, +) + + +def _to_glyph_id(value): + assert isinstance(value, str), "Expected a glyph name" + return value + + +_CONVERTER_OVERRIDES = { + Short: int, + UShort: int, + GlyphID: _to_glyph_id, +} + + +class BuildCallback(enum.Enum): + """Keyed on (BEFORE_BUILD, class[, Format if available]). + Receives (dest, source). + Should return (dest, source), which can be new objects. + """ + + BEFORE_BUILD = enum.auto() + + """Keyed on (AFTER_BUILD, class[, Format if available]). + Receives (dest). + Should return dest, which can be a new object. + """ + AFTER_BUILD = enum.auto() + + """Keyed on (CREATE_DEFAULT, class). + Receives no arguments. + Should return a new instance of class. + """ + CREATE_DEFAULT = enum.auto() + + +def _assignable(convertersByName): + return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)} + + +def convertTupleClass(tupleClass, value): + if isinstance(value, tupleClass): + return value + if isinstance(value, tuple): + return tupleClass(*value) + return tupleClass(value) + + +def _isNonStrSequence(value): + return isinstance(value, collections.abc.Sequence) and not isinstance(value, str) + + +def _set_format(dest, source): + if _isNonStrSequence(source): + assert len(source) > 0, f"{type(dest)} needs at least format from {source}" + dest.Format = source[0] + source = source[1:] + elif isinstance(source, collections.abc.Mapping): + assert "Format" in source, f"{type(dest)} needs at least Format from {source}" + dest.Format = source["Format"] + else: + raise ValueError(f"Not sure how to populate {type(dest)} from {source}") + + assert isinstance( + dest.Format, collections.abc.Hashable + ), f"{type(dest)} Format is not hashable: {dest.Format}" + assert ( + dest.Format in dest.convertersByName + ), f"{dest.Format} invalid Format of {cls}" + + return source + + +class TableBuilder: + """ + Helps to populate things derived from BaseTable from maps, tuples, etc. + + A table of lifecycle callbacks may be provided to add logic beyond what is possible + based on otData info for the target class. See BuildCallbacks. + """ + + def __init__(self, callbackTable=None): + if callbackTable is None: + callbackTable = {} + self._callbackTable = callbackTable + + def _convert(self, dest, field, converter, value): + converter = _CONVERTER_OVERRIDES.get(type(converter), converter) + + enumClass = getattr(converter, "enumClass", None) + tupleClass = getattr(converter, "tupleClass", None) + if tupleClass: + value = convertTupleClass(tupleClass, value) + + elif enumClass: + if isinstance(value, enumClass): + pass + elif isinstance(value, str): + try: + value = getattr(enumClass, value.upper()) + except AttributeError: + raise ValueError(f"{value} is not a valid {enumClass}") + else: + value = enumClass(value) + + elif isinstance(converter, Struct): + if converter.repeat: + if _isNonStrSequence(value): + value = [self.build(converter.tableClass, v) for v in value] + else: + value = [self.build(converter.tableClass, value)] + setattr(dest, converter.repeat, len(value)) + else: + value = self.build(converter.tableClass, value) + elif callable(converter): + value = converter(value) + + setattr(dest, field, value) + + def build(self, cls, source): + assert issubclass(cls, BaseTable) + + if isinstance(source, cls): + return source + + callbackKey = (cls,) + dest = self._callbackTable.get( + (BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls() + )() + assert isinstance(dest, cls) + + convByName = _assignable(cls.convertersByName) + skippedFields = set() + + # For format switchers we need to resolve converters based on format + if issubclass(cls, FormatSwitchingBaseTable): + source = _set_format(dest, source) + + convByName = _assignable(convByName[dest.Format]) + skippedFields.add("Format") + callbackKey = (cls, dest.Format) + + # Convert sequence => mapping so before thunk only has to handle one format + if _isNonStrSequence(source): + # Sequence (typically list or tuple) assumed to match fields in declaration order + assert len(source) <= len( + convByName + ), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values" + source = dict(zip(convByName.keys(), source)) + + dest, source = self._callbackTable.get( + (BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s) + )(dest, source) + + if isinstance(source, collections.abc.Mapping): + for field, value in source.items(): + if field in skippedFields: + continue + converter = convByName.get(field, None) + if not converter: + raise ValueError( + f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}" + ) + self._convert(dest, field, converter, value) + else: + # let's try as a 1-tuple + dest = self.build(cls, (source,)) + + dest = self._callbackTable.get( + (BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d + )(dest) + + return dest diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 389ac5c42..a5c5ad600 100755 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -1588,6 +1588,15 @@ otData = [ ('LOffset', 'Paint', 'LayerCount', 0, 'Array of offsets to Paint tables, from the start of the LayerV1List table.'), ]), + # COLRv1 Affine2x3 uses the same column-major order to serialize a 2D + # Affine Transformation as the one used by fontTools.misc.transform. + # However, for historical reasons, the labels 'xy' and 'yx' are swapped. + # Their fundamental meaning is the same though. + # COLRv1 Affine2x3 follows the names found in FreeType and Cairo. + # In all case, the second element in the 6-tuple correspond to the + # y-part of the x basis vector, and the third to the x-part of the y + # basis vector. + # See https://github.com/googlefonts/colr-gradients-spec/pull/85 ('Affine2x3', [ ('VarFixed', 'xx', None, None, 'x-part of x basis vector'), ('VarFixed', 'yx', None, None, 'y-part of x basis vector'), diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py index a705b38f3..95962f637 100644 --- a/Tests/colorLib/builder_test.py +++ b/Tests/colorLib/builder_test.py @@ -3,11 +3,20 @@ from fontTools.ttLib.tables import otTables as ot from fontTools.colorLib import builder from fontTools.colorLib.geometry import round_start_circle_stable_containment, Circle from fontTools.colorLib.builder import LayerV1ListBuilder, _build_n_ary_tree +from fontTools.colorLib.table_builder import TableBuilder from fontTools.colorLib.errors import ColorLibError import pytest from typing import List +def _build(cls, source): + return LayerV1ListBuilder().tableBuilder.build(cls, source) + + +def _buildPaint(source): + return LayerV1ListBuilder().buildPaint(source) + + def test_buildCOLR_v0(): color_layer_lists = { "a": [("a.color0", 0), ("a.color1", 1)], @@ -223,105 +232,132 @@ def test_buildCPAL_invalid_color(): def test_buildColorIndex(): - c = builder.buildColorIndex(0) - assert c.PaletteIndex == 0 + c = _build(ot.ColorIndex, 1) + assert c.PaletteIndex == 1 assert c.Alpha.value == 1.0 assert c.Alpha.varIdx == 0 - c = builder.buildColorIndex(1, alpha=0.5) + +def test_buildColorIndex_Alpha(): + c = _build(ot.ColorIndex, (1, 0.5)) assert c.PaletteIndex == 1 assert c.Alpha.value == 0.5 assert c.Alpha.varIdx == 0 - c = builder.buildColorIndex(3, alpha=builder.VariableFloat(0.5, varIdx=2)) + +def test_buildColorIndex_Variable(): + c = _build(ot.ColorIndex, (3, builder.VariableFloat(0.5, varIdx=2))) assert c.PaletteIndex == 3 assert c.Alpha.value == 0.5 assert c.Alpha.varIdx == 2 def test_buildPaintSolid(): - p = LayerV1ListBuilder().buildPaintSolid(0) + p = _buildPaint((ot.PaintFormat.PaintSolid, 0)) assert p.Format == ot.PaintFormat.PaintSolid assert p.Color.PaletteIndex == 0 assert p.Color.Alpha.value == 1.0 assert p.Color.Alpha.varIdx == 0 - p = LayerV1ListBuilder().buildPaintSolid(1, alpha=0.5) + +def test_buildPaintSolid_Alpha(): + p = _buildPaint((ot.PaintFormat.PaintSolid, (1, 0.5))) assert p.Format == ot.PaintFormat.PaintSolid assert p.Color.PaletteIndex == 1 assert p.Color.Alpha.value == 0.5 assert p.Color.Alpha.varIdx == 0 - p = LayerV1ListBuilder().buildPaintSolid( - 3, alpha=builder.VariableFloat(0.5, varIdx=2) - ) + +def test_buildPaintSolid_Variable(): + p = _buildPaint((ot.PaintFormat.PaintSolid, (3, builder.VariableFloat(0.5, varIdx=2)))) assert p.Format == ot.PaintFormat.PaintSolid assert p.Color.PaletteIndex == 3 assert p.Color.Alpha.value == 0.5 assert p.Color.Alpha.varIdx == 2 -def test_buildColorStop(): - s = builder.buildColorStop(0.1, 2) +def test_buildColorStop_DefaultAlpha(): + s = _build(ot.ColorStop, (0.1, 2)) assert s.StopOffset == builder.VariableFloat(0.1) assert s.Color.PaletteIndex == 2 assert s.Color.Alpha == builder._DEFAULT_ALPHA - s = builder.buildColorStop(offset=0.2, paletteIndex=3, alpha=0.4) - assert s.StopOffset == builder.VariableFloat(0.2) - assert s.Color == builder.buildColorIndex(3, alpha=0.4) - s = builder.buildColorStop( - offset=builder.VariableFloat(0.0, varIdx=1), - paletteIndex=0, - alpha=builder.VariableFloat(0.3, varIdx=2), +def test_buildColorStop(): + s = _build( + ot.ColorStop, {"StopOffset": 0.2, "Color": {"PaletteIndex": 3, "Alpha": 0.4}} + ) + assert s.StopOffset == builder.VariableFloat(0.2) + assert s.Color == _build(ot.ColorIndex, (3, 0.4)) + + +def test_buildColorStop_Variable(): + s = _build( + ot.ColorStop, + { + "StopOffset": builder.VariableFloat(0.0, varIdx=1), + "Color": { + "PaletteIndex": 0, + "Alpha": builder.VariableFloat(0.3, varIdx=2), + }, + }, ) assert s.StopOffset == builder.VariableFloat(0.0, varIdx=1) assert s.Color.PaletteIndex == 0 assert s.Color.Alpha == builder.VariableFloat(0.3, varIdx=2) -def test_buildColorLine(): +def test_buildColorLine_StopList(): stops = [(0.0, 0), (0.5, 1), (1.0, 2)] - cline = builder.buildColorLine(stops) + cline = _build(ot.ColorLine, {"ColorStop": stops}) assert cline.Extend == builder.ExtendMode.PAD assert cline.StopCount == 3 assert [ (cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop ] == stops - cline = builder.buildColorLine(stops, extend="pad") + cline = _build(ot.ColorLine, {"Extend": "pad", "ColorStop": stops}) assert cline.Extend == builder.ExtendMode.PAD - cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REPEAT) + cline = _build( + ot.ColorLine, {"ColorStop": stops, "Extend": builder.ExtendMode.REPEAT} + ) assert cline.Extend == builder.ExtendMode.REPEAT - cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REFLECT) + cline = _build( + ot.ColorLine, {"ColorStop": stops, "Extend": builder.ExtendMode.REFLECT} + ) assert cline.Extend == builder.ExtendMode.REFLECT - cline = builder.buildColorLine([builder.buildColorStop(*s) for s in stops]) + cline = _build( + ot.ColorLine, {"ColorStop": [_build(ot.ColorStop, s) for s in stops]} + ) assert [ (cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop ] == stops + +def test_buildColorLine_StopMap_Variations(): stops = [ - {"offset": (0.0, 1), "paletteIndex": 0, "alpha": (0.5, 2)}, - {"offset": (1.0, 3), "paletteIndex": 1, "alpha": (0.3, 4)}, + {"StopOffset": (0.0, (1,)), "Color": {"PaletteIndex": 0, "Alpha": (0.5, 2)}}, + {"StopOffset": (1.0, (3,)), "Color": {"PaletteIndex": 1, "Alpha": (0.3, 4)}}, ] - cline = builder.buildColorLine(stops) + cline = _build(ot.ColorLine, {"ColorStop": stops}) assert [ { - "offset": cs.StopOffset, - "paletteIndex": cs.Color.PaletteIndex, - "alpha": cs.Color.Alpha, + "StopOffset": cs.StopOffset, + "Color": { + "PaletteIndex": cs.Color.PaletteIndex, + "Alpha": cs.Color.Alpha, + }, } for cs in cline.ColorStop ] == stops def test_buildAffine2x3(): - matrix = builder.buildAffine2x3((1.5, 0, 0.5, 2.0, 1.0, -3.0)) + matrix = _build(ot.Affine2x3, (1.5, 0, 0.5, 2.0, 1.0, -3.0)) assert matrix.xx == builder.VariableFloat(1.5) assert matrix.yx == builder.VariableFloat(0.0) assert matrix.xy == builder.VariableFloat(0.5) @@ -330,47 +366,55 @@ def test_buildAffine2x3(): assert matrix.dy == builder.VariableFloat(-3.0) -def test_buildPaintLinearGradient(): - layerBuilder = LayerV1ListBuilder() - color_stops = [ - builder.buildColorStop(0.0, 0), - builder.buildColorStop(0.5, 1), - builder.buildColorStop(1.0, 2, alpha=0.8), +def _sample_stops(): + return [ + _build(ot.ColorStop, (0.0, 0)), + _build(ot.ColorStop, (0.5, 1)), + _build(ot.ColorStop, (1.0, (2, 0.8))), ] - color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT) - p0 = (builder.VariableInt(100), builder.VariableInt(200)) - p1 = (builder.VariableInt(150), builder.VariableInt(250)) - gradient = layerBuilder.buildPaintLinearGradient(color_line, p0, p1) - assert gradient.Format == 3 - assert gradient.ColorLine == color_line - assert (gradient.x0, gradient.y0) == p0 - assert (gradient.x1, gradient.y1) == p1 - assert (gradient.x2, gradient.y2) == p1 - gradient = layerBuilder.buildPaintLinearGradient({"stops": color_stops}, p0, p1) +def test_buildPaintLinearGradient(): + color_stops = _sample_stops() + x0, y0, x1, y1, x2, y2 = tuple(builder.VariableInt(v) for v in (1, 2, 3, 4, 5, 6)) + gradient = _buildPaint( + { + "Format": ot.PaintFormat.PaintLinearGradient, + "ColorLine": {"ColorStop": color_stops}, + "x0": x0, + "y0": y0, + "x1": x1, + "y1": y1, + "x2": x2, + "y2": y2, + }, + ) assert gradient.ColorLine.Extend == builder.ExtendMode.PAD assert gradient.ColorLine.ColorStop == color_stops - gradient = layerBuilder.buildPaintLinearGradient(color_line, p0, p1, p2=(150, 230)) - assert (gradient.x2.value, gradient.y2.value) == (150, 230) - assert (gradient.x2, gradient.y2) != (gradient.x1, gradient.y1) + gradient = _buildPaint(gradient) + assert (gradient.x0.value, gradient.y0.value) == (1, 2) + assert (gradient.x1.value, gradient.y1.value) == (3, 4) + assert (gradient.x2.value, gradient.y2.value) == (5, 6) def test_buildPaintRadialGradient(): - layerBuilder = LayerV1ListBuilder() color_stops = [ - builder.buildColorStop(0.0, 0), - builder.buildColorStop(0.5, 1), - builder.buildColorStop(1.0, 2, alpha=0.8), + _build(ot.ColorStop, (0.0, (0,))), + _build(ot.ColorStop, (0.5, 1)), + _build(ot.ColorStop, (1.0, (2, 0.8))), ] - color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT) + color_line = _build( + ot.ColorLine, {"ColorStop": color_stops, "Extend": builder.ExtendMode.REPEAT} + ) c0 = (builder.VariableInt(100), builder.VariableInt(200)) c1 = (builder.VariableInt(150), builder.VariableInt(250)) r0 = builder.VariableInt(10) r1 = builder.VariableInt(5) - gradient = layerBuilder.buildPaintRadialGradient(color_line, c0, c1, r0, r1) + gradient = _build( + ot.Paint, (ot.PaintFormat.PaintRadialGradient, color_line, *c0, r0, *c1, r1) + ) assert gradient.Format == ot.PaintFormat.PaintRadialGradient assert gradient.ColorLine == color_line assert (gradient.x0, gradient.y0) == c0 @@ -378,27 +422,43 @@ def test_buildPaintRadialGradient(): assert gradient.r0 == r0 assert gradient.r1 == r1 - gradient = layerBuilder.buildPaintRadialGradient( - {"stops": color_stops}, c0, c1, r0, r1 + gradient = _build( + ot.Paint, + { + "Format": ot.PaintFormat.PaintRadialGradient, + "ColorLine": {"ColorStop": color_stops}, + "x0": c0[0], + "y0": c0[1], + "x1": c1[0], + "y1": c1[1], + "r0": r0, + "r1": r1, + }, ) assert gradient.ColorLine.Extend == builder.ExtendMode.PAD assert gradient.ColorLine.ColorStop == color_stops + assert (gradient.x0, gradient.y0) == c0 + assert (gradient.x1, gradient.y1) == c1 + assert gradient.r0 == r0 + assert gradient.r1 == r1 def test_buildPaintSweepGradient(): - layerBuilder = LayerV1ListBuilder() - paint = layerBuilder.buildPaintSweepGradient( - colorLine=builder.buildColorLine( - stops=[ - builder.buildColorStop(0.0, 0), - builder.buildColorStop(0.5, 1), - builder.buildColorStop(1.0, 2, alpha=0.8), - ], - ), - centerX=127, - centerY=129, - startAngle=15, - endAngle=42, + paint = _buildPaint( + { + "Format": ot.PaintFormat.PaintSweepGradient, + "ColorLine": { + "ColorStop": ( + (0.0, 0), + (0.5, 1), + (1.0, (2, 0.8)), + ) + }, + "centerX": 127, + "centerY": 129, + "startAngle": 15, + "endAngle": 42, + } ) assert paint.Format == ot.PaintFormat.PaintSweepGradient @@ -409,25 +469,53 @@ def test_buildPaintSweepGradient(): def test_buildPaintGlyph_Solid(): - layerBuilder = LayerV1ListBuilder() - layer = layerBuilder.buildPaintGlyph("a", 2) + layer = _build( + ot.Paint, + ( + ot.PaintFormat.PaintGlyph, + ( + ot.PaintFormat.PaintSolid, + 2, + ), + "a", + ), + ) + assert layer.Format == ot.PaintFormat.PaintGlyph assert layer.Glyph == "a" assert layer.Paint.Format == ot.PaintFormat.PaintSolid assert layer.Paint.Color.PaletteIndex == 2 - layer = layerBuilder.buildPaintGlyph("a", layerBuilder.buildPaintSolid(3, 0.9)) + layer = _build( + ot.Paint, + ( + ot.PaintFormat.PaintGlyph, + ( + ot.PaintFormat.PaintSolid, + (3, 0.9), + ), + "a", + ), + ) assert layer.Paint.Format == ot.PaintFormat.PaintSolid assert layer.Paint.Color.PaletteIndex == 3 assert layer.Paint.Color.Alpha.value == 0.9 def test_buildPaintGlyph_LinearGradient(): - layerBuilder = LayerV1ListBuilder() - layer = layerBuilder.buildPaintGlyph( - "a", - layerBuilder.buildPaintLinearGradient( - {"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250) - ), + layer = _build( + ot.Paint, + { + "Format": ot.PaintFormat.PaintGlyph, + "Glyph": "a", + "Paint": { + "Format": ot.PaintFormat.PaintLinearGradient, + "ColorLine": {"ColorStop": [(0.0, 3), (1.0, 4)]}, + "x0": 100, + "y0": 200, + "x1": 150, + "y1": 250, + }, + }, ) assert layer.Paint.Format == ot.PaintFormat.PaintLinearGradient assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0 @@ -441,23 +529,31 @@ def test_buildPaintGlyph_LinearGradient(): def test_buildPaintGlyph_RadialGradient(): - layerBuilder = LayerV1ListBuilder() - layer = layerBuilder.buildPaintGlyph( - "a", - layerBuilder.buildPaintRadialGradient( - { - "stops": [ - (0.0, 5), - {"offset": 0.5, "paletteIndex": 6, "alpha": 0.8}, - (1.0, 7), - ] - }, - (50, 50), - (75, 75), - 30, - 10, + layer = _build( + ot.Paint, + ( + int(ot.PaintFormat.PaintGlyph), + ( + ot.PaintFormat.PaintRadialGradient, + ( + "pad", + [ + (0.0, 5), + {"StopOffset": 0.5, "Color": {"PaletteIndex": 6, "Alpha": 0.8}}, + (1.0, 7), + ], + ), + 50, + 50, + 30, + 75, + 75, + 10, + ), + "a", ), ) + assert layer.Format == ot.PaintFormat.PaintGlyph assert layer.Paint.Format == ot.PaintFormat.PaintRadialGradient assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0 assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 5 @@ -475,39 +571,58 @@ def test_buildPaintGlyph_RadialGradient(): def test_buildPaintGlyph_Dict_Solid(): - layerBuilder = LayerV1ListBuilder() - layer = layerBuilder.buildPaintGlyph("a", {"format": 2, "paletteIndex": 0}) + layer = _build( + ot.Paint, + ( + int(ot.PaintFormat.PaintGlyph), + (int(ot.PaintFormat.PaintSolid), 1), + "a", + ), + ) + assert layer.Format == ot.PaintFormat.PaintGlyph + assert layer.Format == ot.PaintFormat.PaintGlyph assert layer.Glyph == "a" assert layer.Paint.Format == ot.PaintFormat.PaintSolid - assert layer.Paint.Color.PaletteIndex == 0 + assert layer.Paint.Color.PaletteIndex == 1 def test_buildPaintGlyph_Dict_LinearGradient(): - layerBuilder = LayerV1ListBuilder() - layer = layerBuilder.buildPaintGlyph( - "a", + layer = _build( + ot.Paint, { - "format": 3, - "colorLine": {"stops": [(0.0, 0), (1.0, 1)]}, - "p0": (0, 0), - "p1": (10, 10), + "Format": ot.PaintFormat.PaintGlyph, + "Glyph": "a", + "Paint": { + "Format": 3, + "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]}, + "x0": 0, + "y0": 0, + "x1": 10, + "y1": 10, + }, }, ) + assert layer.Format == ot.PaintFormat.PaintGlyph + assert layer.Glyph == "a" assert layer.Paint.Format == ot.PaintFormat.PaintLinearGradient assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0 def test_buildPaintGlyph_Dict_RadialGradient(): - layerBuilder = LayerV1ListBuilder() - layer = layerBuilder.buildPaintGlyph( - "a", + layer = _buildPaint( { - "format": 4, - "colorLine": {"stops": [(0.0, 0), (1.0, 1)]}, - "c0": (0, 0), - "c1": (10, 10), - "r0": 4, - "r1": 0, + "Glyph": "a", + "Paint": { + "Format": int(ot.PaintFormat.PaintRadialGradient), + "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]}, + "x0": 0, + "y0": 0, + "r0": 4, + "x1": 10, + "y1": 10, + "r1": 0, + }, + "Format": int(ot.PaintFormat.PaintGlyph), }, ) assert layer.Paint.Format == ot.PaintFormat.PaintRadialGradient @@ -515,18 +630,17 @@ def test_buildPaintGlyph_Dict_RadialGradient(): def test_buildPaintColrGlyph(): - paint = LayerV1ListBuilder().buildPaintColrGlyph("a") + paint = _buildPaint((int(ot.PaintFormat.PaintColrGlyph), "a")) assert paint.Format == ot.PaintFormat.PaintColrGlyph assert paint.Glyph == "a" def test_buildPaintTransform(): - layerBuilder = LayerV1ListBuilder() - paint = layerBuilder.buildPaintTransform( - transform=builder.buildAffine2x3((1, 2, 3, 4, 5, 6)), - paint=layerBuilder.buildPaintGlyph( - glyph="a", - paint=layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0), + paint = _buildPaint( + ( + int(ot.PaintFormat.PaintTransform), + (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, (0, 1.0)), "a"), + _build(ot.Affine2x3, (1, 2, 3, 4, 5, 6)), ), ) @@ -541,22 +655,28 @@ def test_buildPaintTransform(): assert paint.Transform.dx.value == 5.0 assert paint.Transform.dy.value == 6.0 - paint = layerBuilder.buildPaintTransform( - (1, 0, 0, 0.3333, 10, 10), + paint = _build( + ot.Paint, { - "format": 4, - "colorLine": {"stops": [(0.0, 0), (1.0, 1)]}, - "c0": (100, 100), - "c1": (100, 100), - "r0": 0, - "r1": 50, + "Format": ot.PaintFormat.PaintTransform, + "Transform": (1, 2, 3, 0.3333, 10, 10), + "Paint": { + "Format": int(ot.PaintFormat.PaintRadialGradient), + "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]}, + "x0": 100, + "y0": 101, + "x1": 102, + "y1": 103, + "r0": 0, + "r1": 50, + }, }, ) assert paint.Format == ot.PaintFormat.PaintTransform assert paint.Transform.xx.value == 1.0 - assert paint.Transform.yx.value == 0.0 - assert paint.Transform.xy.value == 0.0 + assert paint.Transform.yx.value == 2.0 + assert paint.Transform.xy.value == 3.0 assert paint.Transform.yy.value == 0.3333 assert paint.Transform.dx.value == 10 assert paint.Transform.dy.value == 10 @@ -564,18 +684,34 @@ def test_buildPaintTransform(): def test_buildPaintComposite(): - layerBuilder = LayerV1ListBuilder() - composite = layerBuilder.buildPaintComposite( - mode=ot.CompositeMode.SRC_OVER, - source={ - "format": 12, - "mode": "src_over", - "source": {"format": 6, "glyph": "c", "paint": 2}, - "backdrop": {"format": 6, "glyph": "b", "paint": 1}, + composite = _build( + ot.Paint, + { + "Format": int(ot.PaintFormat.PaintComposite), + "CompositeMode": "src_over", + "SourcePaint": { + "Format": ot.PaintFormat.PaintComposite, + "CompositeMode": "src_over", + "SourcePaint": { + "Format": int(ot.PaintFormat.PaintGlyph), + "Glyph": "c", + "Paint": (ot.PaintFormat.PaintSolid, 2), + }, + "BackdropPaint": { + "Format": int(ot.PaintFormat.PaintGlyph), + "Glyph": "b", + "Paint": (ot.PaintFormat.PaintSolid, 1), + }, + }, + "BackdropPaint": { + "Format": ot.PaintFormat.PaintGlyph, + "Glyph": "a", + "Paint": { + "Format": ot.PaintFormat.PaintSolid, + "Color": (0, 1.0), + }, + }, }, - backdrop=layerBuilder.buildPaintGlyph( - "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0) - ), ) assert composite.Format == ot.PaintFormat.PaintComposite @@ -587,9 +723,7 @@ def test_buildPaintComposite(): assert composite.SourcePaint.CompositeMode == ot.CompositeMode.SRC_OVER assert composite.SourcePaint.BackdropPaint.Format == ot.PaintFormat.PaintGlyph assert composite.SourcePaint.BackdropPaint.Glyph == "b" - assert ( - composite.SourcePaint.BackdropPaint.Paint.Format == ot.PaintFormat.PaintSolid - ) + assert composite.SourcePaint.BackdropPaint.Paint.Format == ot.PaintFormat.PaintSolid assert composite.SourcePaint.BackdropPaint.Paint.Color.PaletteIndex == 1 assert composite.CompositeMode == ot.CompositeMode.SRC_OVER assert composite.BackdropPaint.Format == ot.PaintFormat.PaintGlyph @@ -599,13 +733,18 @@ def test_buildPaintComposite(): def test_buildPaintTranslate(): - layerBuilder = LayerV1ListBuilder() - paint = layerBuilder.buildPaintTranslate( - paint=layerBuilder.buildPaintGlyph( - "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0) - ), - dx=123, - dy=-345, + paint = _build( + ot.Paint, + { + "Format": ot.PaintFormat.PaintTranslate, + "Paint": ( + ot.PaintFormat.PaintGlyph, + (ot.PaintFormat.PaintSolid, (0, 1.0)), + "a", + ), + "dx": 123, + "dy": -345, + }, ) assert paint.Format == ot.PaintFormat.PaintTranslate @@ -615,14 +754,19 @@ def test_buildPaintTranslate(): def test_buildPaintRotate(): - layerBuilder = LayerV1ListBuilder() - paint = layerBuilder.buildPaintRotate( - paint=layerBuilder.buildPaintGlyph( - "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0) - ), - angle=15, - centerX=127, - centerY=129, + paint = _build( + ot.Paint, + { + "Format": ot.PaintFormat.PaintRotate, + "Paint": ( + ot.PaintFormat.PaintGlyph, + (ot.PaintFormat.PaintSolid, (0, 1.0)), + "a", + ), + "angle": 15, + "centerX": 127, + "centerY": 129, + }, ) assert paint.Format == ot.PaintFormat.PaintRotate @@ -633,15 +777,20 @@ def test_buildPaintRotate(): def test_buildPaintSkew(): - layerBuilder = LayerV1ListBuilder() - paint = layerBuilder.buildPaintSkew( - paint=layerBuilder.buildPaintGlyph( - "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0) - ), - xSkewAngle=15, - ySkewAngle=42, - centerX=127, - centerY=129, + paint = _build( + ot.Paint, + { + "Format": ot.PaintFormat.PaintSkew, + "Paint": ( + ot.PaintFormat.PaintGlyph, + (ot.PaintFormat.PaintSolid, (0, 1.0)), + "a", + ), + "xSkewAngle": 15, + "ySkewAngle": 42, + "centerX": 127, + "centerY": 129, + }, ) assert paint.Format == ot.PaintFormat.PaintSkew @@ -654,22 +803,44 @@ def test_buildPaintSkew(): def test_buildColrV1(): colorGlyphs = { - "a": [("b", 0), ("c", 1)], - "d": [ - ("e", {"format": 2, "paletteIndex": 2, "alpha": 0.8}), - ( - "f", - { - "format": 4, - "colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "reflect"}, - "c0": (0, 0), - "c1": (0, 0), - "r0": 10, - "r1": 0, - }, - ), - ], - "g": [("h", 5)], + "a": ( + ot.PaintFormat.PaintColrLayers, + [ + (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0), "b"), + (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 1), "c"), + ], + ), + "d": ( + ot.PaintFormat.PaintColrLayers, + [ + ( + ot.PaintFormat.PaintGlyph, + {"Format": 2, "Color": {"PaletteIndex": 2, "Alpha": 0.8}}, + "e", + ), + ( + ot.PaintFormat.PaintGlyph, + { + "Format": 4, + "ColorLine": { + "ColorStop": [(0.0, 3), (1.0, 4)], + "Extend": "reflect", + }, + "x0": 0, + "y0": 0, + "x1": 0, + "y1": 0, + "r0": 10, + "r1": 0, + }, + "f", + ), + ], + ), + "g": ( + ot.PaintFormat.PaintColrLayers, + [(ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 5), "h")], + ), } glyphMap = { ".notdef": 0, @@ -700,14 +871,17 @@ def test_buildColrV1(): def test_buildColrV1_more_than_255_paints(): num_paints = 364 colorGlyphs = { - "a": [ - { - "format": 6, # PaintGlyph - "paint": 0, - "glyph": name, - } - for name in (f"glyph{i}" for i in range(num_paints)) - ], + "a": ( + ot.PaintFormat.PaintColrLayers, + [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": (ot.PaintFormat.PaintSolid, 0), + "Glyph": name, + } + for name in (f"glyph{i}" for i in range(num_paints)) + ], + ), } layers, baseGlyphs = builder.buildColrV1(colorGlyphs) paints = layers.Paint @@ -750,9 +924,7 @@ def test_split_color_glyphs_by_version(): assert colorGlyphsV0 == {"a": [("b", 0), ("c", 1), ("d", 2), ("e", 3)]} assert not colorGlyphsV1 - colorGlyphs = { - "a": [("b", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=0.0))] - } + colorGlyphs = {"a": (ot.PaintFormat.PaintGlyph, 0, "b")} colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs) @@ -798,32 +970,35 @@ def assertNoV0Content(colr): def test_build_layerv1list_empty(): - # Nobody uses PaintColrLayers (format 1), no layerlist + # Nobody uses PaintColrLayers, no layerlist colr = builder.buildCOLR( { - "a": { - "format": 6, # PaintGlyph - "paint": {"format": 2, "paletteIndex": 2, "alpha": 0.8}, - "glyph": "b", - }, - # A list of 1 shouldn't become a PaintColrLayers - "b": [ - { - "format": 6, # PaintGlyph - "paint": { - "format": 3, - "colorLine": { - "stops": [(0.0, 2), (1.0, 3)], - "extend": "reflect", - }, - "p0": (1, 2), - "p1": (3, 4), - "p2": (2, 2), + # BaseGlyph, tuple form + "a": ( + int(ot.PaintFormat.PaintGlyph), + (2, (2, 0.8)), + "b", + ), + # BaseGlyph, map form + "b": { + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": { + "Format": int(ot.PaintFormat.PaintLinearGradient), + "ColorLine": { + "ColorStop": [(0.0, 2), (1.0, 3)], + "Extend": "reflect", }, - "glyph": "bb", - } - ], - } + "x0": 1, + "y0": 2, + "x1": 3, + "y1": 4, + "x2": 2, + "y2": 2, + }, + "Glyph": "bb", + }, + }, + version=1, ) assertIsColrV1(colr) @@ -853,35 +1028,42 @@ def _paint_names(paints) -> List[str]: 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": 2, "paletteIndex": 2, "alpha": 0.8} + solid_paint = {"Format": 2, "Color": {"PaletteIndex": 2, "Alpha": 0.8}} backdrop = { - "format": 6, # PaintGlyph - "paint": solid_paint, - "glyph": "back", + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": solid_paint, + "Glyph": "back", } a_foreground = { - "format": 6, # PaintGlyph - "paint": solid_paint, - "glyph": "a_fore", + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": solid_paint, + "Glyph": "a_fore", } b_foreground = { - "format": 6, # PaintGlyph - "paint": solid_paint, - "glyph": "b_fore", + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": solid_paint, + "Glyph": "b_fore", } - # list => PaintColrLayers, which means contents should be in LayerV1List + # list => PaintColrLayers, contents should land in LayerV1List colr = builder.buildCOLR( { - "a": [ - backdrop, - a_foreground, - ], - "b": [ - backdrop, - b_foreground, - ], - } + "a": ( + ot.PaintFormat.PaintColrLayers, + [ + backdrop, + a_foreground, + ], + ), + "b": { + "Format": ot.PaintFormat.PaintColrLayers, + "Layers": [ + backdrop, + b_foreground, + ], + }, + }, + version=1, ) assertIsColrV1(colr) @@ -902,47 +1084,51 @@ def test_build_layerv1list_simple(): def test_build_layerv1list_with_sharing(): # Three colr glyphs, each with two layers in common - solid_paint = {"format": 2, "paletteIndex": 2, "alpha": 0.8} + solid_paint = {"Format": 2, "Color": (2, 0.8)} backdrop = [ { - "format": 6, # PaintGlyph - "paint": solid_paint, - "glyph": "back1", + "Format": int(ot.PaintFormat.PaintGlyph), + "Paint": solid_paint, + "Glyph": "back1", }, { - "format": 6, # PaintGlyph - "paint": solid_paint, - "glyph": "back2", + "Format": ot.PaintFormat.PaintGlyph, + "Paint": solid_paint, + "Glyph": "back2", }, ] a_foreground = { - "format": 6, # PaintGlyph - "paint": solid_paint, - "glyph": "a_fore", + "Format": ot.PaintFormat.PaintGlyph, + "Paint": solid_paint, + "Glyph": "a_fore", } b_background = { - "format": 6, # PaintGlyph - "paint": solid_paint, - "glyph": "b_back", + "Format": ot.PaintFormat.PaintGlyph, + "Paint": solid_paint, + "Glyph": "b_back", } b_foreground = { - "format": 6, # PaintGlyph - "paint": solid_paint, - "glyph": "b_fore", + "Format": ot.PaintFormat.PaintGlyph, + "Paint": solid_paint, + "Glyph": "b_fore", } c_background = { - "format": 6, # PaintGlyph - "paint": solid_paint, - "glyph": "c_back", + "Format": ot.PaintFormat.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, - } + "a": (ot.PaintFormat.PaintColrLayers, backdrop + [a_foreground]), + "b": ( + ot.PaintFormat.PaintColrLayers, + [b_background] + backdrop + [b_foreground], + ), + "c": (ot.PaintFormat.PaintColrLayers, [c_background] + backdrop), + }, + version=1, ) assertIsColrV1(colr) @@ -974,9 +1160,12 @@ def test_build_layerv1list_with_sharing(): def test_build_layerv1list_with_overlaps(): paints = [ { - "format": 6, # PaintGlyph - "paint": {"format": 2, "paletteIndex": 2, "alpha": 0.8}, - "glyph": c, + "Format": ot.PaintFormat.PaintGlyph, + "Paint": { + "Format": ot.PaintFormat.PaintSolid, + "Color": {"PaletteIndex": 2, "Alpha": 0.8}, + }, + "Glyph": c, } for c in "abcdefghi" ] @@ -984,10 +1173,11 @@ def test_build_layerv1list_with_overlaps(): # list => PaintColrLayers, which means contents should be in LayerV1List colr = builder.buildCOLR( { - "a": paints[0:4], - "b": paints[0:6], - "c": paints[2:8], - } + "a": (ot.PaintFormat.PaintColrLayers, paints[0:4]), + "b": (ot.PaintFormat.PaintColrLayers, paints[0:6]), + "c": (ot.PaintFormat.PaintColrLayers, paints[2:8]), + }, + version=1, ) assertIsColrV1(colr) @@ -1017,6 +1207,26 @@ def test_build_layerv1list_with_overlaps(): assert colr.table.LayerV1List.LayerCount == 11 +def test_explicit_version_1(): + colr = builder.buildCOLR( + { + "a": ( + ot.PaintFormat.PaintColrLayers, + [ + (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 0), "b"), + (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, 1), "c"), + ], + ) + }, + version=1, + ) + assert colr.version == 1 + assert not hasattr(colr, "ColorLayers") + assert hasattr(colr, "table") + assert isinstance(colr.table, ot.COLR) + assert colr.table.VarStore is None + + class BuildCOLRTest(object): def test_automatic_version_all_solid_color_glyphs(self): colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}) @@ -1028,38 +1238,55 @@ class BuildCOLRTest(object): def test_automatic_version_no_solid_color_glyphs(self): colr = builder.buildCOLR( { - "a": [ - ( - "b", - { - "format": 4, - "colorLine": { - "stops": [(0.0, 0), (1.0, 1)], - "extend": "repeat", + "a": ( + ot.PaintFormat.PaintColrLayers, + [ + ( + ot.PaintFormat.PaintGlyph, + { + "Format": int(ot.PaintFormat.PaintRadialGradient), + "ColorLine": { + "ColorStop": [(0.0, 0), (1.0, 1)], + "Extend": "repeat", + }, + "x0": 1, + "y0": 0, + "x1": 10, + "y1": 0, + "r0": 4, + "r1": 2, }, - "c0": (1, 0), - "c1": (10, 0), - "r0": 4, - "r1": 2, - }, - ), - ("c", {"format": 2, "paletteIndex": 2, "alpha": 0.8}), - ], - "d": [ - ( - "e", + "b", + ), + ( + ot.PaintFormat.PaintGlyph, + {"Format": 2, "Color": {"PaletteIndex": 2, "Alpha": 0.8}}, + "c", + ), + ], + ), + "d": ( + ot.PaintFormat.PaintColrLayers, + [ { - "format": 3, - "colorLine": { - "stops": [(0.0, 2), (1.0, 3)], - "extend": "reflect", + "Format": ot.PaintFormat.PaintGlyph, + "Glyph": "e", + "Paint": { + "Format": ot.PaintFormat.PaintLinearGradient, + "ColorLine": { + "ColorStop": [(0.0, 2), (1.0, 3)], + "Extend": "reflect", + }, + "x0": 1, + "y0": 2, + "x1": 3, + "y1": 4, + "x2": 2, + "y2": 2, }, - "p0": (1, 2), - "p1": (3, 4), - "p2": (2, 2), - }, - ), - ], + } + ], + ), } ) assertIsColrV1(colr) @@ -1072,19 +1299,30 @@ class BuildCOLRTest(object): colr = builder.buildCOLR( { "a": [("b", 0), ("c", 1)], - "d": [ - ( - "e", - { - "format": 3, - "colorLine": {"stops": [(0.0, 2), (1.0, 3)]}, - "p0": (1, 2), - "p1": (3, 4), - "p2": (2, 2), - }, - ), - ("f", {"format": 2, "paletteIndex": 2, "alpha": 0.8}), - ], + "d": ( + ot.PaintFormat.PaintColrLayers, + [ + ( + ot.PaintFormat.PaintGlyph, + { + "Format": ot.PaintFormat.PaintLinearGradient, + "ColorLine": {"ColorStop": [(0.0, 2), (1.0, 3)]}, + "x0": 1, + "y0": 2, + "x1": 3, + "y1": 4, + "x2": 2, + "y2": 2, + }, + "e", + ), + ( + ot.PaintFormat.PaintGlyph, + (ot.PaintFormat.PaintSolid, (2, 0.8)), + "f", + ), + ], + ), } ) assertIsColrV1(colr) @@ -1110,7 +1348,26 @@ class BuildCOLRTest(object): assert hasattr(colr, "ColorLayers") def test_explicit_version_1(self): - colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=1) + colr = builder.buildCOLR( + { + "a": ( + ot.PaintFormat.PaintColrLayers, + [ + ( + ot.PaintFormat.PaintGlyph, + (ot.PaintFormat.PaintSolid, 0), + "b", + ), + ( + ot.PaintFormat.PaintGlyph, + (ot.PaintFormat.PaintSolid, 1), + "c", + ), + ], + ) + }, + version=1, + ) assert colr.version == 1 assert not hasattr(colr, "ColorLayers") assert hasattr(colr, "table") diff --git a/Tests/colorLib/unbuilder_test.py b/Tests/colorLib/unbuilder_test.py index decd1856b..fb22abcdb 100644 --- a/Tests/colorLib/unbuilder_test.py +++ b/Tests/colorLib/unbuilder_test.py @@ -5,115 +5,141 @@ import pytest TEST_COLOR_GLYPHS = { - "glyph00010": [ - { - "format": int(ot.PaintFormat.PaintGlyph), - "glyph": "glyph00011", - "paint": { - "format": int(ot.PaintFormat.PaintSolid), - "paletteIndex": 2, - "alpha": 0.5, - }, - }, - { - "format": int(ot.PaintFormat.PaintGlyph), - "glyph": "glyph00012", - "paint": { - "format": int(ot.PaintFormat.PaintLinearGradient), - "colorLine": { - "stops": [ - {"offset": 0.0, "paletteIndex": 3, "alpha": 1.0}, - {"offset": 0.5, "paletteIndex": 4, "alpha": 1.0}, - {"offset": 1.0, "paletteIndex": 5, "alpha": 1.0}, - ], - "extend": "repeat", + "glyph00010": ( + ot.PaintFormat.PaintColrLayers, + [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Glyph": "glyph00011", + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "Color": { + "PaletteIndex": 2, + "Alpha": 0.5, + }, }, - "p0": (1, 2), - "p1": (-3, -4), - "p2": (5, 6), }, - }, - { - "format": int(ot.PaintFormat.PaintGlyph), - "glyph": "glyph00013", - "paint": { - "format": int(ot.PaintFormat.PaintTransform), - "transform": (-13.0, 14.0, 15.0, -17.0, 18.0, 19.0), - "paint": { - "format": int(ot.PaintFormat.PaintRadialGradient), - "colorLine": { - "stops": [ - {"offset": 0.0, "paletteIndex": 6, "alpha": 1.0}, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Glyph": "glyph00012", + "Paint": { + "Format": int(ot.PaintFormat.PaintLinearGradient), + "ColorLine": { + "ColorStop": [ { - "offset": 1.0, - "paletteIndex": 7, - "alpha": 0.4, + "StopOffset": 0.0, + "Color": {"PaletteIndex": 3, "Alpha": 1.0}, + }, + { + "StopOffset": 0.5, + "Color": {"PaletteIndex": 4, "Alpha": 1.0}, + }, + { + "StopOffset": 1.0, + "Color": {"PaletteIndex": 5, "Alpha": 1.0}, }, ], - "extend": "pad", + "Extend": "repeat", }, - "c0": (7, 8), - "r0": 9, - "c1": (10, 11), - "r1": 12, + "x0": 1, + "y0": 2, + "x1": -3, + "y1": -4, + "x2": 5, + "y2": 6, }, }, - }, - { - "format": int(ot.PaintFormat.PaintTranslate), - "dx": 257.0, - "dy": 258.0, - "paint": { - "format": int(ot.PaintFormat.PaintRotate), - "angle": 45.0, - "centerX": 255.0, - "centerY": 256.0, - "paint": { - "format": int(ot.PaintFormat.PaintSkew), - "xSkewAngle": -11.0, - "ySkewAngle": 5.0, - "centerX": 253.0, - "centerY": 254.0, - "paint": { - "format": int(ot.PaintFormat.PaintGlyph), - "glyph": "glyph00011", - "paint": { - "format": int(ot.PaintFormat.PaintSolid), - "paletteIndex": 2, - "alpha": 0.5, + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Glyph": "glyph00013", + "Paint": { + "Format": int(ot.PaintFormat.PaintTransform), + "Transform": (-13.0, 14.0, 15.0, -17.0, 18.0, 19.0), + "Paint": { + "Format": int(ot.PaintFormat.PaintRadialGradient), + "ColorLine": { + "ColorStop": [ + { + "StopOffset": 0.0, + "Color": {"PaletteIndex": 6, "Alpha": 1.0}, + }, + { + "StopOffset": 1.0, + "Color": { + "PaletteIndex": 7, + "Alpha": 0.4, + }, + }, + ], + "Extend": "pad", + }, + "x0": 7, + "y0": 8, + "r0": 9, + "x1": 10, + "y1": 11, + "r1": 12, + }, + }, + }, + { + "Format": int(ot.PaintFormat.PaintTranslate), + "dx": 257.0, + "dy": 258.0, + "Paint": { + "Format": int(ot.PaintFormat.PaintRotate), + "angle": 45.0, + "centerX": 255.0, + "centerY": 256.0, + "Paint": { + "Format": int(ot.PaintFormat.PaintSkew), + "xSkewAngle": -11.0, + "ySkewAngle": 5.0, + "centerX": 253.0, + "centerY": 254.0, + "Paint": { + "Format": int(ot.PaintFormat.PaintGlyph), + "Glyph": "glyph00011", + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "Color": { + "PaletteIndex": 2, + "Alpha": 0.5, + }, + }, }, }, }, }, - }, - ], + ], + ), "glyph00014": { - "format": int(ot.PaintFormat.PaintComposite), - "mode": "src_over", - "source": { - "format": int(ot.PaintFormat.PaintColrGlyph), - "glyph": "glyph00010", + "Format": int(ot.PaintFormat.PaintComposite), + "CompositeMode": "src_over", + "SourcePaint": { + "Format": int(ot.PaintFormat.PaintColrGlyph), + "Glyph": "glyph00010", }, - "backdrop": { - "format": int(ot.PaintFormat.PaintTransform), - "transform": (1.0, 0.0, 0.0, 1.0, 300.0, 0.0), - "paint": { - "format": int(ot.PaintFormat.PaintColrGlyph), - "glyph": "glyph00010", + "BackdropPaint": { + "Format": int(ot.PaintFormat.PaintTransform), + "Transform": (1.0, 0.0, 0.0, 1.0, 300.0, 0.0), + "Paint": { + "Format": int(ot.PaintFormat.PaintColrGlyph), + "Glyph": "glyph00010", }, }, }, "glyph00015": { - "format": int(ot.PaintFormat.PaintGlyph), - "glyph": "glyph00011", - "paint": { - "format": int(ot.PaintFormat.PaintSweepGradient), - "colorLine": { - "stops": [ - {"offset": 0.0, "paletteIndex": 3, "alpha": 1.0}, - {"offset": 1.0, "paletteIndex": 5, "alpha": 1.0}, + "Format": int(ot.PaintFormat.PaintGlyph), + "Glyph": "glyph00011", + "Paint": { + "Format": int(ot.PaintFormat.PaintSweepGradient), + "ColorLine": { + "ColorStop": [ + {"StopOffset": 0.0, "Color": {"PaletteIndex": 3, "Alpha": 1.0}}, + {"StopOffset": 1.0, "Color": {"PaletteIndex": 5, "Alpha": 1.0}}, ], - "extend": "pad", + "Extend": "pad", }, "centerX": 259, "centerY": 300, @@ -121,35 +147,52 @@ TEST_COLOR_GLYPHS = { "endAngle": 135.0, }, }, - "glyph00016": [ - { - "format": int(ot.PaintFormat.PaintGlyph), - "glyph": "glyph00011", - "paint": { - "format": int(ot.PaintFormat.PaintSolid), - "paletteIndex": 2, - "alpha": 0.5, - }, - }, - { - "format": int(ot.PaintFormat.PaintGlyph), - "glyph": "glyph00012", - "paint": { - "format": int(ot.PaintFormat.PaintLinearGradient), - "colorLine": { - "stops": [ - {"offset": 0.0, "paletteIndex": 3, "alpha": 1.0}, - {"offset": 0.5, "paletteIndex": 4, "alpha": 1.0}, - {"offset": 1.0, "paletteIndex": 5, "alpha": 1.0}, - ], - "extend": "repeat", + "glyph00016": ( + ot.PaintFormat.PaintColrLayers, + [ + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Glyph": "glyph00011", + "Paint": { + "Format": int(ot.PaintFormat.PaintSolid), + "Color": { + "PaletteIndex": 2, + "Alpha": 0.5, + }, }, - "p0": (1, 2), - "p1": (-3, -4), - "p2": (5, 6), }, - }, - ], + { + "Format": int(ot.PaintFormat.PaintGlyph), + "Glyph": "glyph00012", + "Paint": { + "Format": int(ot.PaintFormat.PaintLinearGradient), + "ColorLine": { + "ColorStop": [ + { + "StopOffset": 0.0, + "Color": {"PaletteIndex": 3, "Alpha": 1.0}, + }, + { + "StopOffset": 0.5, + "Color": {"PaletteIndex": 4, "Alpha": 1.0}, + }, + { + "StopOffset": 1.0, + "Color": {"PaletteIndex": 5, "Alpha": 1.0}, + }, + ], + "Extend": "repeat", + }, + "x0": 1, + "y0": 2, + "x1": -3, + "y1": -4, + "x2": 5, + "y2": 6, + }, + }, + ], + ), }