aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2021-02-18 21:43:15 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-02-18 21:43:15 +0000
commitd3bdc4415f3873b4274a18e9926932a2bf40bcc4 (patch)
tree09b69c7100162ccdce54cb395de01a0251e6bbf6
parent4311ba4646c0e170ff77288bebc128ba8a21b545 (diff)
parent013c93c250a3e51b4e5e563f4a842bb292b6c6cc (diff)
downloadfonttools-d3bdc4415f3873b4274a18e9926932a2bf40bcc4.tar.gz
Upgrade fonttools to 4.20.0 am: 20c690aa8c am: 6e1560f5dc am: d47a0690f7 am: 013c93c250
Original change: https://android-review.googlesource.com/c/platform/external/fonttools/+/1592900 MUST ONLY BE SUBMITTED BY AUTOMERGER Change-Id: I187cc41b13bc90128f51835860d287bafa1413d6
-rw-r--r--Doc/source/ttLib/ttFont.rst3
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/colorLib/builder.py497
-rw-r--r--Lib/fontTools/colorLib/table_builder.py234
-rw-r--r--Lib/fontTools/colorLib/unbuilder.py79
-rw-r--r--Lib/fontTools/feaLib/ast.py18
-rw-r--r--Lib/fontTools/feaLib/parser.py27
-rw-r--r--Lib/fontTools/fontBuilder.py480
-rw-r--r--Lib/fontTools/misc/arrayTools.py4
-rw-r--r--Lib/fontTools/pens/ttGlyphPen.py39
-rw-r--r--Lib/fontTools/subset/__init__.py115
-rw-r--r--Lib/fontTools/ttLib/tables/C_O_L_R_.py16
-rw-r--r--Lib/fontTools/ttLib/tables/otConverters.py16
-rwxr-xr-xLib/fontTools/ttLib/tables/otData.py161
-rw-r--r--Lib/fontTools/ttLib/tables/otTables.py72
-rw-r--r--Lib/fontTools/ttLib/ttFont.py16
-rw-r--r--METADATA11
-rw-r--r--NEWS.rst19
-rw-r--r--Tests/colorLib/builder_test.py1240
-rw-r--r--Tests/colorLib/table_builder_test.py15
-rw-r--r--Tests/colorLib/unbuilder_test.py210
-rw-r--r--Tests/feaLib/builder_test.py2
-rw-r--r--Tests/feaLib/data/delete_glyph.fea3
-rw-r--r--Tests/feaLib/data/delete_glyph.ttx43
-rw-r--r--Tests/fontBuilder/data/test_var.otf.ttx10
-rw-r--r--Tests/fontBuilder/data/test_var.ttf.ttx22
-rw-r--r--Tests/misc/arrayTools_test.py13
-rw-r--r--Tests/subset/subset_test.py253
-rw-r--r--Tests/ttLib/tables/C_O_L_R_test.py243
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py2
31 files changed, 2728 insertions, 1139 deletions
diff --git a/Doc/source/ttLib/ttFont.rst b/Doc/source/ttLib/ttFont.rst
index 77882cd3..a571050c 100644
--- a/Doc/source/ttLib/ttFont.rst
+++ b/Doc/source/ttLib/ttFont.rst
@@ -5,4 +5,5 @@ ttFont
.. automodule:: fontTools.ttLib.ttFont
:inherited-members:
:members:
- :undoc-members: \ No newline at end of file
+ :undoc-members:
+ :private-members:
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 16ee8a4e..19040e48 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -4,6 +4,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "4.19.1"
+version = __version__ = "4.20.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py
index 998ab60d..821244af 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,87 @@ _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)
+_MAX_REUSE_LEN = 32
+
+
+def _beforeBuildPaintVarRadialGradient(paint, source, srcMapFn=lambda v: v):
+ # 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"] = srcMapFn(x0)
+ source["y0"] = srcMapFn(y0)
+ source["r0"] = srcMapFn(r0)
+ source["x1"] = srcMapFn(x1)
+ source["y1"] = srcMapFn(y1)
+ source["r1"] = srcMapFn(r1)
+
+ return paint, source
+
+
+def _beforeBuildPaintRadialGradient(paint, source):
+ return _beforeBuildPaintVarRadialGradient(paint, source, lambda v: v.value)
+
+
+def _defaultColorIndex():
+ colorIndex = ot.ColorIndex()
+ colorIndex.Alpha = _DEFAULT_ALPHA.value
+ return colorIndex
+
+
+def _defaultVarColorIndex():
+ colorIndex = ot.VarColorIndex()
+ colorIndex.Alpha = _DEFAULT_ALPHA
+ return colorIndex
+
+
+def _defaultColorLine():
+ colorLine = ot.ColorLine()
+ colorLine.Extend = ExtendMode.PAD
+ return colorLine
+
+
+def _defaultVarColorLine():
+ colorLine = ot.VarColorLine()
+ colorLine.Extend = ExtendMode.PAD
+ return colorLine
+
+
+def _buildPaintCallbacks():
+ return {
+ (
+ BuildCallback.BEFORE_BUILD,
+ ot.Paint,
+ ot.PaintFormat.PaintRadialGradient,
+ ): _beforeBuildPaintRadialGradient,
+ (
+ BuildCallback.BEFORE_BUILD,
+ ot.Paint,
+ ot.PaintFormat.PaintVarRadialGradient,
+ ): _beforeBuildPaintVarRadialGradient,
+ (BuildCallback.CREATE_DEFAULT, ot.ColorIndex): _defaultColorIndex,
+ (BuildCallback.CREATE_DEFAULT, ot.VarColorIndex): _defaultVarColorIndex,
+ (BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine,
+ (BuildCallback.CREATE_DEFAULT, ot.VarColorLine): _defaultVarColorLine,
+ }
def populateCOLRv0(
@@ -112,7 +182,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 +193,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.
"""
@@ -161,7 +229,7 @@ def buildCOLR(
self.version = colr.Version = version
if version == 0:
- self._fromOTTable(colr)
+ self.ColorLayers = self._decompileColorLayersV0(colr)
else:
colr.VarStore = varStore
self.table = colr
@@ -295,8 +363,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,124 +394,15 @@ 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):
- # TODO may want a max length to limit scope of search
# Reuse of very large #s of layers is relatively unlikely
# +2: we want sequences of at least 2
# 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)
@@ -463,6 +420,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,217 +456,87 @@ 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.Paint.Format.PaintSolid)
- ot_paint.Color = buildColorIndex(paletteIndex, alpha)
- return ot_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.Paint.Format.PaintLinearGradient)
- ot_paint.ColorLine = _to_color_line(colorLine)
-
- 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.Paint.Format.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 buildPaintGlyph(self, glyph: str, paint: _PaintInput) -> ot.Paint:
- ot_paint = ot.Paint()
- ot_paint.Format = int(ot.Paint.Format.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.Paint.Format.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.Paint.Format.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.Paint.Format.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.Paint.Format.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.Paint.Format.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.Paint.Format.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.Paint.Format.PaintColrLayers)
- self.slices.append(ot_paint)
-
- paints = [
- self.buildPaint(p)
- for p in _build_n_ary_tree(paints, n=MAX_PAINT_COLR_LAYER_COUNT)
- ]
+ # 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)
+
+ # 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"]
+
+ # Convert maps seqs or whatever into typed objects
+ layers = [self.buildPaint(l) for l in layers]
+
+ # No reason to have a colr layers with just one entry
+ if len(layers) == 1:
+ return layers[0], {}
# 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
new_slice = ot.Paint()
- new_slice.Format = int(ot.Paint.Format.PaintColrLayers)
+ 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
+ is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
+ 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
- # 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
- )
+ 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
+ )
- 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()
@@ -707,31 +545,6 @@ class LayerV1ListBuilder:
return layers
-LayerV1ListBuilder._buildFunctions = {
- pf.value: getattr(LayerV1ListBuilder, "build" + pf.name)
- for pf in ot.Paint.Format
- if pf != ot.Paint.Format.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 00000000..18e2de18
--- /dev/null
+++ b/Lib/fontTools/colorLib/table_builder.py
@@ -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
diff --git a/Lib/fontTools/colorLib/unbuilder.py b/Lib/fontTools/colorLib/unbuilder.py
new file mode 100644
index 00000000..43582bde
--- /dev/null
+++ b/Lib/fontTools/colorLib/unbuilder.py
@@ -0,0 +1,79 @@
+from fontTools.ttLib.tables import otTables as ot
+from .table_builder import TableUnbuilder
+
+
+def unbuildColrV1(layerV1List, baseGlyphV1List):
+ unbuilder = LayerV1ListUnbuilder(layerV1List.Paint)
+ return {
+ rec.BaseGlyph: unbuilder.unbuildPaint(rec.Paint)
+ for rec in baseGlyphV1List.BaseGlyphV1Record
+ }
+
+
+def _flatten(lst):
+ for el in lst:
+ if isinstance(el, list):
+ yield from _flatten(el)
+ else:
+ yield el
+
+
+class LayerV1ListUnbuilder:
+ def __init__(self, layers):
+ self.layers = layers
+
+ callbacks = {
+ (
+ ot.Paint,
+ ot.PaintFormat.PaintColrLayers,
+ ): self._unbuildPaintColrLayers,
+ }
+ self.tableUnbuilder = TableUnbuilder(callbacks)
+
+ def unbuildPaint(self, paint):
+ assert isinstance(paint, ot.Paint)
+ return self.tableUnbuilder.unbuild(paint)
+
+ def _unbuildPaintColrLayers(self, source):
+ assert source["Format"] == ot.PaintFormat.PaintColrLayers
+
+ layers = list(
+ _flatten(
+ [
+ self.unbuildPaint(childPaint)
+ for childPaint in self.layers[
+ source["FirstLayerIndex"] : source["FirstLayerIndex"]
+ + source["NumLayers"]
+ ]
+ ]
+ )
+ )
+
+ if len(layers) == 1:
+ return layers[0]
+
+ return {"Format": source["Format"], "Layers": layers}
+
+
+if __name__ == "__main__":
+ from pprint import pprint
+ import sys
+ from fontTools.ttLib import TTFont
+
+ try:
+ fontfile = sys.argv[1]
+ except IndexError:
+ sys.exit("usage: fonttools colorLib.unbuilder FONTFILE")
+
+ font = TTFont(fontfile)
+ colr = font["COLR"]
+ if colr.version < 1:
+ sys.exit(f"error: No COLR table version=1 found in {fontfile}")
+
+ colorGlyphs = unbuildColrV1(
+ colr.table.LayerV1List,
+ colr.table.BaseGlyphV1List,
+ ignoreVarIdx=not colr.table.VarStore,
+ )
+
+ pprint(colorGlyphs)
diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index 7ef9afd9..6c2bfce8 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -188,6 +188,21 @@ class Comment(Element):
return self.text
+class NullGlyph(Expression):
+ """The NULL glyph, used in glyph deletion substitutions."""
+
+ def __init__(self, location=None):
+ Expression.__init__(self, location)
+ #: The name itself as a string
+
+ def glyphSet(self):
+ """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
+ return ()
+
+ def asFea(self, indent=""):
+ return "NULL"
+
+
class GlyphName(Expression):
"""A single glyph name, such as ``cedilla``."""
@@ -1246,8 +1261,9 @@ class MultipleSubstStatement(Statement):
res += " " + " ".join(map(asFea, self.suffix))
else:
res += asFea(self.glyph)
+ replacement = self.replacement or [ NullGlyph() ]
res += " by "
- res += " ".join(map(asFea, self.replacement))
+ res += " ".join(map(asFea, replacement))
res += ";"
return res
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index 7439fbf3..23a49618 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -314,10 +314,15 @@ class Parser(object):
location,
)
- def parse_glyphclass_(self, accept_glyphname):
+ def parse_glyphclass_(self, accept_glyphname, accept_null=False):
# Parses a glyph class, either named or anonymous, or (if
- # ``bool(accept_glyphname)``) a glyph name.
+ # ``bool(accept_glyphname)``) a glyph name. If ``bool(accept_null)`` then
+ # also accept the special NULL glyph.
if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID):
+ if accept_null and self.next_token_ == "NULL":
+ # If you want a glyph called NULL, you should escape it.
+ self.advance_lexer_()
+ return self.ast.NullGlyph(location=self.cur_token_location_)
glyph = self.expect_glyph_()
self.check_glyph_name_in_glyph_set(glyph)
return self.ast.GlyphName(glyph, location=self.cur_token_location_)
@@ -375,7 +380,8 @@ class Parser(object):
self.expect_symbol_("-")
range_end = self.expect_cid_()
self.check_glyph_name_in_glyph_set(
- f"cid{range_start:05d}", f"cid{range_end:05d}",
+ f"cid{range_start:05d}",
+ f"cid{range_end:05d}",
)
glyphs.add_cid_range(
range_start,
@@ -804,7 +810,7 @@ class Parser(object):
if self.next_token_ == "by":
keyword = self.expect_keyword_("by")
while self.next_token_ != ";":
- gc = self.parse_glyphclass_(accept_glyphname=True)
+ gc = self.parse_glyphclass_(accept_glyphname=True, accept_null=True)
new.append(gc)
elif self.next_token_ == "from":
keyword = self.expect_keyword_("from")
@@ -837,6 +843,11 @@ class Parser(object):
num_lookups = len([l for l in lookups if l is not None])
+ is_deletion = False
+ if len(new) == 1 and len(new[0].glyphSet()) == 0:
+ new = [] # Deletion
+ is_deletion = True
+
# GSUB lookup type 1: Single substitution.
# Format A: "substitute a by a.sc;"
# Format B: "substitute [one.fitted one.oldstyle] by one;"
@@ -863,8 +874,10 @@ class Parser(object):
not reverse
and len(old) == 1
and len(old[0].glyphSet()) == 1
- and len(new) > 1
- and max([len(n.glyphSet()) for n in new]) == 1
+ and (
+ (len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1)
+ or len(new) == 0
+ )
and num_lookups == 0
):
return self.ast.MultipleSubstStatement(
@@ -936,7 +949,7 @@ class Parser(object):
)
# If there are remaining glyphs to parse, this is an invalid GSUB statement
- if len(new) != 0:
+ if len(new) != 0 or is_deletion:
raise FeatureLibError("Invalid substitution statement", location)
# GSUB lookup type 6: Chaining contextual substitution.
diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py
index f3fe92a8..f4c943f3 100644
--- a/Lib/fontTools/fontBuilder.py
+++ b/Lib/fontTools/fontBuilder.py
@@ -1,4 +1,3 @@
-
__all__ = ["FontBuilder"]
"""
@@ -136,192 +135,192 @@ from .ttLib.tables._c_m_a_p import cmap_classes
from .ttLib.tables._n_a_m_e import NameRecord, makeName
from .misc.timeTools import timestampNow
import struct
+from collections import OrderedDict
_headDefaults = dict(
- tableVersion = 1.0,
- fontRevision = 1.0,
- checkSumAdjustment = 0,
- magicNumber = 0x5F0F3CF5,
- flags = 0x0003,
- unitsPerEm = 1000,
- created = 0,
- modified = 0,
- xMin = 0,
- yMin = 0,
- xMax = 0,
- yMax = 0,
- macStyle = 0,
- lowestRecPPEM = 3,
- fontDirectionHint = 2,
- indexToLocFormat = 0,
- glyphDataFormat = 0,
+ tableVersion=1.0,
+ fontRevision=1.0,
+ checkSumAdjustment=0,
+ magicNumber=0x5F0F3CF5,
+ flags=0x0003,
+ unitsPerEm=1000,
+ created=0,
+ modified=0,
+ xMin=0,
+ yMin=0,
+ xMax=0,
+ yMax=0,
+ macStyle=0,
+ lowestRecPPEM=3,
+ fontDirectionHint=2,
+ indexToLocFormat=0,
+ glyphDataFormat=0,
)
_maxpDefaultsTTF = dict(
- tableVersion = 0x00010000,
- numGlyphs = 0,
- maxPoints = 0,
- maxContours = 0,
- maxCompositePoints = 0,
- maxCompositeContours = 0,
- maxZones = 2,
- maxTwilightPoints = 0,
- maxStorage = 0,
- maxFunctionDefs = 0,
- maxInstructionDefs = 0,
- maxStackElements = 0,
- maxSizeOfInstructions = 0,
- maxComponentElements = 0,
- maxComponentDepth = 0,
+ tableVersion=0x00010000,
+ numGlyphs=0,
+ maxPoints=0,
+ maxContours=0,
+ maxCompositePoints=0,
+ maxCompositeContours=0,
+ maxZones=2,
+ maxTwilightPoints=0,
+ maxStorage=0,
+ maxFunctionDefs=0,
+ maxInstructionDefs=0,
+ maxStackElements=0,
+ maxSizeOfInstructions=0,
+ maxComponentElements=0,
+ maxComponentDepth=0,
)
_maxpDefaultsOTF = dict(
- tableVersion = 0x00005000,
- numGlyphs = 0,
+ tableVersion=0x00005000,
+ numGlyphs=0,
)
_postDefaults = dict(
- formatType = 3.0,
- italicAngle = 0,
- underlinePosition = 0,
- underlineThickness = 0,
- isFixedPitch = 0,
- minMemType42 = 0,
- maxMemType42 = 0,
- minMemType1 = 0,
- maxMemType1 = 0,
+ formatType=3.0,
+ italicAngle=0,
+ underlinePosition=0,
+ underlineThickness=0,
+ isFixedPitch=0,
+ minMemType42=0,
+ maxMemType42=0,
+ minMemType1=0,
+ maxMemType1=0,
)
_hheaDefaults = dict(
- tableVersion = 0x00010000,
- ascent = 0,
- descent = 0,
- lineGap = 0,
- advanceWidthMax = 0,
- minLeftSideBearing = 0,
- minRightSideBearing = 0,
- xMaxExtent = 0,
- caretSlopeRise = 1,
- caretSlopeRun = 0,
- caretOffset = 0,
- reserved0 = 0,
- reserved1 = 0,
- reserved2 = 0,
- reserved3 = 0,
- metricDataFormat = 0,
- numberOfHMetrics = 0,
+ tableVersion=0x00010000,
+ ascent=0,
+ descent=0,
+ lineGap=0,
+ advanceWidthMax=0,
+ minLeftSideBearing=0,
+ minRightSideBearing=0,
+ xMaxExtent=0,
+ caretSlopeRise=1,
+ caretSlopeRun=0,
+ caretOffset=0,
+ reserved0=0,
+ reserved1=0,
+ reserved2=0,
+ reserved3=0,
+ metricDataFormat=0,
+ numberOfHMetrics=0,
)
_vheaDefaults = dict(
- tableVersion = 0x00010000,
- ascent = 0,
- descent = 0,
- lineGap = 0,
- advanceHeightMax = 0,
- minTopSideBearing = 0,
- minBottomSideBearing = 0,
- yMaxExtent = 0,
- caretSlopeRise = 0,
- caretSlopeRun = 0,
- reserved0 = 0,
- reserved1 = 0,
- reserved2 = 0,
- reserved3 = 0,
- reserved4 = 0,
- metricDataFormat = 0,
- numberOfVMetrics = 0,
+ tableVersion=0x00010000,
+ ascent=0,
+ descent=0,
+ lineGap=0,
+ advanceHeightMax=0,
+ minTopSideBearing=0,
+ minBottomSideBearing=0,
+ yMaxExtent=0,
+ caretSlopeRise=0,
+ caretSlopeRun=0,
+ reserved0=0,
+ reserved1=0,
+ reserved2=0,
+ reserved3=0,
+ reserved4=0,
+ metricDataFormat=0,
+ numberOfVMetrics=0,
)
_nameIDs = dict(
- copyright = 0,
- familyName = 1,
- styleName = 2,
- uniqueFontIdentifier = 3,
- fullName = 4,
- version = 5,
- psName = 6,
- trademark = 7,
- manufacturer = 8,
- designer = 9,
- description = 10,
- vendorURL = 11,
- designerURL = 12,
- licenseDescription = 13,
- licenseInfoURL = 14,
- # reserved = 15,
- typographicFamily = 16,
- typographicSubfamily = 17,
- compatibleFullName = 18,
- sampleText = 19,
- postScriptCIDFindfontName = 20,
- wwsFamilyName = 21,
- wwsSubfamilyName = 22,
- lightBackgroundPalette = 23,
- darkBackgroundPalette = 24,
- variationsPostScriptNamePrefix = 25,
+ copyright=0,
+ familyName=1,
+ styleName=2,
+ uniqueFontIdentifier=3,
+ fullName=4,
+ version=5,
+ psName=6,
+ trademark=7,
+ manufacturer=8,
+ designer=9,
+ description=10,
+ vendorURL=11,
+ designerURL=12,
+ licenseDescription=13,
+ licenseInfoURL=14,
+ # reserved = 15,
+ typographicFamily=16,
+ typographicSubfamily=17,
+ compatibleFullName=18,
+ sampleText=19,
+ postScriptCIDFindfontName=20,
+ wwsFamilyName=21,
+ wwsSubfamilyName=22,
+ lightBackgroundPalette=23,
+ darkBackgroundPalette=24,
+ variationsPostScriptNamePrefix=25,
)
# to insert in setupNameTable doc string:
# print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1])))
_panoseDefaults = dict(
- bFamilyType = 0,
- bSerifStyle = 0,
- bWeight = 0,
- bProportion = 0,
- bContrast = 0,
- bStrokeVariation = 0,
- bArmStyle = 0,
- bLetterForm = 0,
- bMidline = 0,
- bXHeight = 0,
+ bFamilyType=0,
+ bSerifStyle=0,
+ bWeight=0,
+ bProportion=0,
+ bContrast=0,
+ bStrokeVariation=0,
+ bArmStyle=0,
+ bLetterForm=0,
+ bMidline=0,
+ bXHeight=0,
)
_OS2Defaults = dict(
- version = 3,
- xAvgCharWidth = 0,
- usWeightClass = 400,
- usWidthClass = 5,
- fsType = 0x0004, # default: Preview & Print embedding
- ySubscriptXSize = 0,
- ySubscriptYSize = 0,
- ySubscriptXOffset = 0,
- ySubscriptYOffset = 0,
- ySuperscriptXSize = 0,
- ySuperscriptYSize = 0,
- ySuperscriptXOffset = 0,
- ySuperscriptYOffset = 0,
- yStrikeoutSize = 0,
- yStrikeoutPosition = 0,
- sFamilyClass = 0,
- panose = _panoseDefaults,
- ulUnicodeRange1 = 0,
- ulUnicodeRange2 = 0,
- ulUnicodeRange3 = 0,
- ulUnicodeRange4 = 0,
- achVendID = "????",
- fsSelection = 0,
- usFirstCharIndex = 0,
- usLastCharIndex = 0,
- sTypoAscender = 0,
- sTypoDescender = 0,
- sTypoLineGap = 0,
- usWinAscent = 0,
- usWinDescent = 0,
- ulCodePageRange1 = 0,
- ulCodePageRange2 = 0,
- sxHeight = 0,
- sCapHeight = 0,
- usDefaultChar = 0, # .notdef
- usBreakChar = 32, # space
- usMaxContext = 0,
- usLowerOpticalPointSize = 0,
- usUpperOpticalPointSize = 0,
+ version=3,
+ xAvgCharWidth=0,
+ usWeightClass=400,
+ usWidthClass=5,
+ fsType=0x0004, # default: Preview & Print embedding
+ ySubscriptXSize=0,
+ ySubscriptYSize=0,
+ ySubscriptXOffset=0,
+ ySubscriptYOffset=0,
+ ySuperscriptXSize=0,
+ ySuperscriptYSize=0,
+ ySuperscriptXOffset=0,
+ ySuperscriptYOffset=0,
+ yStrikeoutSize=0,
+ yStrikeoutPosition=0,
+ sFamilyClass=0,
+ panose=_panoseDefaults,
+ ulUnicodeRange1=0,
+ ulUnicodeRange2=0,
+ ulUnicodeRange3=0,
+ ulUnicodeRange4=0,
+ achVendID="????",
+ fsSelection=0,
+ usFirstCharIndex=0,
+ usLastCharIndex=0,
+ sTypoAscender=0,
+ sTypoDescender=0,
+ sTypoLineGap=0,
+ usWinAscent=0,
+ usWinDescent=0,
+ ulCodePageRange1=0,
+ ulCodePageRange2=0,
+ sxHeight=0,
+ sCapHeight=0,
+ usDefaultChar=0, # .notdef
+ usBreakChar=32, # space
+ usMaxContext=0,
+ usLowerOpticalPointSize=0,
+ usUpperOpticalPointSize=0,
)
class FontBuilder(object):
-
def __init__(self, unitsPerEm=None, font=None, isTTF=True):
"""Initialize a FontBuilder instance.
@@ -395,7 +394,7 @@ class FontBuilder(object):
"""
subTables = []
highestUnicode = max(cmapping)
- if highestUnicode > 0xffff:
+ if highestUnicode > 0xFFFF:
cmapping_3_1 = dict((k, v) for k, v in cmapping.items() if k < 0x10000)
subTable_3_10 = buildCmapSubTable(cmapping, 12, 3, 10)
subTables.append(subTable_3_10)
@@ -408,7 +407,9 @@ class FontBuilder(object):
except struct.error:
# format 4 overflowed, fall back to format 12
if not allowFallback:
- raise ValueError("cmap format 4 subtable overflowed; sort glyph order by unicode to fix.")
+ raise ValueError(
+ "cmap format 4 subtable overflowed; sort glyph order by unicode to fix."
+ )
format = 12
subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
subTables.append(subTable_3_1)
@@ -489,17 +490,33 @@ class FontBuilder(object):
"""
if "xAvgCharWidth" not in values:
gs = self.font.getGlyphSet()
- widths = [gs[glyphName].width for glyphName in gs.keys() if gs[glyphName].width > 0]
+ widths = [
+ gs[glyphName].width
+ for glyphName in gs.keys()
+ if gs[glyphName].width > 0
+ ]
values["xAvgCharWidth"] = int(round(sum(widths) / float(len(widths))))
self._initTableWithValues("OS/2", _OS2Defaults, values)
- if not ("ulUnicodeRange1" in values or "ulUnicodeRange2" in values or
- "ulUnicodeRange3" in values or "ulUnicodeRange3" in values):
- assert "cmap" in self.font, "the 'cmap' table must be setup before the 'OS/2' table"
+ if not (
+ "ulUnicodeRange1" in values
+ or "ulUnicodeRange2" in values
+ or "ulUnicodeRange3" in values
+ or "ulUnicodeRange3" in values
+ ):
+ assert (
+ "cmap" in self.font
+ ), "the 'cmap' table must be setup before the 'OS/2' table"
self.font["OS/2"].recalcUnicodeRanges(self.font)
def setupCFF(self, psName, fontInfo, charStringsDict, privateDict):
- from .cffLib import CFFFontSet, TopDictIndex, TopDict, CharStrings, \
- GlobalSubrsIndex, PrivateDict
+ from .cffLib import (
+ CFFFontSet,
+ TopDictIndex,
+ TopDict,
+ CharStrings,
+ GlobalSubrsIndex,
+ PrivateDict,
+ )
assert not self.isTTF
self.font.sfntVersion = "OTTO"
@@ -528,7 +545,9 @@ class FontBuilder(object):
scale = 1 / self.font["head"].unitsPerEm
topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
- charStrings = CharStrings(None, topDict.charset, globalSubrs, private, fdSelect, fdArray)
+ charStrings = CharStrings(
+ None, topDict.charset, globalSubrs, private, fdSelect, fdArray
+ )
for glyphName, charString in charStringsDict.items():
charString.private = private
charString.globalSubrs = globalSubrs
@@ -541,8 +560,16 @@ class FontBuilder(object):
self.font["CFF "].cff = fontSet
def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None):
- from .cffLib import CFFFontSet, TopDictIndex, TopDict, CharStrings, \
- GlobalSubrsIndex, PrivateDict, FDArrayIndex, FontDict
+ from .cffLib import (
+ CFFFontSet,
+ TopDictIndex,
+ TopDict,
+ CharStrings,
+ GlobalSubrsIndex,
+ PrivateDict,
+ FDArrayIndex,
+ FontDict,
+ )
assert not self.isTTF
self.font.sfntVersion = "OTTO"
@@ -628,10 +655,40 @@ class FontBuilder(object):
self.calcGlyphBounds()
def setupFvar(self, axes, instances):
+ """Adds an font variations table to the font.
+
+ Args:
+ axes (list): See below.
+ instances (list): See below.
+
+ ``axes`` should be a list of axes, with each axis either supplied as
+ a py:class:`.designspaceLib.AxisDescriptor` object, or a tuple in the
+ format ```tupletag, minValue, defaultValue, maxValue, name``.
+ The ``name`` is either a string, or a dict, mapping language codes
+ to strings, to allow localized name table entries.
+
+ ```instances`` should be a list of instances, with each instance either
+ supplied as a py:class:`.designspaceLib.InstanceDescriptor` object, or a
+ dict with keys ``location`` (mapping of axis tags to float values),
+ ``stylename`` and (optionally) ``postscriptfontname``.
+ The ``stylename`` is either a string, or a dict, mapping language codes
+ to strings, to allow localized name table entries.
+ """
+
addFvar(self.font, axes, instances)
+ def setupAvar(self, axes):
+ """Adds an axis variations table to the font.
+
+ Args:
+ axes (list): A list of py:class:`.designspaceLib.AxisDescriptor` objects.
+ """
+ from .varLib import _add_avar
+
+ _add_avar(self.font, OrderedDict(enumerate(axes))) # Only values are used
+
def setupGvar(self, variations):
- gvar = self.font["gvar"] = newTable('gvar')
+ gvar = self.font["gvar"] = newTable("gvar")
gvar.version = 1
gvar.reserved = 0
gvar.variations = variations
@@ -650,7 +707,7 @@ class FontBuilder(object):
The `metrics` argument must be a dict, mapping glyph names to
`(width, leftSidebearing)` tuples.
"""
- self.setupMetrics('hmtx', metrics)
+ self.setupMetrics("hmtx", metrics)
def setupVerticalMetrics(self, metrics):
"""Create a new `vmtx` table, for horizontal metrics.
@@ -658,7 +715,7 @@ class FontBuilder(object):
The `metrics` argument must be a dict, mapping glyph names to
`(height, topSidebearing)` tuples.
"""
- self.setupMetrics('vmtx', metrics)
+ self.setupMetrics("vmtx", metrics)
def setupMetrics(self, tableTag, metrics):
"""See `setupHorizontalMetrics()` and `setupVerticalMetrics()`."""
@@ -699,8 +756,14 @@ class FontBuilder(object):
bag[vorg] = 1
else:
bag[vorg] += 1
- defaultVerticalOrigin = sorted(bag, key=lambda vorg: bag[vorg], reverse=True)[0]
- self._initTableWithValues("VORG", {}, dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin))
+ defaultVerticalOrigin = sorted(
+ bag, key=lambda vorg: bag[vorg], reverse=True
+ )[0]
+ self._initTableWithValues(
+ "VORG",
+ {},
+ dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin),
+ )
vorgTable = self.font["VORG"]
vorgTable.majorVersion = 1
vorgTable.minorVersion = 0
@@ -711,7 +774,7 @@ class FontBuilder(object):
"""Create a new `post` table and initialize it with default values,
which can be overridden by keyword arguments.
"""
- isCFF2 = 'CFF2' in self.font
+ isCFF2 = "CFF2" in self.font
postTable = self._initTableWithValues("post", _postDefaults, values)
if (self.isTTF or isCFF2) and keepGlyphNames:
postTable.formatType = 2.0
@@ -735,10 +798,10 @@ class FontBuilder(object):
happy. This does not properly sign the font.
"""
values = dict(
- ulVersion = 1,
- usFlag = 0,
- usNumSigs = 0,
- signatureRecords = [],
+ ulVersion=1,
+ usFlag=0,
+ usNumSigs=0,
+ signatureRecords=[],
)
self._initTableWithValues("DSIG", {}, values)
@@ -754,7 +817,10 @@ class FontBuilder(object):
`fontTools.feaLib` for details.
"""
from .feaLib.builder import addOpenTypeFeaturesFromString
- addOpenTypeFeaturesFromString(self.font, features, filename=filename, tables=tables)
+
+ addOpenTypeFeaturesFromString(
+ self.font, features, filename=filename, tables=tables
+ )
def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"):
"""Add conditional substitutions to a Variable Font.
@@ -770,14 +836,17 @@ class FontBuilder(object):
self.font, conditionalSubstitutions, featureTag=featureTag
)
- def setupCOLR(self, colorLayers):
+ def setupCOLR(self, colorLayers, version=None, varStore=None):
"""Build new COLR table using color layers dictionary.
Cf. `fontTools.colorLib.builder.buildCOLR`.
"""
from fontTools.colorLib.builder import buildCOLR
- self.font["COLR"] = buildCOLR(colorLayers)
+ glyphMap = self.font.getReverseGlyphMap()
+ self.font["COLR"] = buildCOLR(
+ colorLayers, version=version, glyphMap=glyphMap, varStore=varStore
+ )
def setupCPAL(
self,
@@ -800,7 +869,7 @@ class FontBuilder(object):
paletteTypes=paletteTypes,
paletteLabels=paletteLabels,
paletteEntryLabels=paletteEntryLabels,
- nameTable=self.font.get("name")
+ nameTable=self.font.get("name"),
)
def setupStat(self, axes, locations=None, elidedFallbackName=2):
@@ -810,6 +879,7 @@ class FontBuilder(object):
the arguments.
"""
from .otlLib.builder import buildStatTable
+
buildStatTable(self.font, axes, locations, elidedFallbackName)
@@ -823,32 +893,58 @@ def buildCmapSubTable(cmapping, format, platformID, platEncID):
def addFvar(font, axes, instances):
- from .misc.py23 import Tag, tounicode
from .ttLib.tables._f_v_a_r import Axis, NamedInstance
+ from .designspaceLib import AxisDescriptor
assert axes
- fvar = newTable('fvar')
- nameTable = font['name']
+ fvar = newTable("fvar")
+ nameTable = font["name"]
- for tag, minValue, defaultValue, maxValue, name in axes:
+ for axis_def in axes:
axis = Axis()
- axis.axisTag = Tag(tag)
- axis.minValue, axis.defaultValue, axis.maxValue = minValue, defaultValue, maxValue
- axis.axisNameID = nameTable.addName(tounicode(name))
+
+ if isinstance(axis_def, tuple):
+ (
+ axis.axisTag,
+ axis.minValue,
+ axis.defaultValue,
+ axis.maxValue,
+ name,
+ ) = axis_def
+ else:
+ (axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue, name) = (
+ axis_def.tag,
+ axis_def.minimum,
+ axis_def.default,
+ axis_def.maximum,
+ axis_def.name,
+ )
+
+ if isinstance(name, str):
+ name = dict(en=name)
+
+ axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font)
fvar.axes.append(axis)
for instance in instances:
- coordinates = instance['location']
- name = tounicode(instance['stylename'])
- psname = instance.get('postscriptfontname')
+ if isinstance(instance, dict):
+ coordinates = instance["location"]
+ name = instance["stylename"]
+ psname = instance.get("postscriptfontname")
+ else:
+ coordinates = instance.location
+ name = instance.localisedStyleName or instance.styleName
+ psname = instance.postScriptFontName
+
+ if isinstance(name, str):
+ name = dict(en=name)
inst = NamedInstance()
- inst.subfamilyNameID = nameTable.addName(name)
+ inst.subfamilyNameID = nameTable.addMultilingualName(name, ttFont=font)
if psname is not None:
- psname = tounicode(psname)
inst.postscriptNameID = nameTable.addName(psname)
inst.coordinates = coordinates
fvar.instances.append(inst)
- font['fvar'] = fvar
+ font["fvar"] = fvar
diff --git a/Lib/fontTools/misc/arrayTools.py b/Lib/fontTools/misc/arrayTools.py
index 81b2418d..e76ced7f 100644
--- a/Lib/fontTools/misc/arrayTools.py
+++ b/Lib/fontTools/misc/arrayTools.py
@@ -313,9 +313,9 @@ class Vector(object):
__rmul__ = __mul__
def __truediv__(self, other):
- return Vector(self._scalarOp(other, operator.div), keep=True)
+ return Vector(self._scalarOp(other, operator.truediv), keep=True)
def __itruediv__(self, other):
- self.values = self._scalarOp(other, operator.div)
+ self.values = self._scalarOp(other, operator.truediv)
return self
def __pos__(self):
diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py
index 0b64cb38..866298be 100644
--- a/Lib/fontTools/pens/ttGlyphPen.py
+++ b/Lib/fontTools/pens/ttGlyphPen.py
@@ -15,22 +15,34 @@ __all__ = ["TTGlyphPen"]
class TTGlyphPen(LoggingPen):
"""Pen used for drawing to a TrueType glyph.
- If `handleOverflowingTransforms` is True, the components' transform values
- are checked that they don't overflow the limits of a F2Dot14 number:
- -2.0 <= v < +2.0. If any transform value exceeds these, the composite
- glyph is decomposed.
- An exception to this rule is done for values that are very close to +2.0
- (both for consistency with the -2.0 case, and for the relative frequency
- these occur in real fonts). When almost +2.0 values occur (and all other
- values are within the range -2.0 <= x <= +2.0), they are clamped to the
- maximum positive value that can still be encoded as an F2Dot14: i.e.
- 1.99993896484375.
- If False, no check is done and all components are translated unmodified
- into the glyf table, followed by an inevitable `struct.error` once an
- attempt is made to compile them.
+ This pen can be used to construct or modify glyphs in a TrueType format
+ font. After using the pen to draw, use the ``.glyph()`` method to retrieve
+ a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
"""
def __init__(self, glyphSet, handleOverflowingTransforms=True):
+ """Construct a new pen.
+
+ Args:
+ glyphSet (ttLib._TTGlyphSet): A glyphset object, used to resolve components.
+ handleOverflowingTransforms (bool): See below.
+
+ If ``handleOverflowingTransforms`` is True, the components' transform values
+ are checked that they don't overflow the limits of a F2Dot14 number:
+ -2.0 <= v < +2.0. If any transform value exceeds these, the composite
+ glyph is decomposed.
+
+ An exception to this rule is done for values that are very close to +2.0
+ (both for consistency with the -2.0 case, and for the relative frequency
+ these occur in real fonts). When almost +2.0 values occur (and all other
+ values are within the range -2.0 <= x <= +2.0), they are clamped to the
+ maximum positive value that can still be encoded as an F2Dot14: i.e.
+ 1.99993896484375.
+
+ If False, no check is done and all components are translated unmodified
+ into the glyf table, followed by an inevitable ``struct.error`` once an
+ attempt is made to compile them.
+ """
self.glyphSet = glyphSet
self.handleOverflowingTransforms = handleOverflowingTransforms
self.init()
@@ -136,6 +148,7 @@ class TTGlyphPen(LoggingPen):
return components
def glyph(self, componentFlags=0x4):
+ """Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph."""
assert self._isClosed(), "Didn't close last contour."
components = self._buildComponents(componentFlags)
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index 82605d51..8162c09c 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -14,7 +14,7 @@ import sys
import struct
import array
import logging
-from collections import Counter
+from collections import Counter, defaultdict
from types import MethodType
__usage__ = "pyftsubset font-file [glyph...] [--option=value]..."
@@ -1983,27 +1983,130 @@ def subset_glyphs(self, s):
else:
assert False, "unknown 'prop' format %s" % prop.Format
+def _paint_glyph_names(paint, colr):
+ result = set()
+
+ def callback(paint):
+ if paint.Format in {
+ otTables.PaintFormat.PaintGlyph,
+ otTables.PaintFormat.PaintColrGlyph,
+ }:
+ result.add(paint.Glyph)
+
+ paint.traverse(colr, callback)
+ return result
+
@_add_method(ttLib.getTableClass('COLR'))
def closure_glyphs(self, s):
+ if self.version > 0:
+ # on decompiling COLRv1, we only keep around the raw otTables
+ # but for subsetting we need dicts with fully decompiled layers;
+ # we store them temporarily in the C_O_L_R_ instance and delete
+ # them after we have finished subsetting.
+ self.ColorLayers = self._decompileColorLayersV0(self.table)
+ self.ColorLayersV1 = {
+ rec.BaseGlyph: rec.Paint
+ for rec in self.table.BaseGlyphV1List.BaseGlyphV1Record
+ }
+
decompose = s.glyphs
while decompose:
layers = set()
for g in decompose:
- for l in self.ColorLayers.get(g, []):
- layers.add(l.name)
+ for layer in self.ColorLayers.get(g, []):
+ layers.add(layer.name)
+
+ if self.version > 0:
+ paint = self.ColorLayersV1.get(g)
+ if paint is not None:
+ layers.update(_paint_glyph_names(paint, self.table))
+
layers -= s.glyphs
s.glyphs.update(layers)
decompose = layers
@_add_method(ttLib.getTableClass('COLR'))
def subset_glyphs(self, s):
+ from fontTools.colorLib.unbuilder import unbuildColrV1
+ from fontTools.colorLib.builder import buildColrV1, populateCOLRv0
+
self.ColorLayers = {g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers}
- return bool(self.ColorLayers)
+ if self.version == 0:
+ return bool(self.ColorLayers)
+
+ colorGlyphsV1 = unbuildColrV1(self.table.LayerV1List, self.table.BaseGlyphV1List)
+ self.table.LayerV1List, self.table.BaseGlyphV1List = buildColrV1(
+ {g: colorGlyphsV1[g] for g in colorGlyphsV1 if g in s.glyphs}
+ )
+ del self.ColorLayersV1
+
+ layersV0 = self.ColorLayers
+ if not self.table.BaseGlyphV1List.BaseGlyphV1Record:
+ # no more COLRv1 glyphs: downgrade to version 0
+ self.version = 0
+ del self.table
+ return bool(layersV0)
+
+ if layersV0:
+ populateCOLRv0(
+ self.table,
+ {
+ g: [(layer.name, layer.colorID) for layer in layersV0[g]]
+ for g in layersV0
+ },
+ )
+ del self.ColorLayers
+
+ # TODO: also prune ununsed varIndices in COLR.VarStore
+ return True
-# TODO: prune unused palettes
@_add_method(ttLib.getTableClass('CPAL'))
def prune_post_subset(self, font, options):
- return True
+ colr = font.get("COLR")
+ if not colr: # drop CPAL if COLR was subsetted to empty
+ return False
+
+ colors_by_index = defaultdict(list)
+
+ def collect_colors_by_index(paint):
+ if hasattr(paint, "Color"): # either solid colors...
+ colors_by_index[paint.Color.PaletteIndex].append(paint.Color)
+ elif hasattr(paint, "ColorLine"): # ... or gradient color stops
+ for stop in paint.ColorLine.ColorStop:
+ colors_by_index[stop.Color.PaletteIndex].append(stop.Color)
+
+ if colr.version == 0:
+ for layers in colr.ColorLayers.values():
+ for layer in layers:
+ colors_by_index[layer.colorID].append(layer)
+ else:
+ if colr.table.LayerRecordArray:
+ for layer in colr.table.LayerRecordArray.LayerRecord:
+ colors_by_index[layer.PaletteIndex].append(layer)
+ for record in colr.table.BaseGlyphV1List.BaseGlyphV1Record:
+ record.Paint.traverse(colr.table, collect_colors_by_index)
+
+ retained_palette_indices = set(colors_by_index.keys())
+ for palette in self.palettes:
+ palette[:] = [c for i, c in enumerate(palette) if i in retained_palette_indices]
+ assert len(palette) == len(retained_palette_indices)
+
+ for new_index, old_index in enumerate(sorted(retained_palette_indices)):
+ for record in colors_by_index[old_index]:
+ if hasattr(record, "colorID"): # v0
+ record.colorID = new_index
+ elif hasattr(record, "PaletteIndex"): # v1
+ record.PaletteIndex = new_index
+ else:
+ raise AssertionError(record)
+
+ self.numPaletteEntries = len(self.palettes[0])
+
+ if self.version == 1:
+ self.paletteEntryLabels = [
+ label for i, label in self.paletteEntryLabels if i in retained_palette_indices
+ ]
+ return bool(self.numPaletteEntries)
@_add_method(otTables.MathGlyphConstruction)
def closure_glyphs(self, glyphs):
diff --git a/Lib/fontTools/ttLib/tables/C_O_L_R_.py b/Lib/fontTools/ttLib/tables/C_O_L_R_.py
index 7a9442de..db490520 100644
--- a/Lib/fontTools/ttLib/tables/C_O_L_R_.py
+++ b/Lib/fontTools/ttLib/tables/C_O_L_R_.py
@@ -14,9 +14,11 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
ttFont['COLR'][<glyphName>] = <value> will set the color layers for any glyph.
"""
- def _fromOTTable(self, table):
- self.version = 0
- self.ColorLayers = colorLayerLists = {}
+ @staticmethod
+ def _decompileColorLayersV0(table):
+ if not table.LayerRecordArray:
+ return {}
+ colorLayerLists = {}
layerRecords = table.LayerRecordArray.LayerRecord
numLayerRecords = len(layerRecords)
for baseRec in table.BaseGlyphRecordArray.BaseGlyphRecord:
@@ -31,6 +33,7 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
LayerRecord(layerRec.LayerGlyph, layerRec.PaletteIndex)
)
colorLayerLists[baseGlyph] = layers
+ return colorLayerLists
def _toOTTable(self, ttFont):
from . import otTables
@@ -61,12 +64,12 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
table = tableClass()
table.decompile(reader, ttFont)
- if table.Version == 0:
- self._fromOTTable(table)
+ self.version = table.Version
+ if self.version == 0:
+ self.ColorLayers = self._decompileColorLayersV0(table)
else:
# for new versions, keep the raw otTables around
self.table = table
- self.version = table.Version
def compile(self, ttFont):
from .otBase import OTTableWriter
@@ -120,6 +123,7 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
self.table = tableClass()
self.table.fromXML(name, attrs, content, ttFont)
self.table.populateDefaults()
+ self.version = self.table.Version
def __getitem__(self, glyphName):
if not isinstance(glyphName, str):
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
index 1b278410..96d461a3 100644
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -59,14 +59,20 @@ def buildConverters(tableSpec, tableNamespace):
converterClass = Struct
else:
converterClass = eval(tp, tableNamespace, converterMapping)
- if tp in ('MortChain', 'MortSubtable', 'MorxChain'):
+
+ conv = converterClass(name, repeat, aux)
+
+ if conv.tableClass:
+ # A "template" such as OffsetTo(AType) knowss the table class already
+ tableClass = conv.tableClass
+ elif tp in ('MortChain', 'MortSubtable', 'MorxChain'):
tableClass = tableNamespace.get(tp)
else:
tableClass = tableNamespace.get(tableName)
- if tableClass is not None:
- conv = converterClass(name, repeat, aux, tableClass=tableClass)
- else:
- conv = converterClass(name, repeat, aux)
+
+ if not conv.tableClass:
+ conv.tableClass = tableClass
+
if name in ["SubTable", "ExtSubTable", "SubStruct"]:
conv.lookupTypes = tableNamespace['lookupTypes']
# also create reverse mapping
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index a6f9619e..28b40c47 100755
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
@@ -1588,7 +1588,24 @@ 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', [
+ ('Fixed', 'xx', None, None, 'x-part of x basis vector'),
+ ('Fixed', 'yx', None, None, 'y-part of x basis vector'),
+ ('Fixed', 'xy', None, None, 'x-part of y basis vector'),
+ ('Fixed', 'yy', None, None, 'y-part of y basis vector'),
+ ('Fixed', 'dx', None, None, 'Translation in x direction'),
+ ('Fixed', 'dy', None, None, 'Translation in y direction'),
+ ]),
+ ('VarAffine2x3', [
('VarFixed', 'xx', None, None, 'x-part of x basis vector'),
('VarFixed', 'yx', None, None, 'y-part of x basis vector'),
('VarFixed', 'xy', None, None, 'x-part of y basis vector'),
@@ -1599,34 +1616,66 @@ otData = [
('ColorIndex', [
('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
+ ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
+ ]),
+ ('VarColorIndex', [
+ ('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
('VarF2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
]),
('ColorStop', [
- ('VarF2Dot14', 'StopOffset', None, None, ''),
+ ('F2Dot14', 'StopOffset', None, None, ''),
('ColorIndex', 'Color', None, None, ''),
]),
+ ('VarColorStop', [
+ ('VarF2Dot14', 'StopOffset', None, None, ''),
+ ('VarColorIndex', 'Color', None, None, ''),
+ ]),
('ColorLine', [
('ExtendMode', 'Extend', None, None, 'Enum {PAD = 0, REPEAT = 1, REFLECT = 2}'),
('uint16', 'StopCount', None, None, 'Number of Color stops.'),
('ColorStop', 'ColorStop', 'StopCount', 0, 'Array of Color stops.'),
]),
+ ('VarColorLine', [
+ ('ExtendMode', 'Extend', None, None, 'Enum {PAD = 0, REPEAT = 1, REFLECT = 2}'),
+ ('uint16', 'StopCount', None, None, 'Number of Color stops.'),
+ ('VarColorStop', 'ColorStop', 'StopCount', 0, 'Array of Color stops.'),
+ ]),
+ # PaintColrLayers
('PaintFormat1', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 1'),
('uint8', 'NumLayers', None, None, 'Number of offsets to Paint to read from LayerV1List.'),
('uint32', 'FirstLayerIndex', None, None, 'Index into LayerV1List.'),
]),
+ # PaintSolid
('PaintFormat2', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 2'),
('ColorIndex', 'Color', None, None, 'A solid color paint.'),
]),
-
+ # PaintVarSolid
('PaintFormat3', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 3'),
- ('Offset24', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
+ ('VarColorIndex', 'Color', None, None, 'A solid color paint.'),
+ ]),
+
+ # PaintLinearGradient
+ ('PaintFormat4', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 4'),
+ ('Offset24', 'ColorLine', None, None, 'Offset (from beginning of PaintLinearGradient table) to ColorLine subtable.'),
+ ('int16', 'x0', None, None, ''),
+ ('int16', 'y0', None, None, ''),
+ ('int16', 'x1', None, None, ''),
+ ('int16', 'y1', None, None, ''),
+ ('int16', 'x2', None, None, ''),
+ ('int16', 'y2', None, None, ''),
+ ]),
+ # PaintVarLinearGradient
+ ('PaintFormat5', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
+ ('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarLinearGradient table) to VarColorLine subtable.'),
('VarInt16', 'x0', None, None, ''),
('VarInt16', 'y0', None, None, ''),
('VarInt16', 'x1', None, None, ''),
@@ -1635,9 +1684,21 @@ otData = [
('VarInt16', 'y2', None, None, ''),
]),
- ('PaintFormat4', [
- ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 4'),
- ('Offset24', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
+ # PaintRadialGradient
+ ('PaintFormat6', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 6'),
+ ('Offset24', 'ColorLine', None, None, 'Offset (from beginning of PaintRadialGradient table) to ColorLine subtable.'),
+ ('int16', 'x0', None, None, ''),
+ ('int16', 'y0', None, None, ''),
+ ('uint16', 'r0', None, None, ''),
+ ('int16', 'x1', None, None, ''),
+ ('int16', 'y1', None, None, ''),
+ ('uint16', 'r1', None, None, ''),
+ ]),
+ # PaintVarRadialGradient
+ ('PaintFormat7', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 7'),
+ ('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarRadialGradient table) to VarColorLine subtable.'),
('VarInt16', 'x0', None, None, ''),
('VarInt16', 'y0', None, None, ''),
('VarUInt16', 'r0', None, None, ''),
@@ -1646,49 +1707,105 @@ otData = [
('VarUInt16', 'r1', None, None, ''),
]),
- ('PaintFormat5', [
- ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
+ # PaintSweepGradient
+ ('PaintFormat8', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 8'),
+ ('Offset24', 'ColorLine', None, None, 'Offset (from beginning of PaintSweepGradient table) to ColorLine subtable.'),
+ ('int16', 'centerX', None, None, 'Center x coordinate.'),
+ ('int16', 'centerY', None, None, 'Center y coordinate.'),
+ ('Fixed', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
+ ('Fixed', 'endAngle', None, None, 'End of the angular range of the gradient.'),
+ ]),
+ # PaintVarSweepGradient
+ ('PaintFormat9', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 9'),
+ ('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarSweepGradient table) to VarColorLine subtable.'),
+ ('VarInt16', 'centerX', None, None, 'Center x coordinate.'),
+ ('VarInt16', 'centerY', None, None, 'Center y coordinate.'),
+ ('VarFixed', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
+ ('VarFixed', 'endAngle', None, None, 'End of the angular range of the gradient.'),
+ ]),
+
+ # PaintGlyph
+ ('PaintFormat10', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 10'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintGlyph table) to Paint subtable.'),
('GlyphID', 'Glyph', None, None, 'Glyph ID for the source outline.'),
]),
- ('PaintFormat6', [
- ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 6'),
+ # PaintColrGlyph
+ ('PaintFormat11', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 11'),
('GlyphID', 'Glyph', None, None, 'Virtual glyph ID for a BaseGlyphV1List base glyph.'),
]),
- ('PaintFormat7', [
- ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 7'),
- ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintTransformed table) to Paint subtable.'),
- ('Affine2x3', 'Transform', None, None, 'Offset (from beginning of PaintTrasformed table) to Affine2x3 subtable.'),
+ # PaintTransform
+ ('PaintFormat12', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 12'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintTransform table) to Paint subtable.'),
+ ('Affine2x3', 'Transform', None, None, '2x3 matrix for 2D affine transformations.'),
+ ]),
+ # PaintVarTransform
+ ('PaintFormat13', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 13'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarTransform table) to Paint subtable.'),
+ ('VarAffine2x3', 'Transform', None, None, '2x3 matrix for 2D affine transformations.'),
]),
- ('PaintFormat8', [
- ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 8'),
+ # PaintTranslate
+ ('PaintFormat14', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 14'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintTranslate table) to Paint subtable.'),
+ ('Fixed', 'dx', None, None, 'Translation in x direction.'),
+ ('Fixed', 'dy', None, None, 'Translation in y direction.'),
+ ]),
+ # PaintVarTranslate
+ ('PaintFormat15', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 15'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarTranslate table) to Paint subtable.'),
('VarFixed', 'dx', None, None, 'Translation in x direction.'),
('VarFixed', 'dy', None, None, 'Translation in y direction.'),
]),
- ('PaintFormat9', [
- ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 9'),
+ # PaintRotate
+ ('PaintFormat16', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 16'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintRotate table) to Paint subtable.'),
+ ('Fixed', 'angle', None, None, ''),
+ ('Fixed', 'centerX', None, None, ''),
+ ('Fixed', 'centerY', None, None, ''),
+ ]),
+ # PaintVarRotate
+ ('PaintFormat17', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 17'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarRotate table) to Paint subtable.'),
('VarFixed', 'angle', None, None, ''),
('VarFixed', 'centerX', None, None, ''),
('VarFixed', 'centerY', None, None, ''),
]),
- ('PaintFormat10', [
- ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 10'),
+ # PaintSkew
+ ('PaintFormat18', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 18'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintSkew table) to Paint subtable.'),
+ ('Fixed', 'xSkewAngle', None, None, ''),
+ ('Fixed', 'ySkewAngle', None, None, ''),
+ ('Fixed', 'centerX', None, None, ''),
+ ('Fixed', 'centerY', None, None, ''),
+ ]),
+ # PaintVarSkew
+ ('PaintFormat19', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 19'),
+ ('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarSkew table) to Paint subtable.'),
('VarFixed', 'xSkewAngle', None, None, ''),
('VarFixed', 'ySkewAngle', None, None, ''),
('VarFixed', 'centerX', None, None, ''),
('VarFixed', 'centerY', None, None, ''),
]),
- ('PaintFormat11', [
- ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 11'),
+ # PaintComposite
+ ('PaintFormat20', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 20'),
('LOffset24To(Paint)', 'SourcePaint', None, None, 'Offset (from beginning of PaintComposite table) to source Paint subtable.'),
('CompositeMode', 'CompositeMode', None, None, 'A CompositeMode enumeration value.'),
('LOffset24To(Paint)', 'BackdropPaint', None, None, 'Offset (from beginning of PaintComposite table) to backdrop Paint subtable.'),
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index 7f42921d..008909bd 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -1324,24 +1324,34 @@ class CompositeMode(IntEnum):
HSL_LUMINOSITY = 26
-class Paint(getFormatSwitchingBaseTableClass("uint8")):
+class PaintFormat(IntEnum):
+ PaintColrLayers = 1
+ PaintSolid = 2
+ PaintVarSolid = 3,
+ PaintLinearGradient = 4
+ PaintVarLinearGradient = 5
+ PaintRadialGradient = 6
+ PaintVarRadialGradient = 7
+ PaintSweepGradient = 8
+ PaintVarSweepGradient = 9
+ PaintGlyph = 10
+ PaintColrGlyph = 11
+ PaintTransform = 12
+ PaintVarTransform = 13
+ PaintTranslate = 14
+ PaintVarTranslate = 15
+ PaintRotate = 16
+ PaintVarRotate = 17
+ PaintSkew = 18
+ PaintVarSkew = 19
+ PaintComposite = 20
+
- class Format(IntEnum):
- PaintColrLayers = 1
- PaintSolid = 2
- PaintLinearGradient = 3
- PaintRadialGradient = 4
- PaintGlyph = 5
- PaintColrGlyph = 6
- PaintTransform = 7
- PaintTranslate = 8
- PaintRotate = 9
- PaintSkew = 10
- PaintComposite = 11
+class Paint(getFormatSwitchingBaseTableClass("uint8")):
def getFormatName(self):
try:
- return self.__class__.Format(self.Format).name
+ return PaintFormat(self.Format).name
except ValueError:
raise NotImplementedError(f"Unknown Paint format: {self.Format}")
@@ -1357,6 +1367,40 @@ class Paint(getFormatSwitchingBaseTableClass("uint8")):
xmlWriter.endtag(tableName)
xmlWriter.newline()
+ def getChildren(self, colr):
+ if self.Format == PaintFormat.PaintColrLayers:
+ return colr.LayerV1List.Paint[
+ self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers
+ ]
+
+ if self.Format == PaintFormat.PaintColrGlyph:
+ for record in colr.BaseGlyphV1List.BaseGlyphV1Record:
+ if record.BaseGlyph == self.Glyph:
+ return [record.Paint]
+ else:
+ raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphV1List")
+
+ children = []
+ for conv in self.getConverters():
+ if conv.tableClass is not None and issubclass(conv.tableClass, type(self)):
+ children.append(getattr(self, conv.name))
+
+ return children
+
+ def traverse(self, colr: COLR, callback):
+ """Depth-first traversal of graph rooted at self, callback on each node."""
+ if not callable(callback):
+ raise TypeError("callback must be callable")
+ stack = [self]
+ visited = set()
+ while stack:
+ current = stack.pop()
+ if id(current) in visited:
+ continue
+ callback(current)
+ visited.add(id(current))
+ stack.extend(reversed(current.getChildren(colr)))
+
# For each subtable format there is a class. However, we don't really distinguish
# between "field name" and "format name": often these are the same. Yet there's
diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py
index ed1ec5e2..811cf003 100644
--- a/Lib/fontTools/ttLib/ttFont.py
+++ b/Lib/fontTools/ttLib/ttFont.py
@@ -700,6 +700,13 @@ class _TTGlyphSet(object):
"""
def __init__(self, ttFont, glyphs, glyphType):
+ """Construct a new glyphset.
+
+ Args:
+ font (TTFont): The font object (used to get metrics).
+ glyphs (dict): A dictionary mapping glyph names to ``_TTGlyph`` objects.
+ glyphType (class): Either ``_TTGlyphCFF`` or ``_TTGlyphGlyf``.
+ """
self._glyphs = glyphs
self._hmtx = ttFont['hmtx']
self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None
@@ -740,6 +747,13 @@ class _TTGlyph(object):
"""
def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None):
+ """Construct a new _TTGlyph.
+
+ Args:
+ glyphset (_TTGlyphSet): A glyphset object used to resolve components.
+ glyph (ttLib.tables._g_l_y_f.Glyph): The glyph object.
+ horizontalMetrics (int, int): The glyph's width and left sidebearing.
+ """
self._glyphset = glyphset
self._glyph = glyph
self.width, self.lsb = horizontalMetrics
@@ -749,7 +763,7 @@ class _TTGlyph(object):
self.height, self.tsb = None, None
def draw(self, pen):
- """Draw the glyph onto Pen. See fontTools.pens.basePen for details
+ """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
how that works.
"""
self._glyph.draw(pen)
diff --git a/METADATA b/METADATA
index 478bd7e1..6bb4a206 100644
--- a/METADATA
+++ b/METADATA
@@ -1,3 +1,6 @@
+# *** THIS PACKAGE HAS SPECIAL LICENSING CONDITIONS. PLEASE
+# CONSULT THE OWNERS AND opensource-licensing@google.com BEFORE
+# DEPENDING ON IT IN YOUR PROJECT. ***
name: "fonttools"
description: "fontTools is a library for manipulating fonts, written in Python."
third_party {
@@ -7,13 +10,13 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/archive/4.19.1.zip"
+ value: "https://github.com/fonttools/fonttools/archive/4.20.0.zip"
}
- version: "4.19.1"
+ version: "4.20.0"
license_type: BY_EXCEPTION_ONLY
last_upgrade_date {
year: 2021
- month: 1
- day: 28
+ month: 2
+ day: 17
}
}
diff --git a/NEWS.rst b/NEWS.rst
index 776deef8..393795cd 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,22 @@
+4.20.0 (released 2021-02-15)
+----------------------------
+
+- [COLRv1] Added ``unbuildColrV1`` to deconstruct COLRv1 otTables to raw json-able
+ data structure; it does the reverse of ``buildColrV1`` (#2171).
+- [feaLib] Allow ``sub X by NULL`` sequence to delete a glyph (#2170).
+- [arrayTools] Fixed ``Vector`` division (#2173).
+- [COLRv1] Define new ``PaintSweepGradient`` (#2172).
+- [otTables] Moved ``Paint.Format`` enum class outside of ``Paint`` class definition,
+ now named ``PaintFormat``. It was clashing with paint instance ``Format`` attribute
+ and thus was breaking lazy load of COLR table which relies on magic ``__getattr__``
+ (#2175).
+- [COLRv1] Replace hand-coded builder functions with otData-driven dynamic
+ implementation (#2181).
+- [COLRv1] Define additional static (non-variable) Paint formats (#2181).
+- [fontBuilder] Allow ``setupFvar`` to optionally take ``designspaceLib.AxisDescriptor``
+ objects. Added new ``setupAvar`` method. Support localised names for axes and
+ named instances (#2185).
+
4.19.1 (released 2021-01-28)
----------------------------
diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py
index 43ec96a4..81da2818 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)],
@@ -222,191 +231,361 @@ def test_buildCPAL_invalid_color():
builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]])
-def test_buildColorIndex():
- c = builder.buildColorIndex(0)
- assert c.PaletteIndex == 0
+def test_buildColorIndex_Minimal():
+ c = _build(ot.ColorIndex, 1)
+ assert c.PaletteIndex == 1
+ assert c.Alpha == 1.0
+
+
+def test_buildVarColorIndex_Minimal():
+ c = _build(ot.VarColorIndex, 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():
+ c = _build(ot.ColorIndex, (1, 0.5))
assert c.PaletteIndex == 1
- assert c.Alpha.value == 0.5
- assert c.Alpha.varIdx == 0
+ assert c.Alpha == 0.5
+
- c = builder.buildColorIndex(3, alpha=builder.VariableFloat(0.5, varIdx=2))
+def test_buildVarColorIndex():
+ c = _build(ot.VarColorIndex, (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)
- assert p.Format == ot.Paint.Format.PaintSolid
+ 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
+ assert p.Color.Alpha == 1.0
- p = LayerV1ListBuilder().buildPaintSolid(1, alpha=0.5)
- assert p.Format == ot.Paint.Format.PaintSolid
+
+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
+ assert p.Color.Alpha == 0.5
- p = LayerV1ListBuilder().buildPaintSolid(
- 3, alpha=builder.VariableFloat(0.5, varIdx=2)
+
+def test_buildPaintVarSolid():
+ p = _buildPaint(
+ (ot.PaintFormat.PaintVarSolid, (3, builder.VariableFloat(0.5, varIdx=2)))
)
- assert p.Format == ot.Paint.Format.PaintSolid
+ assert p.Format == ot.PaintFormat.PaintVarSolid
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_buildVarColorStop_DefaultAlpha():
+ s = _build(ot.ColorStop, (0.1, 2))
+ assert s.StopOffset == 0.1
+ assert s.Color.PaletteIndex == 2
+ assert s.Color.Alpha == builder._DEFAULT_ALPHA.value
+
+
+def test_buildVarColorStop_DefaultAlpha():
+ s = _build(ot.VarColorStop, (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 == 0.2
+ assert s.Color == _build(ot.ColorIndex, (3, 0.4))
+
+
+def test_buildColorStop_Variable():
+ s = _build(
+ ot.VarColorStop,
+ {
+ "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
+ assert [(cs.StopOffset, 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])
- assert [
- (cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop
- ] == stops
+ cline = _build(
+ ot.ColorLine, {"ColorStop": [_build(ot.ColorStop, s) for s in stops]}
+ )
+ assert [(cs.StopOffset, cs.Color.PaletteIndex) for cs in cline.ColorStop] == stops
+
+def test_buildVarColorLine_StopMap():
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.VarColorLine, {"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 checkBuildAffine2x3(cls, resultMapFn):
+ matrix = _build(cls, (1.5, 0, 0.5, 2.0, 1.0, -3.0))
+ assert matrix.xx == resultMapFn(1.5)
+ assert matrix.yx == resultMapFn(0.0)
+ assert matrix.xy == resultMapFn(0.5)
+ assert matrix.yy == resultMapFn(2.0)
+ assert matrix.dx == resultMapFn(1.0)
+ assert matrix.dy == resultMapFn(-3.0)
+
+
def test_buildAffine2x3():
- matrix = builder.buildAffine2x3((1.5, 0, 0.5, 2.0, 1.0, -3.0))
- assert matrix.xx == builder.VariableFloat(1.5)
- assert matrix.yx == builder.VariableFloat(0.0)
- assert matrix.xy == builder.VariableFloat(0.5)
- assert matrix.yy == builder.VariableFloat(2.0)
- assert matrix.dx == builder.VariableFloat(1.0)
- assert matrix.dy == builder.VariableFloat(-3.0)
+ checkBuildAffine2x3(ot.Affine2x3, lambda v: v)
-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 test_buildVarAffine2x3():
+ checkBuildAffine2x3(ot.VarAffine2x3, builder.VariableFloat)
+
+
+def _sample_stops(cls):
+ return [
+ _build(cls, (0.0, 0)),
+ _build(cls, (0.5, 1)),
+ _build(cls, (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 _is_var(fmt):
+ return fmt.name.startswith("PaintVar")
+
+
+def checkBuildPaintLinearGradient(fmt):
+ if _is_var(fmt):
+ inputMapFn = builder.VariableInt
+ outputMapFn = lambda v: v.value
+ color_stops = _sample_stops(ot.VarColorStop)
+ else:
+ inputMapFn = outputMapFn = lambda v: v
+ color_stops = _sample_stops(ot.ColorStop)
+
+ x0, y0, x1, y1, x2, y2 = tuple(inputMapFn(v) for v in (1, 2, 3, 4, 5, 6))
+ gradient = _buildPaint(
+ {
+ "Format": fmt,
+ "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 (outputMapFn(gradient.x0), outputMapFn(gradient.y0)) == (1, 2)
+ assert (outputMapFn(gradient.x1), outputMapFn(gradient.y1)) == (3, 4)
+ assert (outputMapFn(gradient.x2), outputMapFn(gradient.y2)) == (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),
- ]
- color_line = builder.buildColorLine(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)
- assert gradient.Format == ot.Paint.Format.PaintRadialGradient
+def test_buildPaintLinearGradient():
+ assert not _is_var(ot.PaintFormat.PaintLinearGradient)
+ checkBuildPaintLinearGradient(ot.PaintFormat.PaintLinearGradient)
+
+
+def test_buildVarPaintLinearGradient():
+ assert _is_var(ot.PaintFormat.PaintVarLinearGradient)
+ checkBuildPaintLinearGradient(ot.PaintFormat.PaintVarLinearGradient)
+
+
+def checkBuildPaintRadialGradient(fmt):
+ if _is_var(fmt):
+ inputMapFn = builder.VariableInt
+ outputMapFn = lambda v: v
+ color_stops = _sample_stops(ot.VarColorStop)
+ line_cls = ot.VarColorLine
+ else:
+ inputMapFn = outputMapFn = lambda v: v
+ color_stops = _sample_stops(ot.ColorStop)
+ line_cls = ot.ColorLine
+
+ color_line = _build(
+ line_cls, {"ColorStop": color_stops, "Extend": builder.ExtendMode.REPEAT}
+ )
+ c0 = (inputMapFn(100), inputMapFn(200))
+ c1 = (inputMapFn(150), inputMapFn(250))
+ r0 = inputMapFn(10)
+ r1 = inputMapFn(5)
+
+ gradient = _build(ot.Paint, (fmt, color_line, *c0, r0, *c1, r1))
+ assert gradient.Format == fmt
assert gradient.ColorLine == color_line
- assert (gradient.x0, gradient.y0) == c0
- assert (gradient.x1, gradient.y1) == c1
- assert gradient.r0 == r0
- assert gradient.r1 == r1
+ assert (outputMapFn(gradient.x0), outputMapFn(gradient.y0)) == c0
+ assert (outputMapFn(gradient.x1), outputMapFn(gradient.y1)) == c1
+ assert outputMapFn(gradient.r0) == r0
+ assert outputMapFn(gradient.r1) == r1
- gradient = layerBuilder.buildPaintRadialGradient(
- {"stops": color_stops}, c0, c1, r0, r1
+ gradient = _build(
+ ot.Paint,
+ {
+ "Format": fmt,
+ "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 (outputMapFn(gradient.x0), outputMapFn(gradient.y0)) == c0
+ assert (outputMapFn(gradient.x1), outputMapFn(gradient.y1)) == c1
+ assert outputMapFn(gradient.r0) == r0
+ assert outputMapFn(gradient.r1) == r1
+
+
+def test_buildPaintRadialGradient():
+ assert not _is_var(ot.PaintFormat.PaintRadialGradient)
+ checkBuildPaintRadialGradient(ot.PaintFormat.PaintRadialGradient)
+
+
+def test_buildPaintVarRadialGradient():
+ assert _is_var(ot.PaintFormat.PaintVarRadialGradient)
+ checkBuildPaintRadialGradient(ot.PaintFormat.PaintVarRadialGradient)
+
+
+def checkPaintSweepGradient(fmt):
+ if _is_var(fmt):
+ outputMapFn = lambda v: v.value
+ else:
+ outputMapFn = lambda v: v
+
+ paint = _buildPaint(
+ {
+ "Format": fmt,
+ "ColorLine": {
+ "ColorStop": (
+ (0.0, 0),
+ (0.5, 1),
+ (1.0, (2, 0.8)),
+ )
+ },
+ "centerX": 127,
+ "centerY": 129,
+ "startAngle": 15,
+ "endAngle": 42,
+ }
+ )
+
+ assert paint.Format == fmt
+ assert outputMapFn(paint.centerX) == 127
+ assert outputMapFn(paint.centerY) == 129
+ assert outputMapFn(paint.startAngle) == 15
+ assert outputMapFn(paint.endAngle) == 42
+
+
+def test_buildPaintSweepGradient():
+ assert not _is_var(ot.PaintFormat.PaintSweepGradient)
+ checkPaintSweepGradient(ot.PaintFormat.PaintSweepGradient)
+
+
+def test_buildPaintVarSweepGradient():
+ assert _is_var(ot.PaintFormat.PaintVarSweepGradient)
+ checkPaintSweepGradient(ot.PaintFormat.PaintVarSweepGradient)
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.Paint.Format.PaintSolid
+ assert layer.Paint.Format == ot.PaintFormat.PaintSolid
assert layer.Paint.Color.PaletteIndex == 2
- layer = layerBuilder.buildPaintGlyph("a", layerBuilder.buildPaintSolid(3, 0.9))
- assert layer.Paint.Format == ot.Paint.Format.PaintSolid
+ 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
+ assert layer.Paint.Color.Alpha == 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)
- ),
+def test_buildPaintGlyph_VarLinearGradient():
+ layer = _build(
+ ot.Paint,
+ {
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Glyph": "a",
+ "Paint": {
+ "Format": ot.PaintFormat.PaintVarLinearGradient,
+ "ColorLine": {"ColorStop": [(0.0, 3), (1.0, 4)]},
+ "x0": 100,
+ "y0": 200,
+ "x1": 150,
+ "y1": 250,
+ },
+ },
)
- assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient
+
+ assert layer.Format == ot.PaintFormat.PaintGlyph
+ assert layer.Glyph == "a"
+ assert layer.Paint.Format == ot.PaintFormat.PaintVarLinearGradient
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 3
assert layer.Paint.ColorLine.ColorStop[1].StopOffset.value == 1.0
@@ -418,235 +597,387 @@ 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.Paint.Format == ot.Paint.Format.PaintRadialGradient
- assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
+ assert layer.Format == ot.PaintFormat.PaintGlyph
+ assert layer.Paint.Format == ot.PaintFormat.PaintRadialGradient
+ assert layer.Paint.ColorLine.ColorStop[0].StopOffset == 0.0
assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 5
- assert layer.Paint.ColorLine.ColorStop[1].StopOffset.value == 0.5
+ assert layer.Paint.ColorLine.ColorStop[1].StopOffset == 0.5
assert layer.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 6
- assert layer.Paint.ColorLine.ColorStop[1].Color.Alpha.value == 0.8
- assert layer.Paint.ColorLine.ColorStop[2].StopOffset.value == 1.0
+ assert layer.Paint.ColorLine.ColorStop[1].Color.Alpha == 0.8
+ assert layer.Paint.ColorLine.ColorStop[2].StopOffset == 1.0
assert layer.Paint.ColorLine.ColorStop[2].Color.PaletteIndex == 7
- assert layer.Paint.x0.value == 50
- assert layer.Paint.y0.value == 50
- assert layer.Paint.r0.value == 30
- assert layer.Paint.x1.value == 75
- assert layer.Paint.y1.value == 75
- assert layer.Paint.r1.value == 10
+ assert layer.Paint.x0 == 50
+ assert layer.Paint.y0 == 50
+ assert layer.Paint.r0 == 30
+ assert layer.Paint.x1 == 75
+ assert layer.Paint.y1 == 75
+ assert layer.Paint.r1 == 10
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.Paint.Format.PaintSolid
- assert layer.Paint.Color.PaletteIndex == 0
+ assert layer.Paint.Format == ot.PaintFormat.PaintSolid
+ assert layer.Paint.Color.PaletteIndex == 1
-def test_buildPaintGlyph_Dict_LinearGradient():
- layerBuilder = LayerV1ListBuilder()
- layer = layerBuilder.buildPaintGlyph(
- "a",
+def test_buildPaintGlyph_Dict_VarLinearGradient():
+ 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": int(ot.PaintFormat.PaintVarLinearGradient),
+ "ColorLine": {"ColorStop": [(0.0, 0), (1.0, 1)]},
+ "x0": 0,
+ "y0": 0,
+ "x1": 10,
+ "y1": 10,
+ },
},
)
- assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient
+ assert layer.Format == ot.PaintFormat.PaintGlyph
+ assert layer.Glyph == "a"
+ assert layer.Paint.Format == ot.PaintFormat.PaintVarLinearGradient
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.Paint.Format.PaintRadialGradient
- assert layer.Paint.r0.value == 4
+ assert layer.Paint.Format == ot.PaintFormat.PaintRadialGradient
+ assert layer.Paint.r0 == 4
def test_buildPaintColrGlyph():
- paint = LayerV1ListBuilder().buildPaintColrGlyph("a")
- assert paint.Format == ot.Paint.Format.PaintColrGlyph
+ 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),
+def checkBuildPaintTransform(fmt):
+ if _is_var(fmt):
+ inputMapFn = builder.VariableFloat
+ outputMapFn = lambda v: v.value
+ affine_cls = ot.VarAffine2x3
+ else:
+ inputMapFn = outputMapFn = lambda v: v
+ affine_cls = ot.Affine2x3
+
+ paint = _buildPaint(
+ (
+ int(fmt),
+ (ot.PaintFormat.PaintGlyph, (ot.PaintFormat.PaintSolid, (0, 1.0)), "a"),
+ _build(affine_cls, (1, 2, 3, 4, 5, 6)),
),
)
- assert paint.Format == ot.Paint.Format.PaintTransform
- assert paint.Paint.Format == ot.Paint.Format.PaintGlyph
- assert paint.Paint.Paint.Format == ot.Paint.Format.PaintSolid
+ assert paint.Format == fmt
+ assert paint.Paint.Format == ot.PaintFormat.PaintGlyph
+ assert paint.Paint.Paint.Format == ot.PaintFormat.PaintSolid
- assert paint.Transform.xx.value == 1.0
- assert paint.Transform.yx.value == 2.0
- assert paint.Transform.xy.value == 3.0
- assert paint.Transform.yy.value == 4.0
- assert paint.Transform.dx.value == 5.0
- assert paint.Transform.dy.value == 6.0
+ assert outputMapFn(paint.Transform.xx) == 1.0
+ assert outputMapFn(paint.Transform.yx) == 2.0
+ assert outputMapFn(paint.Transform.xy) == 3.0
+ assert outputMapFn(paint.Transform.yy) == 4.0
+ assert outputMapFn(paint.Transform.dx) == 5.0
+ assert outputMapFn(paint.Transform.dy) == 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": fmt,
+ "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.Paint.Format.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.yy.value == 0.3333
- assert paint.Transform.dx.value == 10
- assert paint.Transform.dy.value == 10
- assert paint.Paint.Format == ot.Paint.Format.PaintRadialGradient
+ assert paint.Format == fmt
+ assert outputMapFn(paint.Transform.xx) == 1.0
+ assert outputMapFn(paint.Transform.yx) == 2.0
+ assert outputMapFn(paint.Transform.xy) == 3.0
+ assert outputMapFn(paint.Transform.yy) == 0.3333
+ assert outputMapFn(paint.Transform.dx) == 10
+ assert outputMapFn(paint.Transform.dy) == 10
+ assert paint.Paint.Format == ot.PaintFormat.PaintRadialGradient
+
+
+def test_buildPaintTransform():
+ assert not _is_var(ot.PaintFormat.PaintTransform)
+ checkBuildPaintTransform(ot.PaintFormat.PaintTransform)
+
+
+def test_buildPaintVarTransform():
+ assert _is_var(ot.PaintFormat.PaintVarTransform)
+ checkBuildPaintTransform(ot.PaintFormat.PaintVarTransform)
def test_buildPaintComposite():
- layerBuilder = LayerV1ListBuilder()
- composite = layerBuilder.buildPaintComposite(
- mode=ot.CompositeMode.SRC_OVER,
- source={
- "format": 11,
- "mode": "src_over",
- "source": {"format": 5, "glyph": "c", "paint": 2},
- "backdrop": {"format": 5, "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.Paint.Format.PaintComposite
- assert composite.SourcePaint.Format == ot.Paint.Format.PaintComposite
- assert composite.SourcePaint.SourcePaint.Format == ot.Paint.Format.PaintGlyph
+ assert composite.Format == ot.PaintFormat.PaintComposite
+ assert composite.SourcePaint.Format == ot.PaintFormat.PaintComposite
+ assert composite.SourcePaint.SourcePaint.Format == ot.PaintFormat.PaintGlyph
assert composite.SourcePaint.SourcePaint.Glyph == "c"
- assert composite.SourcePaint.SourcePaint.Paint.Format == ot.Paint.Format.PaintSolid
+ assert composite.SourcePaint.SourcePaint.Paint.Format == ot.PaintFormat.PaintSolid
assert composite.SourcePaint.SourcePaint.Paint.Color.PaletteIndex == 2
assert composite.SourcePaint.CompositeMode == ot.CompositeMode.SRC_OVER
- assert composite.SourcePaint.BackdropPaint.Format == ot.Paint.Format.PaintGlyph
+ assert composite.SourcePaint.BackdropPaint.Format == ot.PaintFormat.PaintGlyph
assert composite.SourcePaint.BackdropPaint.Glyph == "b"
- assert (
- composite.SourcePaint.BackdropPaint.Paint.Format == ot.Paint.Format.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.Paint.Format.PaintGlyph
+ assert composite.BackdropPaint.Format == ot.PaintFormat.PaintGlyph
assert composite.BackdropPaint.Glyph == "a"
- assert composite.BackdropPaint.Paint.Format == ot.Paint.Format.PaintSolid
+ assert composite.BackdropPaint.Paint.Format == ot.PaintFormat.PaintSolid
assert composite.BackdropPaint.Paint.Color.PaletteIndex == 0
+def checkBuildPaintTranslate(fmt):
+ if _is_var(fmt):
+ inputMapFn = builder.VariableInt
+ outputMapFn = lambda v: v.value
+ else:
+ inputMapFn = outputMapFn = lambda v: v
+
+ paint = _build(
+ ot.Paint,
+ {
+ "Format": fmt,
+ "Paint": (
+ ot.PaintFormat.PaintGlyph,
+ (ot.PaintFormat.PaintSolid, (0, 1.0)),
+ "a",
+ ),
+ "dx": 123,
+ "dy": -345,
+ },
+ )
+
+ assert paint.Format == fmt
+ assert paint.Paint.Format == ot.PaintFormat.PaintGlyph
+ assert outputMapFn(paint.dx) == 123
+ assert outputMapFn(paint.dy) == -345
+
+
def test_buildPaintTranslate():
- layerBuilder = LayerV1ListBuilder()
- paint = layerBuilder.buildPaintTranslate(
- paint=layerBuilder.buildPaintGlyph(
- "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0)
- ),
- dx=123,
- dy=-345,
+ assert not _is_var(ot.PaintFormat.PaintTranslate)
+ checkBuildPaintTranslate(ot.PaintFormat.PaintTranslate)
+
+
+def test_buildPaintVarTranslate():
+ assert _is_var(ot.PaintFormat.PaintVarTranslate)
+ checkBuildPaintTranslate(ot.PaintFormat.PaintVarTranslate)
+
+
+def checkBuildPaintRotate(fmt):
+ if _is_var(fmt):
+ inputMapFn = builder.VariableInt
+ outputMapFn = lambda v: v.value
+ else:
+ inputMapFn = outputMapFn = lambda v: v
+
+ paint = _build(
+ ot.Paint,
+ {
+ "Format": fmt,
+ "Paint": (
+ ot.PaintFormat.PaintGlyph,
+ (ot.PaintFormat.PaintSolid, (0, 1.0)),
+ "a",
+ ),
+ "angle": 15,
+ "centerX": 127,
+ "centerY": 129,
+ },
)
- assert paint.Format == ot.Paint.Format.PaintTranslate
- assert paint.Paint.Format == ot.Paint.Format.PaintGlyph
- assert paint.dx.value == 123
- assert paint.dy.value == -345
+ assert paint.Format == fmt
+ assert paint.Paint.Format == ot.PaintFormat.PaintGlyph
+ assert outputMapFn(paint.angle) == 15
+ assert outputMapFn(paint.centerX) == 127
+ assert outputMapFn(paint.centerY) == 129
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,
+ assert not _is_var(ot.PaintFormat.PaintRotate)
+ checkBuildPaintRotate(ot.PaintFormat.PaintRotate)
+
+
+def test_buildPaintVarRotate():
+ assert _is_var(ot.PaintFormat.PaintVarRotate)
+ checkBuildPaintRotate(ot.PaintFormat.PaintVarRotate)
+
+
+def checkBuildPaintSkew(fmt):
+ if _is_var(fmt):
+ inputMapFn = builder.VariableInt
+ outputMapFn = lambda v: v.value
+ else:
+ inputMapFn = outputMapFn = lambda v: v
+
+ paint = _build(
+ ot.Paint,
+ {
+ "Format": fmt,
+ "Paint": (
+ ot.PaintFormat.PaintGlyph,
+ (ot.PaintFormat.PaintSolid, (0, 1.0)),
+ "a",
+ ),
+ "xSkewAngle": 15,
+ "ySkewAngle": 42,
+ "centerX": 127,
+ "centerY": 129,
+ },
)
- assert paint.Format == ot.Paint.Format.PaintRotate
- assert paint.Paint.Format == ot.Paint.Format.PaintGlyph
- assert paint.angle.value == 15
- assert paint.centerX.value == 127
- assert paint.centerY.value == 129
+ assert paint.Format == fmt
+ assert paint.Paint.Format == ot.PaintFormat.PaintGlyph
+ assert outputMapFn(paint.xSkewAngle) == 15
+ assert outputMapFn(paint.ySkewAngle) == 42
+ assert outputMapFn(paint.centerX) == 127
+ assert outputMapFn(paint.centerY) == 129
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,
- )
+ assert not _is_var(ot.PaintFormat.PaintSkew)
+ checkBuildPaintSkew(ot.PaintFormat.PaintSkew)
+
- assert paint.Format == ot.Paint.Format.PaintSkew
- assert paint.Paint.Format == ot.Paint.Format.PaintGlyph
- assert paint.xSkewAngle.value == 15
- assert paint.ySkewAngle.value == 42
- assert paint.centerX.value == 127
- assert paint.centerY.value == 129
+def test_buildPaintVarSkew():
+ assert _is_var(ot.PaintFormat.PaintVarSkew)
+ checkBuildPaintSkew(ot.PaintFormat.PaintVarSkew)
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.PaintVarSolid, 1), "c"),
+ ],
+ ),
+ "d": (
+ ot.PaintFormat.PaintColrLayers,
+ [
+ (
+ ot.PaintFormat.PaintGlyph,
+ {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "Color": {"PaletteIndex": 2, "Alpha": 0.8},
+ },
+ "e",
+ ),
+ (
+ ot.PaintFormat.PaintGlyph,
+ {
+ "Format": int(ot.PaintFormat.PaintVarRadialGradient),
+ "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,
@@ -677,35 +1008,38 @@ def test_buildColrV1():
def test_buildColrV1_more_than_255_paints():
num_paints = 364
colorGlyphs = {
- "a": [
- {
- "format": 5, # 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
assert len(paints) == num_paints + 1
- assert all(paints[i].Format == ot.Paint.Format.PaintGlyph for i in range(255))
+ assert all(paints[i].Format == ot.PaintFormat.PaintGlyph for i in range(255))
- assert paints[255].Format == ot.Paint.Format.PaintColrLayers
+ assert paints[255].Format == ot.PaintFormat.PaintColrLayers
assert paints[255].FirstLayerIndex == 0
assert paints[255].NumLayers == 255
assert all(
- paints[i].Format == ot.Paint.Format.PaintGlyph
+ paints[i].Format == ot.PaintFormat.PaintGlyph
for i in range(256, num_paints + 1)
)
assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a"
assert (
- baseGlyphs.BaseGlyphV1Record[0].Paint.Format == ot.Paint.Format.PaintColrLayers
+ baseGlyphs.BaseGlyphV1Record[0].Paint.Format == ot.PaintFormat.PaintColrLayers
)
assert baseGlyphs.BaseGlyphV1Record[0].Paint.FirstLayerIndex == 255
assert baseGlyphs.BaseGlyphV1Record[0].Paint.NumLayers == num_paints + 1 - 255
@@ -727,9 +1061,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)
@@ -775,32 +1107,35 @@ def assertNoV0Content(colr):
def test_build_layerv1list_empty():
- # Nobody uses PaintColrLayers (format 8), no layerlist
+ # Nobody uses PaintColrLayers, no layerlist
colr = builder.buildCOLR(
{
- "a": {
- "format": 5, # PaintGlyph
- "paint": {"format": 2, "paletteIndex": 2, "alpha": 0.8},
- "glyph": "b",
- },
- # A list of 1 shouldn't become a PaintColrLayers
- "b": [
- {
- "format": 5, # 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)
@@ -818,9 +1153,9 @@ def _paint_names(paints) -> List[str]:
# semi-readable assertions on a LayerV1List order.
result = []
for paint in paints:
- if paint.Format == int(ot.Paint.Format.PaintGlyph):
+ if paint.Format == int(ot.PaintFormat.PaintGlyph):
result.append(paint.Glyph)
- elif paint.Format == int(ot.Paint.Format.PaintColrLayers):
+ elif paint.Format == int(ot.PaintFormat.PaintColrLayers):
result.append(
f"Layers[{paint.FirstLayerIndex}:{paint.FirstLayerIndex+paint.NumLayers}]"
)
@@ -830,35 +1165,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": 5, # PaintGlyph
- "paint": solid_paint,
- "glyph": "back",
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": solid_paint,
+ "Glyph": "back",
}
a_foreground = {
- "format": 5, # PaintGlyph
- "paint": solid_paint,
- "glyph": "a_fore",
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": solid_paint,
+ "Glyph": "a_fore",
}
b_foreground = {
- "format": 5, # 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)
@@ -879,47 +1221,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": 5, # PaintGlyph
- "paint": solid_paint,
- "glyph": "back1",
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": solid_paint,
+ "Glyph": "back1",
},
{
- "format": 5, # PaintGlyph
- "paint": solid_paint,
- "glyph": "back2",
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": solid_paint,
+ "Glyph": "back2",
},
]
a_foreground = {
- "format": 5, # PaintGlyph
- "paint": solid_paint,
- "glyph": "a_fore",
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": solid_paint,
+ "Glyph": "a_fore",
}
b_background = {
- "format": 5, # PaintGlyph
- "paint": solid_paint,
- "glyph": "b_back",
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": solid_paint,
+ "Glyph": "b_back",
}
b_foreground = {
- "format": 5, # PaintGlyph
- "paint": solid_paint,
- "glyph": "b_fore",
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": solid_paint,
+ "Glyph": "b_fore",
}
c_background = {
- "format": 5, # 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)
@@ -951,9 +1297,12 @@ def test_build_layerv1list_with_sharing():
def test_build_layerv1list_with_overlaps():
paints = [
{
- "format": 5, # 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"
]
@@ -961,10 +1310,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)
@@ -994,6 +1344,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)]})
@@ -1005,38 +1375,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)
@@ -1049,19 +1436,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)
@@ -1087,13 +1485,55 @@ 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")
assert isinstance(colr.table, ot.COLR)
assert colr.table.VarStore is None
+ def test_paint_one_colr_layers(self):
+ # A set of one layers should flip to just that layer
+ colr = builder.buildCOLR(
+ {
+ "a": (
+ ot.PaintFormat.PaintColrLayers,
+ [
+ (
+ ot.PaintFormat.PaintGlyph,
+ (ot.PaintFormat.PaintSolid, 0),
+ "b",
+ ),
+ ],
+ )
+ },
+ )
+
+ assert len(colr.table.LayerV1List.Paint) == 0, "PaintColrLayers should be gone"
+ assert colr.table.BaseGlyphV1List.BaseGlyphCount == 1
+ paint = colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].Paint
+ assert paint.Format == ot.PaintFormat.PaintGlyph
+ assert paint.Paint.Format == ot.PaintFormat.PaintSolid
+
class TrickyRadialGradientTest:
@staticmethod
diff --git a/Tests/colorLib/table_builder_test.py b/Tests/colorLib/table_builder_test.py
new file mode 100644
index 00000000..d0a76f5a
--- /dev/null
+++ b/Tests/colorLib/table_builder_test.py
@@ -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"
diff --git a/Tests/colorLib/unbuilder_test.py b/Tests/colorLib/unbuilder_test.py
new file mode 100644
index 00000000..81169e03
--- /dev/null
+++ b/Tests/colorLib/unbuilder_test.py
@@ -0,0 +1,210 @@
+from fontTools.ttLib.tables import otTables as ot
+from fontTools.colorLib.builder import buildColrV1
+from fontTools.colorLib.unbuilder import unbuildColrV1
+import pytest
+
+
+TEST_COLOR_GLYPHS = {
+ "glyph00010": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "Color": {"PaletteIndex": 2, "Alpha": 0.5},
+ },
+ "Glyph": "glyph00011",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintVarLinearGradient),
+ "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",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintVarTransform),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintRadialGradient),
+ "ColorLine": {
+ "Extend": "pad",
+ "ColorStop": [
+ {
+ "StopOffset": 0,
+ "Color": {"PaletteIndex": 6, "Alpha": 1.0},
+ },
+ {
+ "StopOffset": 1.0,
+ "Color": {"PaletteIndex": 7, "Alpha": 0.4},
+ },
+ ],
+ },
+ "x0": 7,
+ "y0": 8,
+ "r0": 9,
+ "x1": 10,
+ "y1": 11,
+ "r1": 12,
+ },
+ "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.PaintVarTranslate),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintRotate),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintVarSkew),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "Color": {"PaletteIndex": 2, "Alpha": 0.5},
+ },
+ "Glyph": "glyph00011",
+ },
+ "xSkewAngle": (-11.0, 0),
+ "ySkewAngle": (5.0, 0),
+ "centerX": (253.0, 0),
+ "centerY": (254.0, 0),
+ },
+ "angle": 45.0,
+ "centerX": 255.0,
+ "centerY": 256.0,
+ },
+ "dx": (257.0, 0),
+ "dy": (258.0, 0),
+ },
+ ],
+ },
+ "glyph00014": {
+ "Format": int(ot.PaintFormat.PaintComposite),
+ "SourcePaint": {
+ "Format": int(ot.PaintFormat.PaintColrGlyph),
+ "Glyph": "glyph00010",
+ },
+ "CompositeMode": "src_over",
+ "BackdropPaint": {
+ "Format": int(ot.PaintFormat.PaintTransform),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintColrGlyph),
+ "Glyph": "glyph00010",
+ },
+ "Transform": {
+ "xx": 1.0,
+ "yx": 0.0,
+ "xy": 0.0,
+ "yy": 1.0,
+ "dx": 300.0,
+ "dy": 0.0,
+ },
+ },
+ },
+ "glyph00015": {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSweepGradient),
+ "ColorLine": {
+ "Extend": "pad",
+ "ColorStop": [
+ {
+ "StopOffset": 0.0,
+ "Color": {"PaletteIndex": 3, "Alpha": 1.0},
+ },
+ {
+ "StopOffset": 1.0,
+ "Color": {"PaletteIndex": 5, "Alpha": 1.0},
+ },
+ ],
+ },
+ "centerX": 259,
+ "centerY": 300,
+ "startAngle": 45.0,
+ "endAngle": 135.0,
+ },
+ "Glyph": "glyph00011",
+ },
+ "glyph00016": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintVarSolid),
+ "Color": {"PaletteIndex": 2, "Alpha": (0.5, 0)},
+ },
+ "Glyph": "glyph00011",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintVarLinearGradient),
+ "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():
+ layersV1, baseGlyphsV1 = buildColrV1(TEST_COLOR_GLYPHS)
+ colorGlyphs = unbuildColrV1(layersV1, baseGlyphsV1)
+ assert colorGlyphs == TEST_COLOR_GLYPHS
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 151cd896..279e8ca8 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -73,7 +73,7 @@ class BuilderTest(unittest.TestCase):
LigatureSubtable AlternateSubtable MultipleSubstSubtable
SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
- GSUB_5_formats
+ GSUB_5_formats delete_glyph
""".split()
def __init__(self, methodName):
diff --git a/Tests/feaLib/data/delete_glyph.fea b/Tests/feaLib/data/delete_glyph.fea
new file mode 100644
index 00000000..36e0f0f9
--- /dev/null
+++ b/Tests/feaLib/data/delete_glyph.fea
@@ -0,0 +1,3 @@
+feature test {
+ sub a by NULL;
+} test;
diff --git a/Tests/feaLib/data/delete_glyph.ttx b/Tests/feaLib/data/delete_glyph.ttx
new file mode 100644
index 00000000..777f6e36
--- /dev/null
+++ b/Tests/feaLib/data/delete_glyph.ttx
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="test"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="2"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <MultipleSubst index="0">
+ <Substitution in="a" out=""/>
+ </MultipleSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/fontBuilder/data/test_var.otf.ttx b/Tests/fontBuilder/data/test_var.otf.ttx
index ccf64dc6..09246e5b 100644
--- a/Tests/fontBuilder/data/test_var.otf.ttx
+++ b/Tests/fontBuilder/data/test_var.otf.ttx
@@ -141,9 +141,6 @@
Test Axis
</namerecord>
<namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
- TotallyNormal
- </namerecord>
- <namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True">
TotallyTested
</namerecord>
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
@@ -165,9 +162,6 @@
Test Axis
</namerecord>
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
- TotallyNormal
- </namerecord>
- <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
TotallyTested
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
@@ -290,12 +284,12 @@
</Axis>
<!-- TotallyNormal -->
- <NamedInstance flags="0x0" subfamilyNameID="257">
+ <NamedInstance flags="0x0" subfamilyNameID="2">
<coord axis="TEST" value="0.0"/>
</NamedInstance>
<!-- TotallyTested -->
- <NamedInstance flags="0x0" subfamilyNameID="258">
+ <NamedInstance flags="0x0" subfamilyNameID="257">
<coord axis="TEST" value="100.0"/>
</NamedInstance>
</fvar>
diff --git a/Tests/fontBuilder/data/test_var.ttf.ttx b/Tests/fontBuilder/data/test_var.ttf.ttx
index ed8fd307..781bb646 100644
--- a/Tests/fontBuilder/data/test_var.ttf.ttx
+++ b/Tests/fontBuilder/data/test_var.ttf.ttx
@@ -199,12 +199,9 @@
Down
</namerecord>
<namerecord nameID="260" platformID="1" platEncID="0" langID="0x0" unicode="True">
- TotallyNormal
- </namerecord>
- <namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Right Up
</namerecord>
- <namerecord nameID="262" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ <namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Neutral
</namerecord>
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
@@ -235,12 +232,9 @@
Down
</namerecord>
<namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
- TotallyNormal
- </namerecord>
- <namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Right Up
</namerecord>
- <namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
+ <namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Neutral
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
@@ -400,7 +394,7 @@
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
<Flags value="2"/>
- <ValueNameID value="262"/> <!-- Neutral -->
+ <ValueNameID value="261"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="1" Format="1">
@@ -412,7 +406,7 @@
<AxisValue index="2" Format="1">
<AxisIndex value="1"/>
<Flags value="2"/>
- <ValueNameID value="262"/> <!-- Neutral -->
+ <ValueNameID value="261"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="3" Format="1">
@@ -424,7 +418,7 @@
<AxisValue index="4" Format="1">
<AxisIndex value="2"/>
<Flags value="2"/>
- <ValueNameID value="262"/> <!-- Neutral -->
+ <ValueNameID value="261"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="5" Format="1">
@@ -436,7 +430,7 @@
<AxisValue index="6" Format="1">
<AxisIndex value="3"/>
<Flags value="2"/>
- <ValueNameID value="262"/> <!-- Neutral -->
+ <ValueNameID value="261"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="7" Format="1">
@@ -492,7 +486,7 @@
</Axis>
<!-- TotallyNormal -->
- <NamedInstance flags="0x0" subfamilyNameID="260">
+ <NamedInstance flags="0x0" subfamilyNameID="2">
<coord axis="LEFT" value="0.0"/>
<coord axis="RGHT" value="0.0"/>
<coord axis="UPPP" value="0.0"/>
@@ -500,7 +494,7 @@
</NamedInstance>
<!-- Right Up -->
- <NamedInstance flags="0x0" subfamilyNameID="261">
+ <NamedInstance flags="0x0" subfamilyNameID="260">
<coord axis="LEFT" value="0.0"/>
<coord axis="RGHT" value="100.0"/>
<coord axis="UPPP" value="100.0"/>
diff --git a/Tests/misc/arrayTools_test.py b/Tests/misc/arrayTools_test.py
index 127f153c..73e0ab17 100644
--- a/Tests/misc/arrayTools_test.py
+++ b/Tests/misc/arrayTools_test.py
@@ -1,7 +1,7 @@
from fontTools.misc.py23 import *
from fontTools.misc.py23 import round3
from fontTools.misc.arrayTools import (
- calcBounds, calcIntBounds, updateBounds, pointInRect, pointsInRect,
+ Vector, calcBounds, calcIntBounds, updateBounds, pointInRect, pointsInRect,
vectorLength, asInt16, normRect, scaleRect, offsetRect, insetRect,
sectRect, unionRect, rectCenter, intRect)
import math
@@ -88,3 +88,14 @@ def test_rectCenter():
def test_intRect():
assert intRect((0.9, 2.9, 3.1, 4.1)) == (0, 2, 4, 5)
+
+
+def test_Vector():
+ v = Vector([100, 200])
+ assert v == Vector([100, 200])
+ assert v == [100, 200]
+ assert v + Vector([1, 2]) == [101, 202]
+ assert v - Vector([1, 2]) == [99, 198]
+ assert v * 2 == [200, 400]
+ assert v * 0.5 == [50, 100]
+ assert v / 2 == [50, 100]
diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py
index d37634f1..370f9b62 100644
--- a/Tests/subset/subset_test.py
+++ b/Tests/subset/subset_test.py
@@ -3,7 +3,9 @@ from fontTools.misc.py23 import *
from fontTools.misc.testTools import getXML
from fontTools import subset
from fontTools.fontBuilder import FontBuilder
+from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.ttLib import TTFont, newTable
+from fontTools.ttLib.tables import otTables as ot
from fontTools.misc.loggingTools import CapturingLogHandler
import difflib
import logging
@@ -930,5 +932,256 @@ def test_subset_empty_glyf(tmp_path, ttf_path):
assert all(loc == 0 for loc in loca)
+@pytest.fixture
+def colrv1_path(tmp_path):
+ base_glyph_names = ["uni%04X" % i for i in range(0xE000, 0xE000 + 10)]
+ layer_glyph_names = ["glyph%05d" % i for i in range(10, 20)]
+ glyph_order = [".notdef"] + base_glyph_names + layer_glyph_names
+
+ pen = TTGlyphPen(glyphSet=None)
+ pen.moveTo((0, 0))
+ pen.lineTo((0, 500))
+ pen.lineTo((500, 500))
+ pen.lineTo((500, 0))
+ pen.closePath()
+ glyph = pen.glyph()
+ glyphs = {g: glyph for g in glyph_order}
+
+ fb = FontBuilder(unitsPerEm=1024, isTTF=True)
+ fb.setupGlyphOrder(glyph_order)
+ fb.setupCharacterMap({int(name[3:], 16): name for name in base_glyph_names})
+ fb.setupGlyf(glyphs)
+ fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order})
+ fb.setupHorizontalHeader()
+ fb.setupOS2()
+ fb.setupPost()
+ fb.setupNameTable({"familyName": "TestCOLRv1", "styleName": "Regular"})
+
+ fb.setupCOLR(
+ {
+ "uniE000": (
+ ot.PaintFormat.PaintColrLayers,
+ [
+ {
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": (ot.PaintFormat.PaintSolid, 0),
+ "Glyph": "glyph00010",
+ },
+ {
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": (ot.PaintFormat.PaintSolid, (2, 0.3)),
+ "Glyph": "glyph00011",
+ },
+ ],
+ ),
+ "uniE001": (
+ ot.PaintFormat.PaintColrLayers,
+ [
+ {
+ "Format": ot.PaintFormat.PaintTransform,
+ "Paint": {
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": {
+ "Format": ot.PaintFormat.PaintRadialGradient,
+ "x0": 250,
+ "y0": 250,
+ "r0": 250,
+ "x1": 200,
+ "y1": 200,
+ "r1": 0,
+ "ColorLine": {
+ "ColorStop": [(0.0, 1), (1.0, 2)],
+ "Extend": "repeat",
+ },
+ },
+ "Glyph": "glyph00012",
+ },
+ "Transform": (0.7071, 0.7071, -0.7071, 0.7071, 0, 0),
+ },
+ {
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": (ot.PaintFormat.PaintSolid, (1, 0.5)),
+ "Glyph": "glyph00013",
+ },
+ ],
+ ),
+ "uniE002": (
+ ot.PaintFormat.PaintColrLayers,
+ [
+ {
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": {
+ "Format": ot.PaintFormat.PaintLinearGradient,
+ "x0": 0,
+ "y0": 0,
+ "x1": 500,
+ "y1": 500,
+ "x2": -500,
+ "y2": 500,
+ "ColorLine": {"ColorStop": [(0.0, 1), (1.0, 2)]},
+ },
+ "Glyph": "glyph00014",
+ },
+ {
+ "Format": ot.PaintFormat.PaintTransform,
+ "Paint": {
+ "Format": ot.PaintFormat.PaintGlyph,
+ "Paint": (ot.PaintFormat.PaintSolid, 1),
+ "Glyph": "glyph00015",
+ },
+ "Transform": (1, 0, 0, 1, 400, 400),
+ },
+ ],
+ ),
+ "uniE003": {
+ "Format": ot.PaintFormat.PaintRotate,
+ "Paint": {
+ "Format": ot.PaintFormat.PaintColrGlyph,
+ "Glyph": "uniE001",
+ },
+ "angle": 45,
+ "centerX": 250,
+ "centerY": 250,
+ },
+ "uniE004": [
+ ("glyph00016", 1),
+ ("glyph00017", 2),
+ ],
+ },
+ )
+ fb.setupCPAL(
+ [
+ [
+ (1.0, 0.0, 0.0, 1.0), # red
+ (0.0, 1.0, 0.0, 1.0), # green
+ (0.0, 0.0, 1.0, 1.0), # blue
+ ],
+ ],
+ )
+
+ output_path = tmp_path / "TestCOLRv1.ttf"
+ fb.save(output_path)
+
+ return output_path
+
+
+def test_subset_COLRv1_and_CPAL(colrv1_path):
+ subset_path = colrv1_path.parent / (colrv1_path.name + ".subset")
+
+ subset.main(
+ [
+ str(colrv1_path),
+ "--glyph-names",
+ f"--output-file={subset_path}",
+ "--unicodes=E002,E003,E004",
+ ]
+ )
+ subset_font = TTFont(subset_path)
+
+ glyph_set = set(subset_font.getGlyphOrder())
+
+ # uniE000 and its children are excluded from subset
+ assert "uniE000" not in glyph_set
+ assert "glyph00010" not in glyph_set
+ assert "glyph00011" not in glyph_set
+
+ # uniE001 and children are pulled in indirectly as PaintColrGlyph by uniE003
+ assert "uniE001" in glyph_set
+ assert "glyph00012" in glyph_set
+ assert "glyph00013" in glyph_set
+
+ assert "uniE002" in glyph_set
+ assert "glyph00014" in glyph_set
+ assert "glyph00015" in glyph_set
+
+ assert "uniE003" in glyph_set
+
+ assert "uniE004" in glyph_set
+ assert "glyph00016" in glyph_set
+ assert "glyph00017" in glyph_set
+
+ assert "COLR" in subset_font
+ colr = subset_font["COLR"].table
+ assert colr.Version == 1
+ assert len(colr.BaseGlyphRecordArray.BaseGlyphRecord) == 1
+ assert len(colr.BaseGlyphV1List.BaseGlyphV1Record) == 3 # was 4
+
+ base = colr.BaseGlyphV1List.BaseGlyphV1Record[0]
+ assert base.BaseGlyph == "uniE001"
+ layers = colr.LayerV1List.Paint[
+ base.Paint.FirstLayerIndex: base.Paint.FirstLayerIndex + base.Paint.NumLayers
+ ]
+ assert len(layers) == 2
+ # check v1 palette indices were remapped
+ assert layers[0].Paint.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 0
+ assert layers[0].Paint.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 1
+ assert layers[1].Paint.Color.PaletteIndex == 0
+
+ baseRecV0 = colr.BaseGlyphRecordArray.BaseGlyphRecord[0]
+ assert baseRecV0.BaseGlyph == "uniE004"
+ layersV0 = colr.LayerRecordArray.LayerRecord
+ assert len(layersV0) == 2
+ # check v0 palette indices were remapped
+ assert layersV0[0].PaletteIndex == 0
+ assert layersV0[1].PaletteIndex == 1
+
+ assert "CPAL" in subset_font
+ cpal = subset_font["CPAL"]
+ assert [
+ tuple(v / 255 for v in (c.red, c.green, c.blue, c.alpha))
+ for c in cpal.palettes[0]
+ ] == [
+ # the first color 'red' was pruned
+ (0.0, 1.0, 0.0, 1.0), # green
+ (0.0, 0.0, 1.0, 1.0), # blue
+ ]
+
+
+def test_subset_COLRv1_and_CPAL_drop_empty(colrv1_path):
+ subset_path = colrv1_path.parent / (colrv1_path.name + ".subset")
+
+ subset.main(
+ [
+ str(colrv1_path),
+ "--glyph-names",
+ f"--output-file={subset_path}",
+ "--glyphs=glyph00010",
+ ]
+ )
+ subset_font = TTFont(subset_path)
+
+ glyph_set = set(subset_font.getGlyphOrder())
+
+ assert "glyph00010" in glyph_set
+ assert "uniE000" not in glyph_set
+
+ assert "COLR" not in subset_font
+ assert "CPAL" not in subset_font
+
+
+def test_subset_COLRv1_downgrade_version(colrv1_path):
+ subset_path = colrv1_path.parent / (colrv1_path.name + ".subset")
+
+ subset.main(
+ [
+ str(colrv1_path),
+ "--glyph-names",
+ f"--output-file={subset_path}",
+ "--unicodes=E004",
+ ]
+ )
+ subset_font = TTFont(subset_path)
+
+ assert set(subset_font.getGlyphOrder()) == {
+ ".notdef",
+ "uniE004",
+ "glyph00016",
+ "glyph00017",
+ }
+
+ assert "COLR" in subset_font
+ assert subset_font["COLR"].version == 0
+
+
if __name__ == "__main__":
sys.exit(unittest.main())
diff --git a/Tests/ttLib/tables/C_O_L_R_test.py b/Tests/ttLib/tables/C_O_L_R_test.py
index 7f3f71ea..4855f58f 100644
--- a/Tests/ttLib/tables/C_O_L_R_test.py
+++ b/Tests/ttLib/tables/C_O_L_R_test.py
@@ -46,13 +46,15 @@ def dump(table, ttFont=None):
def diff_binary_fragments(font_bytes, expected_fragments):
pos = 0
prev_desc = ""
+ errors = 0
for expected_bytes, description in expected_fragments:
actual_bytes = font_bytes[pos : pos + len(expected_bytes)]
- assert (
- actual_bytes == expected_bytes
- ), f'{description} (previous "{prev_desc}", bytes: {str(font_bytes[pos:pos+16])}'
+ if actual_bytes != expected_bytes:
+ print(f'{description} (previous "{prev_desc}", actual_bytes: {"".join("%02x" % v for v in actual_bytes)} bytes: {str(font_bytes[pos:pos+16])}')
+ errors += 1
pos += len(expected_bytes)
prev_desc = description
+ assert errors == 0
assert pos == len(
font_bytes
), f"Leftover font bytes, used {pos} of {len(font_bytes)}"
@@ -106,115 +108,135 @@ COLR_V1_SAMPLE = (
(b"\x00\x00\x00 ", "Offset to LayerRecordArray from beginning of table (32)"),
(b"\x00\x03", "LayerRecordCount (3)"),
(b"\x00\x00\x00,", "Offset to BaseGlyphV1List from beginning of table (44)"),
- (b"\x00\x00\x00\x81", "Offset to LayerV1List from beginning of table (129)"),
+ (b"\x00\x00\x00\xac", "Offset to LayerV1List from beginning of table (172)"),
(b"\x00\x00\x00\x00", "Offset to VarStore (NULL)"),
(b"\x00\x06", "BaseGlyphRecord[0].BaseGlyph (6)"),
(b"\x00\x00", "BaseGlyphRecord[0].FirstLayerIndex (0)"),
- (b"\x00\x04", "BaseGlyphRecord[0].NumLayers (4)"),
+ (b"\x00\x03", "BaseGlyphRecord[0].NumLayers (3)"),
(b"\x00\x07", "LayerRecord[0].LayerGlyph (7)"),
(b"\x00\x00", "LayerRecord[0].PaletteIndex (0)"),
(b"\x00\x08", "LayerRecord[1].LayerGlyph (8)"),
(b"\x00\x01", "LayerRecord[1].PaletteIndex (1)"),
(b"\x00\t", "LayerRecord[2].LayerGlyph (9)"),
(b"\x00\x02", "LayerRecord[2].PaletteIndex (2)"),
- (b"\x00\x00\x00\x02", "BaseGlyphV1List.BaseGlyphCount (2)"),
+ # BaseGlyphV1List
+ (b"\x00\x00\x00\x03", "BaseGlyphV1List.BaseGlyphCount (3)"),
(b"\x00\n", "BaseGlyphV1List.BaseGlyphV1Record[0].BaseGlyph (10)"),
(
- b"\x00\x00\x00\x10",
- "Offset to Paint table from beginning of BaseGlyphV1List (16)",
+ b"\x00\x00\x00\x16",
+ "Offset to Paint table from beginning of BaseGlyphV1List (22)",
),
(b"\x00\x0e", "BaseGlyphV1List.BaseGlyphV1Record[1].BaseGlyph (14)"),
(
- b"\x00\x00\x00\x16",
- "Offset to Paint table from beginning of BaseGlyphV1List (22)",
+ b"\x00\x00\x00\x1c",
+ "Offset to Paint table from beginning of BaseGlyphV1List (28)",
),
+ (b"\x00\x0f", "BaseGlyphV1List.BaseGlyphV1Record[2].BaseGlyph (15)"),
+ (
+ b"\x00\x00\x00\x5b",
+ "Offset to Paint table from beginning of BaseGlyphV1List (91)",
+ ),
+ # BaseGlyphV1Record[0]
(b"\x01", "BaseGlyphV1Record[0].Paint.Format (1)"),
(b"\x04", "BaseGlyphV1Record[0].Paint.NumLayers (4)"),
(b"\x00\x00\x00\x00", "BaseGlyphV1Record[0].Paint.FirstLayerIndex (0)"),
- (b"\x0B", "BaseGlyphV1Record[1].Paint.Format (11)"),
+ # BaseGlyphV1Record[1]
+ (b"\x14", "BaseGlyphV1Record[1].Paint.Format (20)"),
(b"\x00\x00<", "Offset to SourcePaint from beginning of PaintComposite (60)"),
(b"\x03", "BaseGlyphV1Record[1].Paint.CompositeMode [SRC_OVER] (3)"),
(b"\x00\x00\x08", "Offset to BackdropPaint from beginning of PaintComposite (8)"),
- (b"\x07", "BaseGlyphV1Record[1].Paint.BackdropPaint.Format (7)"),
- (b"\x00\x00\x34", "Offset to Paint from beginning of PaintTransform (52)"),
+ (b"\x0d", "BaseGlyphV1Record[1].Paint.BackdropPaint.Format (13)"),
+ (b"\x00\x00\x34", "Offset to Paint from beginning of PaintVarTransform (52)"),
(b"\x00\x01\x00\x00\x00\x00\x00\x00", "Affine2x3.xx.value (1.0)"),
(b"\x00\x00\x00\x00\x00\x00\x00\x00", "Affine2x3.xy.value (0.0)"),
(b"\x00\x00\x00\x00\x00\x00\x00\x00", "Affine2x3.yx.value (0.0)"),
(b"\x00\x01\x00\x00\x00\x00\x00\x00", "Affine2x3.yy.value (1.0)"),
(b"\x01\x2c\x00\x00\x00\x00\x00\x00", "Affine2x3.dx.value (300.0)"),
(b"\x00\x00\x00\x00\x00\x00\x00\x00", "Affine2x3.dy.value (0.0)"),
- (b"\x06", "BaseGlyphV1Record[1].Paint.SourcePaint.Format (6)"),
+ (b"\x0b", "BaseGlyphV1Record[1].Paint.SourcePaint.Format (11)"),
(b"\x00\n", "BaseGlyphV1Record[1].Paint.SourcePaint.Glyph (10)"),
+ # BaseGlyphV1Record[2]
+ (b"\x0a", "BaseGlyphV1Record[2].Paint.Format (10)"),
+ (b"\x00\x00\x06", "Offset to Paint subtable from beginning of PaintGlyph (6)"),
+ (b"\x00\x0b", "BaseGlyphV1Record[2].Paint.Glyph (11)"),
+ (b"\x08", "BaseGlyphV1Record[2].Paint.Paint.Format (8)"),
+ (b"\x00\x00\x10", "Offset to ColorLine from beginning of PaintSweepGradient (16)"),
+ (b"\x01\x03", "centerX (259)"),
+ (b"\x01\x2c", "centerY (300)"),
+ (b"\x00\x2d\x00\x00", "startAngle (45.0)"),
+ (b"\x00\x87\x00\x00", "endAngle (135.0)"),
+ (b"\x00", "ColorLine.Extend (0; pad)"),
+ (b"\x00\x02", "ColorLine.StopCount (2)"),
+ (b"\x00\x00", "ColorLine.ColorStop[0].StopOffset (0.0)"),
+ (b"\x00\x03", "ColorLine.ColorStop[0].Color.PaletteIndex (3)"),
+ (b"@\x00", "ColorLine.ColorStop[0].Color.Alpha (1.0)"),
+ (b"@\x00", "ColorLine.ColorStop[1].StopOffset (1.0)"),
+ (b"\x00\x05", "ColorLine.ColorStop[1].Color.PaletteIndex (5)"),
+ (b"@\x00", "ColorLine.ColorStop[1].Color.Alpha (1.0)"),
+ # LayerV1List
(b"\x00\x00\x00\x04", "LayerV1List.LayerCount (4)"),
(
b"\x00\x00\x00\x14",
"First Offset to Paint table from beginning of LayerV1List (20)",
),
(
- b"\x00\x00\x00\x1a",
- "Second Offset to Paint table from beginning of LayerV1List (26)",
+ b"\x00\x00\x00\x23",
+ "Second Offset to Paint table from beginning of LayerV1List (35)",
),
(
- b"\x00\x00\x00u",
- "Third Offset to Paint table from beginning of LayerV1List (117)",
+ b"\x00\x00\x00\x4e",
+ "Third Offset to Paint table from beginning of LayerV1List (78)",
),
(
- b"\x00\x00\x00\xf6",
- "Fourth Offset to Paint table from beginning of LayerV1List (246)",
+ b"\x00\x00\x00\xb7",
+ "Fourth Offset to Paint table from beginning of LayerV1List (183)",
),
# PaintGlyph glyph00011
- (b"\x05", "LayerV1List.Paint[0].Format (5)"),
- (b"\x00\x01<", "Offset24 to Paint subtable from beginning of PaintGlyph (316)"),
+ (b"\x0a", "LayerV1List.Paint[0].Format (10)"),
+ (b"\x00\x00\x06", "Offset24 to Paint subtable from beginning of PaintGlyph (6)"),
(b"\x00\x0b", "LayerV1List.Paint[0].Glyph (glyph00011)"),
+ # PaintVarSolid
+ (b"\x03", "LayerV1List.Paint[0].Paint.Format (3)"),
+ (b"\x00\x02", "Paint.Color.PaletteIndex (2)"),
+ (b" \x00", "Paint.Color.Alpha.value (0.5)"),
+ (b"\x00\x00\x00\x00", "Paint.Color.Alpha.varIdx (0)"),
# PaintGlyph glyph00012
- (b"\x05", "LayerV1List.Paint[1].Format (5)"),
+ (b"\x0a", "LayerV1List.Paint[1].Format (10)"),
(b"\x00\x00\x06", "Offset to Paint subtable from beginning of PaintGlyph (6)"),
(b"\x00\x0c", "LayerV1List.Paint[1].Glyph (glyph00012)"),
- (b"\x03", "LayerV1List.Paint[1].Paint.Format (3)"),
- (b"\x00\x00(", "Offset to ColorLine from beginning of PaintLinearGradient (40)"),
- (b"\x00\x01", "Paint.x0.value (1)"),
- (b"\x00\x00\x00\x00", "Paint.x0.varIdx (0)"),
- (b"\x00\x02", "Paint.y0.value (2)"),
- (b"\x00\x00\x00\x00", "Paint.y0.varIdx (0)"),
- (b"\xff\xfd", "Paint.x1.value (-3)"),
- (b"\x00\x00\x00\x00", "Paint.x1.varIdx (0)"),
- (b"\xff\xfc", "Paint.y1.value (-4)"),
- (b"\x00\x00\x00\x00", "Paint.y1.varIdx (0)"),
- (b"\x00\x05", "Paint.x2.value (5)"),
- (b"\x00\x00\x00\x00", "Paint.x2.varIdx (0)"),
- (b"\x00\x06", "Paint.y2.value (6)"),
- (b"\x00\x00\x00\x00", "Paint.y2.varIdx (0)"),
+ (b"\x04", "LayerV1List.Paint[1].Paint.Format (4)"),
+ (b"\x00\x00\x10", "Offset to ColorLine from beginning of PaintLinearGradient (16)"),
+ (b"\x00\x01", "Paint.x0 (1)"),
+ (b"\x00\x02", "Paint.y0 (2)"),
+ (b"\xff\xfd", "Paint.x1 (-3)"),
+ (b"\xff\xfc", "Paint.y1 (-4)"),
+ (b"\x00\x05", "Paint.x2 (5)"),
+ (b"\x00\x06", "Paint.y2 (6)"),
(b"\x01", "ColorLine.Extend (1; repeat)"),
(b"\x00\x03", "ColorLine.StopCount (3)"),
- (b"\x00\x00", "ColorLine.ColorStop[0].StopOffset.value (0.0)"),
- (b"\x00\x00\x00\x00", "ColorLine.ColorStop[0].StopOffset.varIdx (0)"),
+ (b"\x00\x00", "ColorLine.ColorStop[0].StopOffset (0.0)"),
(b"\x00\x03", "ColorLine.ColorStop[0].Color.PaletteIndex (3)"),
- (b"@\x00", "ColorLine.ColorStop[0].Color.Alpha.value (1.0)"),
- (b"\x00\x00\x00\x00", "ColorLine.ColorStop[0].Color.Alpha.varIdx (0)"),
- (b" \x00", "ColorLine.ColorStop[1].StopOffset.value (0.5)"),
- (b"\x00\x00\x00\x00", "ColorLine.ColorStop[1].StopOffset.varIdx (0)"),
+ (b"@\x00", "ColorLine.ColorStop[0].Color.Alpha (1.0)"),
+ (b" \x00", "ColorLine.ColorStop[1].StopOffset (0.5)"),
(b"\x00\x04", "ColorLine.ColorStop[1].Color.PaletteIndex (4)"),
- (b"@\x00", "ColorLine.ColorStop[1].Color.Alpha.value (1.0)"),
- (b"\x00\x00\x00\x00", "ColorLine.ColorStop[1].Color.Alpha.varIdx (0)"),
- (b"@\x00", "ColorLine.ColorStop[2].StopOffset.value (1.0)"),
- (b"\x00\x00\x00\x00", "ColorLine.ColorStop[2].StopOffset.varIdx (0)"),
+ (b"@\x00", "ColorLine.ColorStop[1].Color.Alpha (1.0)"),
+ (b"@\x00", "ColorLine.ColorStop[2].StopOffset (1.0)"),
(b"\x00\x05", "ColorLine.ColorStop[2].Color.PaletteIndex (5)"),
- (b"@\x00", "ColorLine.ColorStop[2].Color.Alpha.value (1.0)"),
- (b"\x00\x00\x00\x00", "ColorLine.ColorStop[2].Color.Alpha.varIdx (0)"),
+ (b"@\x00", "ColorLine.ColorStop[2].Color.Alpha (1.0)"),
# PaintGlyph glyph00013
- (b"\x05", "LayerV1List.Paint[2].Format (5)"),
+ (b"\x0a", "LayerV1List.Paint[2].Format (10)"),
(b"\x00\x00\x06", "Offset to Paint subtable from beginning of PaintGlyph (6)"),
(b"\x00\r", "LayerV1List.Paint[2].Glyph (13)"),
- (b"\x07", "LayerV1List.Paint[2].Paint.Format (5)"),
- (b"\x00\x00\x34", "Offset to Paint subtable from beginning of PaintTransform (52)"),
- (b"\xff\xf3\x00\x00\x00\x00\x00\x00", "Affine2x3.xx.value (-13)"),
- (b"\x00\x0e\x00\x00\x00\x00\x00\x00", "Affine2x3.xy.value (14)"),
- (b"\x00\x0f\x00\x00\x00\x00\x00\x00", "Affine2x3.yx.value (15)"),
- (b"\xff\xef\x00\x00\x00\x00\x00\x00", "Affine2x3.yy.value (-17)"),
- (b"\x00\x12\x00\x00\x00\x00\x00\x00", "Affine2x3.yy.value (18)"),
- (b"\x00\x13\x00\x00\x00\x00\x00\x00", "Affine2x3.yy.value (19)"),
- (b"\x04", "LayerV1List.Paint[2].Paint.Paint.Format (4)"),
- (b"\x00\x00(", "Offset to ColorLine from beginning of PaintRadialGradient (40)"),
+ (b"\x0c", "LayerV1List.Paint[2].Paint.Format (12)"),
+ (b"\x00\x00\x1c", "Offset to Paint subtable from beginning of PaintTransform (28)"),
+ (b"\xff\xf3\x00\x00", "Affine2x3.xx (-13)"),
+ (b"\x00\x0e\x00\x00", "Affine2x3.xy (14)"),
+ (b"\x00\x0f\x00\x00", "Affine2x3.yx (15)"),
+ (b"\xff\xef\x00\x00", "Affine2x3.yy (-17)"),
+ (b"\x00\x12\x00\x00", "Affine2x3.yy (18)"),
+ (b"\x00\x13\x00\x00", "Affine2x3.yy (19)"),
+ (b"\x07", "LayerV1List.Paint[2].Paint.Paint.Format (7)"),
+ (b"\x00\x00(", "Offset to ColorLine from beginning of PaintVarRadialGradient (40)"),
(b"\x00\x07\x00\x00\x00\x00", "Paint.x0.value (7)"),
(b"\x00\x08\x00\x00\x00\x00", "Paint.y0.value (8)"),
(b"\x00\t\x00\x00\x00\x00", "Paint.r0.value (9)"),
@@ -230,32 +252,31 @@ COLR_V1_SAMPLE = (
(b"\x00\x07", "ColorLine.ColorStop[1].Color.PaletteIndex (7)"),
(b"\x19\x9a\x00\x00\x00\x00", "ColorLine.ColorStop[1].Color.Alpha.value (0.4)"),
# PaintTranslate
- (b"\x08", "LayerV1List.Paint[3].Format (8)"),
- (b"\x00\x00\x14", "Offset to Paint subtable from beginning of PaintTranslate (20)"),
- (b"\x01\x01\x00\x00\x00\x00\x00\x00", "dx.value (257)"),
- (b"\x01\x02\x00\x00\x00\x00\x00\x00", "dy.value (258)"),
+ (b"\x0e", "LayerV1List.Paint[3].Format (14)"),
+ (b"\x00\x00\x0c", "Offset to Paint subtable from beginning of PaintTranslate (12)"),
+ (b"\x01\x01\x00\x00", "dx (257)"),
+ (b"\x01\x02\x00\x00", "dy (258)"),
# PaintRotate
- (b"\x09", "LayerV1List.Paint[3].Paint.Format (9)"),
- (b"\x00\x00\x1c", "Offset to Paint subtable from beginning of PaintRotate (28)"),
- (b"\x00\x2d\x00\x00\x00\x00\x00\x00", "angle.value (45)"),
- (b"\x00\xff\x00\x00\x00\x00\x00\x00", "centerX.value (255)"),
- (b"\x01\x00\x00\x00\x00\x00\x00\x00", "centerY.value (256)"),
+ (b"\x10", "LayerV1List.Paint[3].Paint.Format (16)"),
+ (b"\x00\x00\x10", "Offset to Paint subtable from beginning of PaintRotate (16)"),
+ (b"\x00\x2d\x00\x00", "angle (45)"),
+ (b"\x00\xff\x00\x00", "centerX (255)"),
+ (b"\x01\x00\x00\x00", "centerY (256)"),
# PaintSkew
- (b"\x0a", "LayerV1List.Paint[3].Paint.Paint.Format (10)"),
- (b"\x00\x00\x24", "Offset to Paint subtable from beginning of PaintSkew (36)"),
- (b"\xff\xf5\x00\x00\x00\x00\x00\x00", "xSkewAngle (-11)"),
- (b"\x00\x05\x00\x00\x00\x00\x00\x00", "ySkewAngle (5)"),
- (b"\x00\xfd\x00\x00\x00\x00\x00\x00", "centerX.value (253)"),
- (b"\x00\xfe\x00\x00\x00\x00\x00\x00", "centerY.value (254)"),
+ (b"\x12", "LayerV1List.Paint[3].Paint.Paint.Format (18)"),
+ (b"\x00\x00\x14", "Offset to Paint subtable from beginning of PaintSkew (20)"),
+ (b"\xff\xf5\x00\x00", "xSkewAngle (-11)"),
+ (b"\x00\x05\x00\x00", "ySkewAngle (5)"),
+ (b"\x00\xfd\x00\x00", "centerX.value (253)"),
+ (b"\x00\xfe\x00\x00", "centerY.value (254)"),
# PaintGlyph
- (b"\x05", "LayerV1List.Paint[2].Format (5)"),
+ (b"\x0a", "LayerV1List.Paint[3].Paint.Paint.Paint.Format (10)"),
(b"\x00\x00\x06", "Offset to Paint subtable from beginning of PaintGlyph (6)"),
(b"\x00\x0b", "LayerV1List.Paint[2].Glyph (11)"),
# PaintSolid
- (b"\x02", "LayerV1List.Paint[0].Paint.Format (2)"),
+ (b"\x02", "LayerV1List.Paint[0].Paint.Paint.Paint.Paint.Format (2)"),
(b"\x00\x02", "Paint.Color.PaletteIndex (2)"),
- (b" \x00", "Paint.Color.Alpha.value (0.5)"),
- (b"\x00\x00\x00\x00", "Paint.Color.Alpha.varIdx (0)"),
+ (b" \x00", "Paint.Color.Alpha (0.5)"),
)
COLR_V1_DATA = b"".join(t[0] for t in COLR_V1_SAMPLE)
@@ -267,7 +288,7 @@ COLR_V1_XML = [
' <BaseGlyphRecord index="0">',
' <BaseGlyph value="glyph00006"/>',
' <FirstLayerIndex value="0"/>',
- ' <NumLayers value="4"/>',
+ ' <NumLayers value="3"/>',
" </BaseGlyphRecord>",
"</BaseGlyphRecordArray>",
"<LayerRecordArray>",
@@ -286,7 +307,7 @@ COLR_V1_XML = [
"</LayerRecordArray>",
"<!-- LayerRecordCount=3 -->",
"<BaseGlyphV1List>",
- " <!-- BaseGlyphCount=2 -->",
+ " <!-- BaseGlyphCount=3 -->",
' <BaseGlyphV1Record index="0">',
' <BaseGlyph value="glyph00010"/>',
' <Paint Format="1"><!-- PaintColrLayers -->',
@@ -296,13 +317,13 @@ COLR_V1_XML = [
" </BaseGlyphV1Record>",
' <BaseGlyphV1Record index="1">',
' <BaseGlyph value="glyph00014"/>',
- ' <Paint Format="11"><!-- PaintComposite -->',
- ' <SourcePaint Format="6"><!-- PaintColrGlyph -->',
+ ' <Paint Format="20"><!-- PaintComposite -->',
+ ' <SourcePaint Format="11"><!-- PaintColrGlyph -->',
' <Glyph value="glyph00010"/>',
" </SourcePaint>",
' <CompositeMode value="src_over"/>',
- ' <BackdropPaint Format="7"><!-- PaintTransform -->',
- ' <Paint Format="6"><!-- PaintColrGlyph -->',
+ ' <BackdropPaint Format="13"><!-- PaintVarTransform -->',
+ ' <Paint Format="11"><!-- PaintColrGlyph -->',
' <Glyph value="glyph00010"/>',
" </Paint>",
" <Transform>",
@@ -316,11 +337,41 @@ COLR_V1_XML = [
" </BackdropPaint>",
" </Paint>",
" </BaseGlyphV1Record>",
+ ' <BaseGlyphV1Record index="2">',
+ ' <BaseGlyph value="glyph00015"/>',
+ ' <Paint Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="8"><!-- PaintSweepGradient -->',
+ " <ColorLine>",
+ ' <Extend value="pad"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ " <Color>",
+ ' <PaletteIndex value="3"/>',
+ ' <Alpha value="1.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ " <Color>",
+ ' <PaletteIndex value="5"/>',
+ ' <Alpha value="1.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <centerX value="259"/>',
+ ' <centerY value="300"/>',
+ ' <startAngle value="45.0"/>',
+ ' <endAngle value="135.0"/>',
+ " </Paint>",
+ ' <Glyph value="glyph00011"/>',
+ " </Paint>",
+ " </BaseGlyphV1Record>",
"</BaseGlyphV1List>",
"<LayerV1List>",
" <!-- LayerCount=4 -->",
- ' <Paint index="0" Format="5"><!-- PaintGlyph -->',
- ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <Paint index="0" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="3"><!-- PaintVarSolid -->',
" <Color>",
' <PaletteIndex value="2"/>',
' <Alpha value="0.5"/>',
@@ -328,8 +379,8 @@ COLR_V1_XML = [
" </Paint>",
' <Glyph value="glyph00011"/>',
" </Paint>",
- ' <Paint index="1" Format="5"><!-- PaintGlyph -->',
- ' <Paint Format="3"><!-- PaintLinearGradient -->',
+ ' <Paint index="1" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="4"><!-- PaintLinearGradient -->',
" <ColorLine>",
' <Extend value="repeat"/>',
" <!-- StopCount=3 -->",
@@ -364,9 +415,9 @@ COLR_V1_XML = [
" </Paint>",
' <Glyph value="glyph00012"/>',
" </Paint>",
- ' <Paint index="2" Format="5"><!-- PaintGlyph -->',
- ' <Paint Format="7"><!-- PaintTransform -->',
- ' <Paint Format="4"><!-- PaintRadialGradient -->',
+ ' <Paint index="2" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="12"><!-- PaintTransform -->',
+ ' <Paint Format="7"><!-- PaintVarRadialGradient -->',
" <ColorLine>",
' <Extend value="pad"/>',
" <!-- StopCount=2 -->",
@@ -403,10 +454,10 @@ COLR_V1_XML = [
" </Paint>",
' <Glyph value="glyph00013"/>',
" </Paint>",
- ' <Paint index="3" Format="8"><!-- PaintTranslate -->',
- ' <Paint Format="9"><!-- PaintRotate -->',
- ' <Paint Format="10"><!-- PaintSkew -->',
- ' <Paint Format="5"><!-- PaintGlyph -->',
+ ' <Paint index="3" Format="14"><!-- PaintTranslate -->',
+ ' <Paint Format="16"><!-- PaintRotate -->',
+ ' <Paint Format="18"><!-- PaintSkew -->',
+ ' <Paint Format="10"><!-- PaintGlyph -->',
' <Paint Format="2"><!-- PaintSolid -->',
" <Color>",
' <PaletteIndex value="2"/>',
diff --git a/setup.cfg b/setup.cfg
index 7691af57..75ddb71b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.19.1
+current_version = 4.20.0
commit = True
tag = False
tag_name = {new_version}
diff --git a/setup.py b/setup.py
index 7f131226..e3e7b526 100755
--- a/setup.py
+++ b/setup.py
@@ -441,7 +441,7 @@ if ext_modules:
setup_params = dict(
name="fonttools",
- version="4.19.1",
+ version="4.20.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",