Merge pull request #2181 from fonttools/colr_to_from_dicts
Use otData for dict to COLR conversion
This commit is contained in:
commit
6106bf7c14
@ -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_O_L_R_
|
||||||
from fontTools.ttLib.tables import C_P_A_L_
|
from fontTools.ttLib.tables import C_P_A_L_
|
||||||
from fontTools.ttLib.tables import _n_a_m_e
|
from fontTools.ttLib.tables import _n_a_m_e
|
||||||
from fontTools.ttLib.tables.otBase import BaseTable
|
|
||||||
from fontTools.ttLib.tables import otTables as ot
|
from fontTools.ttLib.tables import otTables as ot
|
||||||
from fontTools.ttLib.tables.otTables import (
|
from fontTools.ttLib.tables.otTables import (
|
||||||
ExtendMode,
|
ExtendMode,
|
||||||
@ -36,6 +35,11 @@ from fontTools.ttLib.tables.otTables import (
|
|||||||
)
|
)
|
||||||
from .errors import ColorLibError
|
from .errors import ColorLibError
|
||||||
from .geometry import round_start_circle_stable_containment
|
from .geometry import round_start_circle_stable_containment
|
||||||
|
from .table_builder import (
|
||||||
|
convertTupleClass,
|
||||||
|
BuildCallback,
|
||||||
|
TableBuilder,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO move type aliases to colorLib.types?
|
# TODO move type aliases to colorLib.types?
|
||||||
@ -45,21 +49,64 @@ _PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]]
|
|||||||
_PaintInputList = Sequence[_PaintInput]
|
_PaintInputList = Sequence[_PaintInput]
|
||||||
_ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]]
|
_ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]]
|
||||||
_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
|
_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
|
MAX_PAINT_COLR_LAYER_COUNT = 255
|
||||||
|
_DEFAULT_ALPHA = VariableFloat(1.0)
|
||||||
|
_MAX_REUSE_LEN = 32
|
||||||
|
|
||||||
|
|
||||||
|
def _beforeBuildPaintRadialGradient(paint, source):
|
||||||
|
# normalize input types (which may or may not specify a varIdx)
|
||||||
|
x0 = convertTupleClass(VariableFloat, source["x0"])
|
||||||
|
y0 = convertTupleClass(VariableFloat, source["y0"])
|
||||||
|
r0 = convertTupleClass(VariableFloat, source["r0"])
|
||||||
|
x1 = convertTupleClass(VariableFloat, source["x1"])
|
||||||
|
y1 = convertTupleClass(VariableFloat, source["y1"])
|
||||||
|
r1 = convertTupleClass(VariableFloat, source["r1"])
|
||||||
|
|
||||||
|
# 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(
|
def populateCOLRv0(
|
||||||
@ -112,7 +159,6 @@ def buildCOLR(
|
|||||||
varStore: Optional[ot.VarStore] = None,
|
varStore: Optional[ot.VarStore] = None,
|
||||||
) -> C_O_L_R_.table_C_O_L_R_:
|
) -> C_O_L_R_.table_C_O_L_R_:
|
||||||
"""Build COLR table from color layers mapping.
|
"""Build COLR table from color layers mapping.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
colorGlyphs: map of base glyph name to, either list of (layer glyph name,
|
colorGlyphs: map of base glyph name to, either list of (layer glyph name,
|
||||||
color palette index) tuples for COLRv0; or a single Paint (dict) or
|
color palette index) tuples for COLRv0; or a single Paint (dict) or
|
||||||
@ -124,7 +170,6 @@ def buildCOLR(
|
|||||||
glyphMap: a map from glyph names to glyph indices, as returned from
|
glyphMap: a map from glyph names to glyph indices, as returned from
|
||||||
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
|
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
|
||||||
varStore: Optional ItemVarationStore for deltas associated with v1 layer.
|
varStore: Optional ItemVarationStore for deltas associated with v1 layer.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A new COLR table.
|
A new COLR table.
|
||||||
"""
|
"""
|
||||||
@ -295,8 +340,6 @@ def buildCPAL(
|
|||||||
# COLR v1 tables
|
# COLR v1 tables
|
||||||
# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
|
# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
|
||||||
|
|
||||||
_DEFAULT_ALPHA = VariableFloat(1.0)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_colrv0_layer(layer: Any) -> bool:
|
def _is_colrv0_layer(layer: Any) -> bool:
|
||||||
# Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which
|
# Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which
|
||||||
@ -328,124 +371,15 @@ def _split_color_glyphs_by_version(
|
|||||||
return colorGlyphsV0, colorGlyphsV1
|
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]:
|
def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
|
||||||
# TODO feels like something itertools might have already
|
# TODO feels like something itertools might have already
|
||||||
for lbound in range(num_layers):
|
for lbound in range(num_layers):
|
||||||
# TODO may want a max length to limit scope of search
|
|
||||||
# Reuse of very large #s of layers is relatively unlikely
|
# Reuse of very large #s of layers is relatively unlikely
|
||||||
# +2: we want sequences of at least 2
|
# +2: we want sequences of at least 2
|
||||||
# otData handles single-record duplication
|
# otData handles single-record duplication
|
||||||
for ubound in range(lbound + 2, num_layers + 1):
|
for ubound in range(
|
||||||
|
lbound + 2, min(num_layers + 1, lbound + 2 + _MAX_REUSE_LEN)
|
||||||
|
):
|
||||||
yield (lbound, ubound)
|
yield (lbound, ubound)
|
||||||
|
|
||||||
|
|
||||||
@ -463,6 +397,17 @@ class LayerV1ListBuilder:
|
|||||||
self.tuples = {}
|
self.tuples = {}
|
||||||
self.keepAlive = []
|
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):
|
def _paint_tuple(self, paint: ot.Paint):
|
||||||
# start simple, who even cares about cyclic graphs or interesting field types
|
# start simple, who even cares about cyclic graphs or interesting field types
|
||||||
def _tuple_safe(value):
|
def _tuple_safe(value):
|
||||||
@ -488,186 +433,41 @@ class LayerV1ListBuilder:
|
|||||||
def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
|
def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
|
||||||
return tuple(self._paint_tuple(p) for p in paints)
|
return tuple(self._paint_tuple(p) for p in paints)
|
||||||
|
|
||||||
def buildPaintSolid(
|
# COLR layers is unusual in that it modifies shared state
|
||||||
self, paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA
|
# so we need a callback into an object
|
||||||
) -> ot.Paint:
|
def _beforeBuildPaintColrLayers(self, dest, source):
|
||||||
ot_paint = ot.Paint()
|
paint = ot.Paint()
|
||||||
ot_paint.Format = int(ot.PaintFormat.PaintSolid)
|
paint.Format = int(ot.PaintFormat.PaintColrLayers)
|
||||||
ot_paint.Color = buildColorIndex(paletteIndex, alpha)
|
self.slices.append(paint)
|
||||||
return ot_paint
|
|
||||||
|
|
||||||
def buildPaintLinearGradient(
|
# Sketchy gymnastics: a sequence input will have dropped it's layers
|
||||||
self,
|
# into NumLayers; get it back
|
||||||
colorLine: _ColorLineInput,
|
if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
|
||||||
p0: _PointTuple,
|
layers = source["NumLayers"]
|
||||||
p1: _PointTuple,
|
else:
|
||||||
p2: Optional[_PointTuple] = None,
|
layers = source["Layers"]
|
||||||
) -> ot.Paint:
|
|
||||||
ot_paint = ot.Paint()
|
|
||||||
ot_paint.Format = int(ot.PaintFormat.PaintLinearGradient)
|
|
||||||
ot_paint.ColorLine = _to_color_line(colorLine)
|
|
||||||
|
|
||||||
if p2 is None:
|
# Convert maps seqs or whatever into typed objects
|
||||||
p2 = copy.copy(p1)
|
layers = [self.buildPaint(l) for l in layers]
|
||||||
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
|
# No reason to have a colr layers with just one entry
|
||||||
|
if len(layers) == 1:
|
||||||
def buildPaintRadialGradient(
|
return layers[0], {}
|
||||||
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)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Look for reuse, with preference to longer sequences
|
# Look for reuse, with preference to longer sequences
|
||||||
|
# This may make the layer list smaller
|
||||||
found_reuse = True
|
found_reuse = True
|
||||||
while found_reuse:
|
while found_reuse:
|
||||||
found_reuse = False
|
found_reuse = False
|
||||||
|
|
||||||
ranges = sorted(
|
ranges = sorted(
|
||||||
_reuse_ranges(len(paints)),
|
_reuse_ranges(len(layers)),
|
||||||
key=lambda t: (t[1] - t[0], t[1], t[0]),
|
key=lambda t: (t[1] - t[0], t[1], t[0]),
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
for lbound, ubound in ranges:
|
for lbound, ubound in ranges:
|
||||||
reuse_lbound = self.reusePool.get(
|
reuse_lbound = self.reusePool.get(
|
||||||
self._as_tuple(paints[lbound:ubound]), -1
|
self._as_tuple(layers[lbound:ubound]), -1
|
||||||
)
|
)
|
||||||
if reuse_lbound == -1:
|
if reuse_lbound == -1:
|
||||||
continue
|
continue
|
||||||
@ -675,47 +475,45 @@ class LayerV1ListBuilder:
|
|||||||
new_slice.Format = int(ot.PaintFormat.PaintColrLayers)
|
new_slice.Format = int(ot.PaintFormat.PaintColrLayers)
|
||||||
new_slice.NumLayers = ubound - lbound
|
new_slice.NumLayers = ubound - lbound
|
||||||
new_slice.FirstLayerIndex = reuse_lbound
|
new_slice.FirstLayerIndex = reuse_lbound
|
||||||
paints = paints[:lbound] + [new_slice] + paints[ubound:]
|
layers = layers[:lbound] + [new_slice] + layers[ubound:]
|
||||||
found_reuse = True
|
found_reuse = True
|
||||||
break
|
break
|
||||||
|
|
||||||
ot_paint.NumLayers = len(paints)
|
# The layer list is now final; if it's too big we need to tree it
|
||||||
ot_paint.FirstLayerIndex = len(self.layers)
|
is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
|
||||||
self.layers.extend(paints)
|
layers = _build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
|
||||||
|
|
||||||
# Register our parts for reuse
|
# We now have a tree of sequences with Paint leaves.
|
||||||
for lbound, ubound in _reuse_ranges(len(paints)):
|
# Convert the sequences into PaintColrLayers.
|
||||||
self.reusePool[self._as_tuple(paints[lbound:ubound])] = (
|
def listToColrLayers(layer):
|
||||||
lbound + ot_paint.FirstLayerIndex
|
if isinstance(layer, collections.abc.Sequence):
|
||||||
)
|
return self.buildPaint(
|
||||||
|
{
|
||||||
|
"Format": ot.PaintFormat.PaintColrLayers,
|
||||||
|
"Layers": [listToColrLayers(l) for l in layer],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return layer
|
||||||
|
|
||||||
return ot_paint
|
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 provided we aren't a tree
|
||||||
|
# If we are a tree the leaves registered for reuse and that will suffice
|
||||||
|
if not is_tree:
|
||||||
|
for lbound, ubound in _reuse_ranges(len(layers)):
|
||||||
|
self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
|
||||||
|
lbound + paint.FirstLayerIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
# we've fully built dest; empty source prevents generalized build from kicking in
|
||||||
|
return paint, {}
|
||||||
|
|
||||||
def buildPaint(self, paint: _PaintInput) -> ot.Paint:
|
def buildPaint(self, paint: _PaintInput) -> ot.Paint:
|
||||||
if isinstance(paint, ot.Paint):
|
return self.tableBuilder.build(ot.Paint, 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}")
|
|
||||||
|
|
||||||
def build(self) -> ot.LayerV1List:
|
def build(self) -> ot.LayerV1List:
|
||||||
layers = ot.LayerV1List()
|
layers = ot.LayerV1List()
|
||||||
@ -724,31 +522,6 @@ class LayerV1ListBuilder:
|
|||||||
return layers
|
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(
|
def buildBaseGlyphV1Record(
|
||||||
baseGlyph: str, layerBuilder: LayerV1ListBuilder, paint: _PaintInput
|
baseGlyph: str, layerBuilder: LayerV1ListBuilder, paint: _PaintInput
|
||||||
) -> ot.BaseGlyphV1List:
|
) -> ot.BaseGlyphV1List:
|
||||||
|
234
Lib/fontTools/colorLib/table_builder.py
Normal file
234
Lib/fontTools/colorLib/table_builder.py
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
"""
|
||||||
|
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,
|
||||||
|
SimpleValue,
|
||||||
|
Struct,
|
||||||
|
Short,
|
||||||
|
UInt8,
|
||||||
|
UShort,
|
||||||
|
VarInt16,
|
||||||
|
VarUInt16,
|
||||||
|
IntValue,
|
||||||
|
FloatValue,
|
||||||
|
)
|
||||||
|
from fontTools.misc.fixedTools import otRound
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
tupleClass = getattr(converter, "tupleClass", None)
|
||||||
|
enumClass = getattr(converter, "enumClass", 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, IntValue):
|
||||||
|
value = otRound(value)
|
||||||
|
elif isinstance(converter, FloatValue):
|
||||||
|
value = float(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
|
||||||
|
|
||||||
|
|
||||||
|
class TableUnbuilder:
|
||||||
|
def __init__(self, callbackTable=None):
|
||||||
|
if callbackTable is None:
|
||||||
|
callbackTable = {}
|
||||||
|
self._callbackTable = callbackTable
|
||||||
|
|
||||||
|
def unbuild(self, table):
|
||||||
|
assert isinstance(table, BaseTable)
|
||||||
|
|
||||||
|
source = {}
|
||||||
|
|
||||||
|
callbackKey = (type(table),)
|
||||||
|
if isinstance(table, FormatSwitchingBaseTable):
|
||||||
|
source["Format"] = int(table.Format)
|
||||||
|
callbackKey += (table.Format,)
|
||||||
|
|
||||||
|
for converter in table.getConverters():
|
||||||
|
if isinstance(converter, ComputedInt):
|
||||||
|
continue
|
||||||
|
value = getattr(table, converter.name)
|
||||||
|
|
||||||
|
tupleClass = getattr(converter, "tupleClass", None)
|
||||||
|
enumClass = getattr(converter, "enumClass", None)
|
||||||
|
if tupleClass:
|
||||||
|
source[converter.name] = tuple(value)
|
||||||
|
elif enumClass:
|
||||||
|
source[converter.name] = value.name.lower()
|
||||||
|
elif isinstance(converter, Struct):
|
||||||
|
if converter.repeat:
|
||||||
|
source[converter.name] = [self.unbuild(v) for v in value]
|
||||||
|
else:
|
||||||
|
source[converter.name] = self.unbuild(value)
|
||||||
|
elif isinstance(converter, SimpleValue):
|
||||||
|
# "simple" values (e.g. int, float, str) need no further un-building
|
||||||
|
source[converter.name] = value
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Don't know how unbuild {value!r} with {converter!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
source = self._callbackTable.get(callbackKey, lambda s: s)(source)
|
||||||
|
|
||||||
|
return source
|
@ -1,47 +1,15 @@
|
|||||||
from fontTools.ttLib.tables import otTables as ot
|
from fontTools.ttLib.tables import otTables as ot
|
||||||
|
from .table_builder import TableUnbuilder
|
||||||
|
|
||||||
|
|
||||||
def unbuildColrV1(layerV1List, baseGlyphV1List, ignoreVarIdx=False):
|
def unbuildColrV1(layerV1List, baseGlyphV1List):
|
||||||
unbuilder = LayerV1ListUnbuilder(layerV1List.Paint, ignoreVarIdx=ignoreVarIdx)
|
unbuilder = LayerV1ListUnbuilder(layerV1List.Paint)
|
||||||
return {
|
return {
|
||||||
rec.BaseGlyph: unbuilder.unbuildPaint(rec.Paint)
|
rec.BaseGlyph: unbuilder.unbuildPaint(rec.Paint)
|
||||||
for rec in baseGlyphV1List.BaseGlyphV1Record
|
for rec in baseGlyphV1List.BaseGlyphV1Record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _unbuildVariableValue(v, ignoreVarIdx=False):
|
|
||||||
return v.value if ignoreVarIdx else (v.value, v.varIdx)
|
|
||||||
|
|
||||||
|
|
||||||
def unbuildColorStop(colorStop, ignoreVarIdx=False):
|
|
||||||
return {
|
|
||||||
"offset": _unbuildVariableValue(
|
|
||||||
colorStop.StopOffset, ignoreVarIdx=ignoreVarIdx
|
|
||||||
),
|
|
||||||
"paletteIndex": colorStop.Color.PaletteIndex,
|
|
||||||
"alpha": _unbuildVariableValue(
|
|
||||||
colorStop.Color.Alpha, ignoreVarIdx=ignoreVarIdx
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def unbuildColorLine(colorLine, ignoreVarIdx=False):
|
|
||||||
return {
|
|
||||||
"stops": [
|
|
||||||
unbuildColorStop(stop, ignoreVarIdx=ignoreVarIdx)
|
|
||||||
for stop in colorLine.ColorStop
|
|
||||||
],
|
|
||||||
"extend": colorLine.Extend.name.lower(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def unbuildAffine2x3(transform, ignoreVarIdx=False):
|
|
||||||
return tuple(
|
|
||||||
_unbuildVariableValue(getattr(transform, attr), ignoreVarIdx=ignoreVarIdx)
|
|
||||||
for attr in ("xx", "yx", "xy", "yy", "dx", "dy")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _flatten(lst):
|
def _flatten(lst):
|
||||||
for el in lst:
|
for el in lst:
|
||||||
if isinstance(el, list):
|
if isinstance(el, list):
|
||||||
@ -51,142 +19,40 @@ def _flatten(lst):
|
|||||||
|
|
||||||
|
|
||||||
class LayerV1ListUnbuilder:
|
class LayerV1ListUnbuilder:
|
||||||
def __init__(self, layers, ignoreVarIdx=False):
|
def __init__(self, layers):
|
||||||
self.layers = layers
|
self.layers = layers
|
||||||
self.ignoreVarIdx = ignoreVarIdx
|
|
||||||
|
callbacks = {
|
||||||
|
(
|
||||||
|
ot.Paint,
|
||||||
|
ot.PaintFormat.PaintColrLayers,
|
||||||
|
): self._unbuildPaintColrLayers,
|
||||||
|
}
|
||||||
|
self.tableUnbuilder = TableUnbuilder(callbacks)
|
||||||
|
|
||||||
def unbuildPaint(self, paint):
|
def unbuildPaint(self, paint):
|
||||||
try:
|
assert isinstance(paint, ot.Paint)
|
||||||
return self._unbuildFunctions[paint.Format](self, paint)
|
return self.tableUnbuilder.unbuild(paint)
|
||||||
except KeyError:
|
|
||||||
raise ValueError(f"Unrecognized paint format: {paint.Format}")
|
|
||||||
|
|
||||||
def unbuildVariableValue(self, value):
|
def _unbuildPaintColrLayers(self, source):
|
||||||
return _unbuildVariableValue(value, ignoreVarIdx=self.ignoreVarIdx)
|
assert source["Format"] == ot.PaintFormat.PaintColrLayers
|
||||||
|
|
||||||
def unbuildPaintColrLayers(self, paint):
|
layers = list(
|
||||||
return list(
|
|
||||||
_flatten(
|
_flatten(
|
||||||
[
|
[
|
||||||
self.unbuildPaint(childPaint)
|
self.unbuildPaint(childPaint)
|
||||||
for childPaint in self.layers[
|
for childPaint in self.layers[
|
||||||
paint.FirstLayerIndex : paint.FirstLayerIndex + paint.NumLayers
|
source["FirstLayerIndex"] : source["FirstLayerIndex"]
|
||||||
|
+ source["NumLayers"]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def unbuildPaintSolid(self, paint):
|
if len(layers) == 1:
|
||||||
return {
|
return layers[0]
|
||||||
"format": int(paint.Format),
|
|
||||||
"paletteIndex": paint.Color.PaletteIndex,
|
|
||||||
"alpha": self.unbuildVariableValue(paint.Color.Alpha),
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintLinearGradient(self, paint):
|
return {"Format": source["Format"], "Layers": layers}
|
||||||
p0 = (self.unbuildVariableValue(paint.x0), self.unbuildVariableValue(paint.y0))
|
|
||||||
p1 = (self.unbuildVariableValue(paint.x1), self.unbuildVariableValue(paint.y1))
|
|
||||||
p2 = (self.unbuildVariableValue(paint.x2), self.unbuildVariableValue(paint.y2))
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"colorLine": unbuildColorLine(
|
|
||||||
paint.ColorLine, ignoreVarIdx=self.ignoreVarIdx
|
|
||||||
),
|
|
||||||
"p0": p0,
|
|
||||||
"p1": p1,
|
|
||||||
"p2": p2,
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintRadialGradient(self, paint):
|
|
||||||
c0 = (self.unbuildVariableValue(paint.x0), self.unbuildVariableValue(paint.y0))
|
|
||||||
r0 = self.unbuildVariableValue(paint.r0)
|
|
||||||
c1 = (self.unbuildVariableValue(paint.x1), self.unbuildVariableValue(paint.y1))
|
|
||||||
r1 = self.unbuildVariableValue(paint.r1)
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"colorLine": unbuildColorLine(
|
|
||||||
paint.ColorLine, ignoreVarIdx=self.ignoreVarIdx
|
|
||||||
),
|
|
||||||
"c0": c0,
|
|
||||||
"r0": r0,
|
|
||||||
"c1": c1,
|
|
||||||
"r1": r1,
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintSweepGradient(self, paint):
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"colorLine": unbuildColorLine(
|
|
||||||
paint.ColorLine, ignoreVarIdx=self.ignoreVarIdx
|
|
||||||
),
|
|
||||||
"centerX": self.unbuildVariableValue(paint.centerX),
|
|
||||||
"centerY": self.unbuildVariableValue(paint.centerY),
|
|
||||||
"startAngle": self.unbuildVariableValue(paint.startAngle),
|
|
||||||
"endAngle": self.unbuildVariableValue(paint.endAngle),
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintGlyph(self, paint):
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"glyph": paint.Glyph,
|
|
||||||
"paint": self.unbuildPaint(paint.Paint),
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintColrGlyph(self, paint):
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"glyph": paint.Glyph,
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintTransform(self, paint):
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"transform": unbuildAffine2x3(
|
|
||||||
paint.Transform, ignoreVarIdx=self.ignoreVarIdx
|
|
||||||
),
|
|
||||||
"paint": self.unbuildPaint(paint.Paint),
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintTranslate(self, paint):
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"dx": self.unbuildVariableValue(paint.dx),
|
|
||||||
"dy": self.unbuildVariableValue(paint.dy),
|
|
||||||
"paint": self.unbuildPaint(paint.Paint),
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintRotate(self, paint):
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"angle": self.unbuildVariableValue(paint.angle),
|
|
||||||
"centerX": self.unbuildVariableValue(paint.centerX),
|
|
||||||
"centerY": self.unbuildVariableValue(paint.centerY),
|
|
||||||
"paint": self.unbuildPaint(paint.Paint),
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintSkew(self, paint):
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"xSkewAngle": self.unbuildVariableValue(paint.xSkewAngle),
|
|
||||||
"ySkewAngle": self.unbuildVariableValue(paint.ySkewAngle),
|
|
||||||
"centerX": self.unbuildVariableValue(paint.centerX),
|
|
||||||
"centerY": self.unbuildVariableValue(paint.centerY),
|
|
||||||
"paint": self.unbuildPaint(paint.Paint),
|
|
||||||
}
|
|
||||||
|
|
||||||
def unbuildPaintComposite(self, paint):
|
|
||||||
return {
|
|
||||||
"format": int(paint.Format),
|
|
||||||
"mode": paint.CompositeMode.name.lower(),
|
|
||||||
"source": self.unbuildPaint(paint.SourcePaint),
|
|
||||||
"backdrop": self.unbuildPaint(paint.BackdropPaint),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
LayerV1ListUnbuilder._unbuildFunctions = {
|
|
||||||
pf.value: getattr(LayerV1ListUnbuilder, "unbuild" + pf.name)
|
|
||||||
for pf in ot.PaintFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -1588,6 +1588,15 @@ otData = [
|
|||||||
('LOffset', 'Paint', 'LayerCount', 0, 'Array of offsets to Paint tables, from the start of the LayerV1List table.'),
|
('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', [
|
('Affine2x3', [
|
||||||
('VarFixed', 'xx', None, None, 'x-part of x basis vector'),
|
('VarFixed', 'xx', None, None, 'x-part of x basis vector'),
|
||||||
('VarFixed', 'yx', None, None, 'y-part of x basis vector'),
|
('VarFixed', 'yx', None, None, 'y-part of x basis vector'),
|
||||||
|
File diff suppressed because it is too large
Load Diff
15
Tests/colorLib/table_builder_test.py
Normal file
15
Tests/colorLib/table_builder_test.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from fontTools.ttLib.tables import otTables # trigger setup to occur
|
||||||
|
from fontTools.ttLib.tables.otConverters import UShort
|
||||||
|
from fontTools.colorLib.table_builder import TableBuilder
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
class WriteMe:
|
||||||
|
value = None
|
||||||
|
|
||||||
|
|
||||||
|
def test_intValue_otRound():
|
||||||
|
dest = WriteMe()
|
||||||
|
converter = UShort("value", None, None)
|
||||||
|
TableBuilder()._convert(dest, "value", converter, 85.6)
|
||||||
|
assert dest.value == 86, "Should have used otRound"
|
@ -5,155 +5,206 @@ import pytest
|
|||||||
|
|
||||||
|
|
||||||
TEST_COLOR_GLYPHS = {
|
TEST_COLOR_GLYPHS = {
|
||||||
"glyph00010": [
|
"glyph00010": {
|
||||||
{
|
"Format": int(ot.PaintFormat.PaintColrLayers),
|
||||||
"format": int(ot.PaintFormat.PaintGlyph),
|
"Layers": [
|
||||||
"glyph": "glyph00011",
|
{
|
||||||
"paint": {
|
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||||
"format": int(ot.PaintFormat.PaintSolid),
|
"Paint": {
|
||||||
"paletteIndex": 2,
|
"Format": int(ot.PaintFormat.PaintSolid),
|
||||||
"alpha": 0.5,
|
"Color": {"PaletteIndex": 2, "Alpha": (0.5, 0)},
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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",
|
|
||||||
},
|
},
|
||||||
"p0": (1, 2),
|
"Glyph": "glyph00011",
|
||||||
"p1": (-3, -4),
|
|
||||||
"p2": (5, 6),
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||||
"format": int(ot.PaintFormat.PaintGlyph),
|
"Paint": {
|
||||||
"glyph": "glyph00013",
|
"Format": int(ot.PaintFormat.PaintLinearGradient),
|
||||||
"paint": {
|
"ColorLine": {
|
||||||
"format": int(ot.PaintFormat.PaintTransform),
|
"Extend": "repeat",
|
||||||
"transform": (-13.0, 14.0, 15.0, -17.0, 18.0, 19.0),
|
"ColorStop": [
|
||||||
"paint": {
|
|
||||||
"format": int(ot.PaintFormat.PaintRadialGradient),
|
|
||||||
"colorLine": {
|
|
||||||
"stops": [
|
|
||||||
{"offset": 0.0, "paletteIndex": 6, "alpha": 1.0},
|
|
||||||
{
|
{
|
||||||
"offset": 1.0,
|
"StopOffset": (0.0, 0),
|
||||||
"paletteIndex": 7,
|
"Color": {"PaletteIndex": 3, "Alpha": (1.0, 0)},
|
||||||
"alpha": 0.4,
|
},
|
||||||
|
{
|
||||||
|
"StopOffset": (0.5, 0),
|
||||||
|
"Color": {"PaletteIndex": 4, "Alpha": (1.0, 0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StopOffset": (1.0, 0),
|
||||||
|
"Color": {"PaletteIndex": 5, "Alpha": (1.0, 0)},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"extend": "pad",
|
|
||||||
},
|
},
|
||||||
"c0": (7, 8),
|
"x0": (1, 0),
|
||||||
"r0": 9,
|
"y0": (2, 0),
|
||||||
"c1": (10, 11),
|
"x1": (-3, 0),
|
||||||
"r1": 12,
|
"y1": (-4, 0),
|
||||||
|
"x2": (5, 0),
|
||||||
|
"y2": (6, 0),
|
||||||
},
|
},
|
||||||
|
"Glyph": "glyph00012",
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||||
"format": int(ot.PaintFormat.PaintTranslate),
|
"Paint": {
|
||||||
"dx": 257.0,
|
"Format": int(ot.PaintFormat.PaintTransform),
|
||||||
"dy": 258.0,
|
"Paint": {
|
||||||
"paint": {
|
"Format": int(ot.PaintFormat.PaintRadialGradient),
|
||||||
"format": int(ot.PaintFormat.PaintRotate),
|
"ColorLine": {
|
||||||
"angle": 45.0,
|
"Extend": "pad",
|
||||||
"centerX": 255.0,
|
"ColorStop": [
|
||||||
"centerY": 256.0,
|
{
|
||||||
"paint": {
|
"StopOffset": (0.0, 0),
|
||||||
"format": int(ot.PaintFormat.PaintSkew),
|
"Color": {"PaletteIndex": 6, "Alpha": (1.0, 0)},
|
||||||
"xSkewAngle": -11.0,
|
},
|
||||||
"ySkewAngle": 5.0,
|
{
|
||||||
"centerX": 253.0,
|
"StopOffset": (1.0, 0),
|
||||||
"centerY": 254.0,
|
"Color": {"PaletteIndex": 7, "Alpha": (0.4, 0)},
|
||||||
"paint": {
|
},
|
||||||
"format": int(ot.PaintFormat.PaintGlyph),
|
],
|
||||||
"glyph": "glyph00011",
|
|
||||||
"paint": {
|
|
||||||
"format": int(ot.PaintFormat.PaintSolid),
|
|
||||||
"paletteIndex": 2,
|
|
||||||
"alpha": 0.5,
|
|
||||||
},
|
},
|
||||||
|
"x0": (7, 0),
|
||||||
|
"y0": (8, 0),
|
||||||
|
"r0": (9, 0),
|
||||||
|
"x1": (10, 0),
|
||||||
|
"y1": (11, 0),
|
||||||
|
"r1": (12, 0),
|
||||||
|
},
|
||||||
|
"Transform": {
|
||||||
|
"xx": (-13.0, 0),
|
||||||
|
"yx": (14.0, 0),
|
||||||
|
"xy": (15.0, 0),
|
||||||
|
"yy": (-17.0, 0),
|
||||||
|
"dx": (18.0, 0),
|
||||||
|
"dy": (19.0, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"Glyph": "glyph00013",
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
],
|
"Format": int(ot.PaintFormat.PaintTranslate),
|
||||||
|
"Paint": {
|
||||||
|
"Format": int(ot.PaintFormat.PaintRotate),
|
||||||
|
"Paint": {
|
||||||
|
"Format": int(ot.PaintFormat.PaintSkew),
|
||||||
|
"Paint": {
|
||||||
|
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||||
|
"Paint": {
|
||||||
|
"Format": int(ot.PaintFormat.PaintSolid),
|
||||||
|
"Color": {"PaletteIndex": 2, "Alpha": (0.5, 0)},
|
||||||
|
},
|
||||||
|
"Glyph": "glyph00011",
|
||||||
|
},
|
||||||
|
"xSkewAngle": (-11.0, 0),
|
||||||
|
"ySkewAngle": (5.0, 0),
|
||||||
|
"centerX": (253.0, 0),
|
||||||
|
"centerY": (254.0, 0),
|
||||||
|
},
|
||||||
|
"angle": (45.0, 0),
|
||||||
|
"centerX": (255.0, 0),
|
||||||
|
"centerY": (256.0, 0),
|
||||||
|
},
|
||||||
|
"dx": (257.0, 0),
|
||||||
|
"dy": (258.0, 0),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
"glyph00014": {
|
"glyph00014": {
|
||||||
"format": int(ot.PaintFormat.PaintComposite),
|
"Format": int(ot.PaintFormat.PaintComposite),
|
||||||
"mode": "src_over",
|
"SourcePaint": {
|
||||||
"source": {
|
"Format": int(ot.PaintFormat.PaintColrGlyph),
|
||||||
"format": int(ot.PaintFormat.PaintColrGlyph),
|
"Glyph": "glyph00010",
|
||||||
"glyph": "glyph00010",
|
|
||||||
},
|
},
|
||||||
"backdrop": {
|
"CompositeMode": "src_over",
|
||||||
"format": int(ot.PaintFormat.PaintTransform),
|
"BackdropPaint": {
|
||||||
"transform": (1.0, 0.0, 0.0, 1.0, 300.0, 0.0),
|
"Format": int(ot.PaintFormat.PaintTransform),
|
||||||
"paint": {
|
"Paint": {
|
||||||
"format": int(ot.PaintFormat.PaintColrGlyph),
|
"Format": int(ot.PaintFormat.PaintColrGlyph),
|
||||||
"glyph": "glyph00010",
|
"Glyph": "glyph00010",
|
||||||
|
},
|
||||||
|
"Transform": {
|
||||||
|
"xx": (1.0, 0),
|
||||||
|
"yx": (0.0, 0),
|
||||||
|
"xy": (0.0, 0),
|
||||||
|
"yy": (1.0, 0),
|
||||||
|
"dx": (300.0, 0),
|
||||||
|
"dy": (0.0, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"glyph00015": {
|
"glyph00015": {
|
||||||
"format": int(ot.PaintFormat.PaintGlyph),
|
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||||
"glyph": "glyph00011",
|
"Paint": {
|
||||||
"paint": {
|
"Format": int(ot.PaintFormat.PaintSweepGradient),
|
||||||
"format": int(ot.PaintFormat.PaintSweepGradient),
|
"ColorLine": {
|
||||||
"colorLine": {
|
"Extend": "pad",
|
||||||
"stops": [
|
"ColorStop": [
|
||||||
{"offset": 0.0, "paletteIndex": 3, "alpha": 1.0},
|
{
|
||||||
{"offset": 1.0, "paletteIndex": 5, "alpha": 1.0},
|
"StopOffset": (0.0, 0),
|
||||||
|
"Color": {"PaletteIndex": 3, "Alpha": (1.0, 0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StopOffset": (1.0, 0),
|
||||||
|
"Color": {"PaletteIndex": 5, "Alpha": (1.0, 0)},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"extend": "pad",
|
|
||||||
},
|
},
|
||||||
"centerX": 259,
|
"centerX": (259, 0),
|
||||||
"centerY": 300,
|
"centerY": (300, 0),
|
||||||
"startAngle": 45.0,
|
"startAngle": (45.0, 0),
|
||||||
"endAngle": 135.0,
|
"endAngle": (135.0, 0),
|
||||||
},
|
},
|
||||||
|
"Glyph": "glyph00011",
|
||||||
},
|
},
|
||||||
"glyph00016": [
|
"glyph00016": {
|
||||||
{
|
"Format": int(ot.PaintFormat.PaintColrLayers),
|
||||||
"format": int(ot.PaintFormat.PaintGlyph),
|
"Layers": [
|
||||||
"glyph": "glyph00011",
|
{
|
||||||
"paint": {
|
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||||
"format": int(ot.PaintFormat.PaintSolid),
|
"Paint": {
|
||||||
"paletteIndex": 2,
|
"Format": int(ot.PaintFormat.PaintSolid),
|
||||||
"alpha": 0.5,
|
"Color": {"PaletteIndex": 2, "Alpha": (0.5, 0)},
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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",
|
|
||||||
},
|
},
|
||||||
"p0": (1, 2),
|
"Glyph": "glyph00011",
|
||||||
"p1": (-3, -4),
|
|
||||||
"p2": (5, 6),
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
],
|
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||||
|
"Paint": {
|
||||||
|
"Format": int(ot.PaintFormat.PaintLinearGradient),
|
||||||
|
"ColorLine": {
|
||||||
|
"Extend": "repeat",
|
||||||
|
"ColorStop": [
|
||||||
|
{
|
||||||
|
"StopOffset": (0.0, 0),
|
||||||
|
"Color": {"PaletteIndex": 3, "Alpha": (1.0, 0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StopOffset": (0.5, 0),
|
||||||
|
"Color": {"PaletteIndex": 4, "Alpha": (1.0, 0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StopOffset": (1.0, 0),
|
||||||
|
"Color": {"PaletteIndex": 5, "Alpha": (1.0, 0)},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"x0": (1, 0),
|
||||||
|
"y0": (2, 0),
|
||||||
|
"x1": (-3, 0),
|
||||||
|
"y1": (-4, 0),
|
||||||
|
"x2": (5, 0),
|
||||||
|
"y2": (6, 0),
|
||||||
|
},
|
||||||
|
"Glyph": "glyph00012",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_unbuildColrV1():
|
def test_unbuildColrV1():
|
||||||
layersV1, baseGlyphsV1 = buildColrV1(TEST_COLOR_GLYPHS)
|
layersV1, baseGlyphsV1 = buildColrV1(TEST_COLOR_GLYPHS)
|
||||||
colorGlyphs = unbuildColrV1(layersV1, baseGlyphsV1, ignoreVarIdx=True)
|
colorGlyphs = unbuildColrV1(layersV1, baseGlyphsV1)
|
||||||
assert colorGlyphs == TEST_COLOR_GLYPHS
|
assert colorGlyphs == TEST_COLOR_GLYPHS
|
||||||
|
Loading…
x
Reference in New Issue
Block a user