aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2020-11-13 20:05:31 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-11-13 20:05:31 +0000
commitfb15f40514d3a102156c2ed878d255424c9f8631 (patch)
tree192806230538dc20558de6552927db4c11b89375
parentcfd31b38e050b8d808e92610133fb7df44e2e4d2 (diff)
parentaf68f781c30c1f38ce57ef1218ebd2c49d79cf15 (diff)
downloadfonttools-fb15f40514d3a102156c2ed878d255424c9f8631.tar.gz
Upgrade fonttools to 4.17.0 am: af6cd2e5e0 am: 7c16f49eb6 am: 06febd39f2 am: af68f781c3
Original change: https://android-review.googlesource.com/c/platform/external/fonttools/+/1498016 Change-Id: I43dc02a919ca5c8369a6d860709c1e5b6516d95e
-rw-r--r--Doc/docs-requirements.txt4
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/colorLib/builder.py521
-rw-r--r--Lib/fontTools/feaLib/builder.py11
-rw-r--r--Lib/fontTools/misc/bezierTools.py4
-rw-r--r--Lib/fontTools/pens/hashPointPen.py73
-rw-r--r--Lib/fontTools/svgLib/path/parser.py69
-rw-r--r--Lib/fontTools/ttLib/tables/otBase.py54
-rw-r--r--Lib/fontTools/ttLib/tables/otConverters.py66
-rwxr-xr-xLib/fontTools/ttLib/tables/otData.py69
-rw-r--r--Lib/fontTools/ttLib/tables/otTables.py74
-rw-r--r--Lib/fontTools/varLib/__init__.py28
-rw-r--r--Lib/fontTools/varLib/varStore.py2
-rw-r--r--Lib/fonttools.egg-info/PKG-INFO17
-rw-r--r--Lib/fonttools.egg-info/SOURCES.txt4
-rw-r--r--METADATA8
-rw-r--r--NEWS.rst15
-rw-r--r--PKG-INFO17
-rw-r--r--Tests/colorLib/builder_test.py543
-rw-r--r--Tests/pens/hashPointPen_test.py138
-rw-r--r--Tests/svgLib/path/parser_test.py83
-rw-r--r--Tests/ttLib/tables/C_O_L_R_test.py291
-rw-r--r--Tests/varLib/data/FeatureVarsCustomTag.designspace77
-rw-r--r--Tests/varLib/data/test_results/FeatureVarsCustomTag.ttx180
-rw-r--r--Tests/varLib/varLib_test.py12
-rw-r--r--requirements.txt6
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py2
28 files changed, 1860 insertions, 512 deletions
diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt
index b877d4e4..bf77512f 100644
--- a/Doc/docs-requirements.txt
+++ b/Doc/docs-requirements.txt
@@ -1,3 +1,3 @@
-sphinx==3.2.1
+sphinx==3.3.0
sphinx_rtd_theme==0.5.0
-reportlab==3.5.49
+reportlab==3.5.55
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index a39ea4ba..7712d6d6 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.16.1"
+version = __version__ = "4.17.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py
index 48ade448..3d75567e 100644
--- a/Lib/fontTools/colorLib/builder.py
+++ b/Lib/fontTools/colorLib/builder.py
@@ -6,13 +6,29 @@ import collections
import copy
import enum
from functools import partial
-from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
+from typing import (
+ Any,
+ Dict,
+ Generator,
+ Iterable,
+ List,
+ Mapping,
+ Optional,
+ Sequence,
+ Tuple,
+ Type,
+ TypeVar,
+ Union,
+)
+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,
+ CompositeMode,
VariableValue,
VariableFloat,
VariableInt,
@@ -21,11 +37,11 @@ from .errors import ColorLibError
# TODO move type aliases to colorLib.types?
+T = TypeVar("T")
_Kwargs = Mapping[str, Any]
-_PaintInput = Union[int, _Kwargs, ot.Paint]
-_LayerTuple = Tuple[str, _PaintInput]
-_LayersList = Sequence[_LayerTuple]
-_ColorGlyphsDict = Dict[str, _LayersList]
+_PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]]
+_PaintInputList = Sequence[_PaintInput]
+_ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]]
_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
_Number = Union[int, float]
_ScalarInput = Union[_Number, VariableValue, Tuple[_Number, int]]
@@ -33,10 +49,15 @@ _ColorStopTuple = Tuple[_ScalarInput, int]
_ColorStopInput = Union[_ColorStopTuple, _Kwargs, ot.ColorStop]
_ColorStopsList = Sequence[_ColorStopInput]
_ExtendInput = Union[int, str, ExtendMode]
+_CompositeInput = Union[int, str, CompositeMode]
_ColorLineInput = Union[_Kwargs, ot.ColorLine]
_PointTuple = Tuple[_ScalarInput, _ScalarInput]
-_AffineTuple = Tuple[_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput]
-_AffineInput = Union[_AffineTuple, ot.Affine2x2]
+_AffineTuple = Tuple[
+ _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput
+]
+_AffineInput = Union[_AffineTuple, ot.Affine2x3]
+
+MAX_PAINT_COLR_LAYER_COUNT = 255
def populateCOLRv0(
@@ -91,14 +112,13 @@ def buildCOLR(
"""Build COLR table from color layers mapping.
Args:
- colorGlyphs: map of base glyph names to lists of (layer glyph names,
- Paint) tuples. For COLRv0, a paint is simply the color palette index
- (int); for COLRv1, paint can be either solid colors (with variable
- opacity), linear gradients or radial gradients.
+ colorGlyphs: map of base glyph name to, either list of (layer glyph name,
+ color palette index) tuples for COLRv0; or a single Paint (dict) or
+ list of Paint for COLRv1.
version: the version of COLR table. If None, the version is determined
- by the presence of gradients or variation data (varStore), which
- require version 1; otherwise, if there are only simple colors, version
- 0 is used.
+ by the presence of COLRv1 paints or variation data (varStore), which
+ require version 1; otherwise, if all base glyphs use only simple color
+ layers, version 0 is used.
glyphMap: a map from glyph names to glyph indices, as returned from
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
varStore: Optional ItemVarationStore for deltas associated with v1 layer.
@@ -113,10 +133,9 @@ def buildCOLR(
if version in (None, 0) and not varStore:
# split color glyphs into v0 and v1 and encode separately
- colorGlyphsV0, colorGlyphsV1 = _splitSolidAndGradientGlyphs(colorGlyphs)
+ colorGlyphsV0, colorGlyphsV1 = _split_color_glyphs_by_version(colorGlyphs)
if version == 0 and colorGlyphsV1:
- # TODO Derive "average" solid color from gradients?
- raise ValueError("Can't encode gradients in COLRv0")
+ raise ValueError("Can't encode COLRv1 glyphs in COLRv0")
else:
# unless explicitly requested for v1 or have variations, in which case
# we encode all color glyph as v1
@@ -131,7 +150,7 @@ def buildCOLR(
colr.BaseGlyphRecordArray = colr.LayerRecordArray = None
if colorGlyphsV1:
- colr.BaseGlyphV1List = buildBaseGlyphV1List(colorGlyphsV1, glyphMap)
+ colr.LayerV1List, colr.BaseGlyphV1List = buildColrV1(colorGlyphsV1, glyphMap)
if version is None:
version = 1 if (varStore or colorGlyphsV1) else 0
@@ -277,29 +296,16 @@ def buildCPAL(
_DEFAULT_ALPHA = VariableFloat(1.0)
-def _splitSolidAndGradientGlyphs(
+def _split_color_glyphs_by_version(
colorGlyphs: _ColorGlyphsDict,
-) -> Tuple[Dict[str, List[Tuple[str, int]]], Dict[str, List[Tuple[str, ot.Paint]]]]:
+) -> Tuple[_ColorGlyphsV0Dict, _ColorGlyphsDict]:
colorGlyphsV0 = {}
colorGlyphsV1 = {}
for baseGlyph, layers in colorGlyphs.items():
- newLayers = []
- allSolidColors = True
- for layerGlyph, paint in layers:
- paint = _to_ot_paint(paint)
- if (
- paint.Format != 1
- or paint.Color.Alpha.value != _DEFAULT_ALPHA.value
- ):
- allSolidColors = False
- newLayers.append((layerGlyph, paint))
- if allSolidColors:
- colorGlyphsV0[baseGlyph] = [
- (layerGlyph, paint.Color.PaletteIndex)
- for layerGlyph, paint in newLayers
- ]
+ if all(isinstance(l, tuple) and isinstance(l[1], int) for l in layers):
+ colorGlyphsV0[baseGlyph] = layers
else:
- colorGlyphsV1[baseGlyph] = newLayers
+ colorGlyphsV1[baseGlyph] = layers
# sanity check
assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
@@ -307,19 +313,50 @@ def _splitSolidAndGradientGlyphs(
return colorGlyphsV0, colorGlyphsV1
-def _to_variable_value(value: _ScalarInput, cls=VariableValue) -> VariableValue:
- if isinstance(value, cls):
- return value
- try:
- it = iter(value)
- except TypeError: # not iterable
- return cls(value)
- else:
- return cls._make(it)
-
-
-_to_variable_float = partial(_to_variable_value, cls=VariableFloat)
-_to_variable_int = partial(_to_variable_value, cls=VariableInt)
+def _to_variable_value(
+ value: _ScalarInput,
+ minValue: _Number,
+ maxValue: _Number,
+ cls: Type[VariableValue],
+) -> VariableValue:
+ if not isinstance(value, cls):
+ try:
+ it = iter(value)
+ except TypeError: # not iterable
+ value = cls(value)
+ else:
+ value = cls._make(it)
+ if value.value < minValue:
+ raise OverflowError(f"{cls.__name__}: {value.value} < {minValue}")
+ if 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(
@@ -327,16 +364,7 @@ def buildColorIndex(
) -> ot.ColorIndex:
self = ot.ColorIndex()
self.PaletteIndex = int(paletteIndex)
- self.Alpha = _to_variable_float(alpha)
- return self
-
-
-def buildSolidColorPaint(
- paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA
-) -> ot.Paint:
- self = ot.Paint()
- self.Format = 1
- self.Color = buildColorIndex(paletteIndex, alpha)
+ self.Alpha = _to_variable_f2dot14_float(alpha)
return self
@@ -346,20 +374,28 @@ def buildColorStop(
alpha: _ScalarInput = _DEFAULT_ALPHA,
) -> ot.ColorStop:
self = ot.ColorStop()
- self.StopOffset = _to_variable_float(offset)
+ self.StopOffset = _to_variable_f2dot14_float(offset)
self.Color = buildColorIndex(paletteIndex, alpha)
return self
-def _to_extend_mode(v: _ExtendInput) -> ExtendMode:
- if isinstance(v, ExtendMode):
+def _to_enum_value(v: Union[str, int, T], enumClass: Type[T]) -> T:
+ if isinstance(v, enumClass):
return v
elif isinstance(v, str):
try:
- return getattr(ExtendMode, v.upper())
+ return getattr(enumClass, v.upper())
except AttributeError:
- raise ValueError(f"{v!r} is not a valid ExtendMode")
- return ExtendMode(v)
+ raise ValueError(f"{v!r} is not a valid {enumClass.__name__}")
+ return enumClass(v)
+
+
+def _to_extend_mode(v: _ExtendInput) -> ExtendMode:
+ return _to_enum_value(v, ExtendMode)
+
+
+def _to_composite_mode(v: _CompositeInput) -> CompositeMode:
+ return _to_enum_value(v, CompositeMode)
def buildColorLine(
@@ -387,136 +423,269 @@ def _to_color_line(obj):
raise TypeError(obj)
-def buildLinearGradientPaint(
- colorLine: _ColorLineInput,
- p0: _PointTuple,
- p1: _PointTuple,
- p2: Optional[_PointTuple] = None,
-) -> ot.Paint:
- self = ot.Paint()
- self.Format = 2
- self.ColorLine = _to_color_line(colorLine)
-
- if p2 is None:
- p2 = copy.copy(p1)
- for i, (x, y) in enumerate((p0, p1, p2)):
- setattr(self, f"x{i}", _to_variable_int(x))
- setattr(self, f"y{i}", _to_variable_int(y))
-
- return self
-
-
-def buildAffine2x2(
- xx: _ScalarInput, xy: _ScalarInput, yx: _ScalarInput, yy: _ScalarInput
-) -> ot.Affine2x2:
- self = ot.Affine2x2()
- locs = locals()
- for attr in ("xx", "xy", "yx", "yy"):
- value = locs[attr]
- setattr(self, attr, _to_variable_float(value))
- return self
-
-
-def buildRadialGradientPaint(
- colorLine: _ColorLineInput,
- c0: _PointTuple,
- c1: _PointTuple,
- r0: _ScalarInput,
- r1: _ScalarInput,
- transform: Optional[_AffineInput] = None,
-) -> ot.Paint:
-
- self = ot.Paint()
- self.Format = 3
- self.ColorLine = _to_color_line(colorLine)
-
- for i, (x, y), r in [(0, c0, r0), (1, c1, r1)]:
- setattr(self, f"x{i}", _to_variable_int(x))
- setattr(self, f"y{i}", _to_variable_int(y))
- setattr(self, f"r{i}", _to_variable_int(r))
-
- if transform is not None and not isinstance(transform, ot.Affine2x2):
- transform = buildAffine2x2(*transform)
- self.Transform = transform
-
- return self
-
-
-def _to_ot_paint(paint: _PaintInput) -> ot.Paint:
- if isinstance(paint, ot.Paint):
- return paint
- elif isinstance(paint, int):
- paletteIndex = paint
- return buildSolidColorPaint(paletteIndex)
- elif isinstance(paint, collections.abc.Mapping):
- return buildPaint(**paint)
- raise TypeError(f"expected int, Mapping or ot.Paint, found {type(paint.__name__)}")
+def _as_tuple(obj) -> Tuple[Any, ...]:
+ # start simple, who even cares about cyclic graphs or interesting field types
+ def _tuple_safe(value):
+ if isinstance(value, enum.Enum):
+ return value
+ elif hasattr(value, "__dict__"):
+ return tuple((k, _tuple_safe(v)) for k, v in value.__dict__.items())
+ elif isinstance(value, collections.abc.MutableSequence):
+ return tuple(_tuple_safe(e) for e in value)
+ return value
+ return tuple(_tuple_safe(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):
+ yield (lbound, ubound)
+
+
+class LayerV1ListBuilder:
+ slices: List[ot.Paint]
+ layers: List[ot.Paint]
+ reusePool: Mapping[Tuple[Any, ...], int]
+
+ def __init__(self):
+ self.slices = []
+ self.layers = []
+ self.reusePool = {}
+
+ 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)
+
+ for i, (x, y), r in [(0, c0, r0), (1, c1, r1)]:
+ 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 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 paints]
+
+ # Look for reuse, with preference to longer sequences
+ found_reuse = True
+ while found_reuse:
+ found_reuse = False
+
+ ranges = sorted(
+ _reuse_ranges(len(paints)),
+ key=lambda t: (t[1] - t[0], t[1], t[0]),
+ reverse=True,
+ )
+ for lbound, ubound in ranges:
+ reuse_lbound = self.reusePool.get(_as_tuple(paints[lbound:ubound]), -1)
+ if reuse_lbound == -1:
+ continue
+ new_slice = ot.Paint()
+ new_slice.Format = int(ot.Paint.Format.PaintColrLayers)
+ new_slice.NumLayers = ubound - lbound
+ new_slice.FirstLayerIndex = reuse_lbound
+ paints = paints[:lbound] + [new_slice] + paints[ubound:]
+ found_reuse = True
+ break
+
+ ot_paint.NumLayers = len(paints)
+ ot_paint.FirstLayerIndex = len(self.layers)
+ self.layers.extend(paints)
+
+ # Register our parts for reuse
+ for lbound, ubound in _reuse_ranges(len(paints)):
+ self.reusePool[_as_tuple(paints[lbound:ubound])] = (
+ lbound + ot_paint.FirstLayerIndex
+ )
-def buildLayerV1Record(layerGlyph: str, paint: _PaintInput) -> ot.LayerV1Record:
- self = ot.LayerV1Record()
- self.LayerGlyph = layerGlyph
- self.Paint = _to_ot_paint(paint)
- return self
+ return ot_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}")
+
+ def build(self) -> ot.LayerV1List:
+ layers = ot.LayerV1List()
+ layers.LayerCount = len(self.layers)
+ layers.Paint = self.layers
+ return layers
+
+
+LayerV1ListBuilder._buildFunctions = {
+ pf.value: getattr(LayerV1ListBuilder, "build" + pf.name)
+ for pf in ot.Paint.Format
+ if pf != ot.Paint.Format.PaintColrLayers
+}
-def buildLayerV1List(
- layers: Sequence[Union[_LayerTuple, ot.LayerV1Record]]
-) -> ot.LayerV1List:
- self = ot.LayerV1List()
- self.LayerCount = len(layers)
- records = []
- for layer in layers:
- if isinstance(layer, ot.LayerV1Record):
- record = layer
- else:
- layerGlyph, paint = layer
- record = buildLayerV1Record(layerGlyph, paint)
- records.append(record)
- self.LayerV1Record = records
+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, layers: Union[_LayersList, ot.LayerV1List]
+ baseGlyph: str, layerBuilder: LayerV1ListBuilder, paint: _PaintInput
) -> ot.BaseGlyphV1List:
self = ot.BaseGlyphV1Record()
self.BaseGlyph = baseGlyph
- if not isinstance(layers, ot.LayerV1List):
- layers = buildLayerV1List(layers)
- self.LayerV1List = layers
+ self.Paint = layerBuilder.buildPaint(paint)
return self
-def buildBaseGlyphV1List(
- colorGlyphs: Union[_ColorGlyphsDict, Dict[str, ot.LayerV1List]],
+def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
+ lines = []
+ for baseGlyph, error in sorted(errors.items()):
+ lines.append(f" {baseGlyph} => {type(error).__name__}: {error}")
+ return "\n".join(lines)
+
+
+def buildColrV1(
+ colorGlyphs: _ColorGlyphsDict,
glyphMap: Optional[Mapping[str, int]] = None,
-) -> ot.BaseGlyphV1List:
+) -> Tuple[ot.LayerV1List, ot.BaseGlyphV1List]:
if glyphMap is not None:
colorGlyphItems = sorted(
colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
)
else:
colorGlyphItems = colorGlyphs.items()
- records = [
- buildBaseGlyphV1Record(baseGlyph, layers)
- for baseGlyph, layers in colorGlyphItems
- ]
- self = ot.BaseGlyphV1List()
- self.BaseGlyphCount = len(records)
- self.BaseGlyphV1Record = records
- return self
-
-
-_PAINT_BUILDERS = {
- 1: buildSolidColorPaint,
- 2: buildLinearGradientPaint,
- 3: buildRadialGradientPaint,
-}
-
-def buildPaint(format: int, **kwargs) -> ot.Paint:
- try:
- return _PAINT_BUILDERS[format](**kwargs)
- except KeyError:
- raise NotImplementedError(format)
+ errors = {}
+ baseGlyphs = []
+ layerBuilder = LayerV1ListBuilder()
+ for baseGlyph, paint in colorGlyphItems:
+ try:
+ baseGlyphs.append(buildBaseGlyphV1Record(baseGlyph, layerBuilder, paint))
+
+ except (ColorLibError, OverflowError, ValueError, TypeError) as e:
+ errors[baseGlyph] = e
+
+ if errors:
+ failed_glyphs = _format_glyph_errors(errors)
+ exc = ColorLibError(f"Failed to build BaseGlyphV1List:\n{failed_glyphs}")
+ exc.errors = errors
+ raise exc from next(iter(errors.values()))
+
+ layers = layerBuilder.build()
+ glyphs = ot.BaseGlyphV1List()
+ glyphs.BaseGlyphCount = len(baseGlyphs)
+ glyphs.BaseGlyphV1Record = baseGlyphs
+ return (layers, glyphs)
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index f8b6a33b..6baaeeb2 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -30,6 +30,7 @@ from fontTools.otlLib.error import OpenTypeLibError
from collections import defaultdict
import itertools
import logging
+import warnings
log = logging.getLogger(__name__)
@@ -707,9 +708,13 @@ class Builder(object):
continue
for ix in lookup_indices:
- self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
- str(ix)
- ]._replace(feature=key)
+ try:
+ self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
+ str(ix)
+ ]._replace(feature=key)
+ except KeyError:
+ warnings.warn("feaLib.Builder subclass needs upgrading to "
+ "stash debug information. See fonttools#2065.")
feature_key = (feature_tag, lookup_indices)
feature_index = feature_indices.get(feature_key)
diff --git a/Lib/fontTools/misc/bezierTools.py b/Lib/fontTools/misc/bezierTools.py
index bc3bea2e..659de34e 100644
--- a/Lib/fontTools/misc/bezierTools.py
+++ b/Lib/fontTools/misc/bezierTools.py
@@ -715,8 +715,8 @@ def calcCubicParameters(pt1, pt2, pt3, pt4):
x3, y3 = pt3
x4, y4 = pt4
dx, dy = pt1
- cx = (x2 -dx) * 3.0
- cy = (y2 -dy) * 3.0
+ cx = (x2 - dx) * 3.0
+ cy = (y2 - dy) * 3.0
bx = (x3 - x2) * 3.0 - cx
by = (y3 - y2) * 3.0 - cy
ax = x4 - dx - cx - bx
diff --git a/Lib/fontTools/pens/hashPointPen.py b/Lib/fontTools/pens/hashPointPen.py
new file mode 100644
index 00000000..f3276f70
--- /dev/null
+++ b/Lib/fontTools/pens/hashPointPen.py
@@ -0,0 +1,73 @@
+# Modified from https://github.com/adobe-type-tools/psautohint/blob/08b346865710ed3c172f1eb581d6ef243b203f99/python/psautohint/ufoFont.py#L800-L838
+import hashlib
+
+from fontTools.pens.pointPen import AbstractPointPen
+
+
+class HashPointPen(AbstractPointPen):
+ """
+ This pen can be used to check if a glyph's contents (outlines plus
+ components) have changed.
+
+ Components are added as the original outline plus each composite's
+ transformation.
+
+ Example: You have some TrueType hinting code for a glyph which you want to
+ compile. The hinting code specifies a hash value computed with HashPointPen
+ that was valid for the glyph's outlines at the time the hinting code was
+ written. Now you can calculate the hash for the glyph's current outlines to
+ check if the outlines have changed, which would probably make the hinting
+ code invalid.
+
+ > glyph = ufo[name]
+ > hash_pen = HashPointPen(glyph.width, ufo)
+ > glyph.drawPoints(hash_pen)
+ > ttdata = glyph.lib.get("public.truetype.instructions", None)
+ > stored_hash = ttdata.get("id", None) # The hash is stored in the "id" key
+ > if stored_hash is None or stored_hash != hash_pen.hash:
+ > logger.error(f"Glyph hash mismatch, glyph '{name}' will have no instructions in font.")
+ > else:
+ > # The hash values are identical, the outline has not changed.
+ > # Compile the hinting code ...
+ > pass
+ """
+
+ def __init__(self, glyphWidth=0, glyphSet=None):
+ self.glyphset = glyphSet
+ self.data = ["w%s" % round(glyphWidth, 9)]
+
+ @property
+ def hash(self):
+ data = "".join(self.data)
+ if len(data) >= 128:
+ data = hashlib.sha512(data.encode("ascii")).hexdigest()
+ return data
+
+ def beginPath(self, identifier=None, **kwargs):
+ pass
+
+ def endPath(self):
+ self.data.append("|")
+
+ def addPoint(
+ self,
+ pt,
+ segmentType=None,
+ smooth=False,
+ name=None,
+ identifier=None,
+ **kwargs,
+ ):
+ if segmentType is None:
+ pt_type = "o" # offcurve
+ else:
+ pt_type = segmentType[0]
+ self.data.append(f"{pt_type}{pt[0]:g}{pt[1]:+g}")
+
+ def addComponent(
+ self, baseGlyphName, transformation, identifier=None, **kwargs
+ ):
+ tr = "".join([f"{t:+}" for t in transformation])
+ self.data.append("[")
+ self.glyphset[baseGlyphName].drawPoints(self)
+ self.data.append(f"({tr})]")
diff --git a/Lib/fontTools/svgLib/path/parser.py b/Lib/fontTools/svgLib/path/parser.py
index e23d8aa8..3d8d539d 100644
--- a/Lib/fontTools/svgLib/path/parser.py
+++ b/Lib/fontTools/svgLib/path/parser.py
@@ -13,18 +13,81 @@ import re
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
+ARC_COMMANDS = set("Aa")
UPPERCASE = set('MZLHVCSQTA')
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
-FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
+FLOAT_RE = re.compile(
+ r"[-+]?" # optional sign
+ r"(?:"
+ r"(?:0|[1-9][0-9]*)(?:\.[0-9]+(?:[eE][-+]?[0-9]+)?)?" # int/float
+ r"|"
+ r"(?:\.[0-9]+(?:[eE][-+]?[0-9]+)?)" # float with leading dot (e.g. '.42')
+ r")"
+)
+BOOL_RE = re.compile("^[01]")
+SEPARATOR_RE = re.compile(f"[, \t]")
def _tokenize_path(pathdef):
+ arc_cmd = None
for x in COMMAND_RE.split(pathdef):
if x in COMMANDS:
+ arc_cmd = x if x in ARC_COMMANDS else None
yield x
- for token in FLOAT_RE.findall(x):
- yield token
+ continue
+
+ if arc_cmd:
+ try:
+ yield from _tokenize_arc_arguments(x)
+ except ValueError as e:
+ raise ValueError(f"Invalid arc command: '{arc_cmd}{x}'") from e
+ else:
+ for token in FLOAT_RE.findall(x):
+ yield token
+
+
+ARC_ARGUMENT_TYPES = (
+ ("rx", FLOAT_RE),
+ ("ry", FLOAT_RE),
+ ("x-axis-rotation", FLOAT_RE),
+ ("large-arc-flag", BOOL_RE),
+ ("sweep-flag", BOOL_RE),
+ ("x", FLOAT_RE),
+ ("y", FLOAT_RE),
+)
+
+
+def _tokenize_arc_arguments(arcdef):
+ raw_args = [s for s in SEPARATOR_RE.split(arcdef) if s]
+ if not raw_args:
+ raise ValueError(f"Not enough arguments: '{arcdef}'")
+ raw_args.reverse()
+
+ i = 0
+ while raw_args:
+ arg = raw_args.pop()
+
+ name, pattern = ARC_ARGUMENT_TYPES[i]
+ match = pattern.search(arg)
+ if not match:
+ raise ValueError(f"Invalid argument for '{name}' parameter: {arg!r}")
+
+ j, k = match.span()
+ yield arg[j:k]
+ arg = arg[k:]
+
+ if arg:
+ raw_args.append(arg)
+
+ # wrap around every 7 consecutive arguments
+ if i == 6:
+ i = 0
+ else:
+ i += 1
+
+ if i != 0:
+ raise ValueError(f"Not enough arguments: '{arcdef}'")
def parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc):
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
index aa00c97b..5ce32c17 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -208,14 +208,24 @@ class OTTableWriter(object):
"""Helper class to gather and assemble data for OpenType tables."""
- def __init__(self, localState=None, tableTag=None):
+ def __init__(self, localState=None, tableTag=None, offsetSize=2):
self.items = []
self.pos = None
self.localState = localState
self.tableTag = tableTag
- self.longOffset = False
+ self.offsetSize = offsetSize
self.parent = None
+ # DEPRECATED: 'longOffset' is kept as a property for backward compat with old code.
+ # You should use 'offsetSize' instead (2, 3 or 4 bytes).
+ @property
+ def longOffset(self):
+ return self.offsetSize == 4
+
+ @longOffset.setter
+ def longOffset(self, value):
+ self.offsetSize = 4 if value else 2
+
def __setitem__(self, name, value):
state = self.localState.copy() if self.localState else dict()
state[name] = value
@@ -236,7 +246,7 @@ class OTTableWriter(object):
if hasattr(item, "getCountData"):
l += item.size
elif hasattr(item, "getData"):
- l += 4 if item.longOffset else 2
+ l += item.offsetSize
else:
l = l + len(item)
return l
@@ -250,9 +260,9 @@ class OTTableWriter(object):
item = items[i]
if hasattr(item, "getData"):
- if item.longOffset:
+ if item.offsetSize == 4:
items[i] = packULong(item.pos - pos)
- else:
+ elif item.offsetSize == 2:
try:
items[i] = packUShort(item.pos - pos)
except struct.error:
@@ -260,6 +270,10 @@ class OTTableWriter(object):
overflowErrorRecord = self.getOverflowErrorRecord(item)
raise OTLOffsetOverflowError(overflowErrorRecord)
+ elif item.offsetSize == 3:
+ items[i] = packUInt24(item.pos - pos)
+ else:
+ raise ValueError(item.offsetSize)
return bytesjoin(items)
@@ -274,7 +288,7 @@ class OTTableWriter(object):
def __eq__(self, other):
if type(self) != type(other):
return NotImplemented
- return self.longOffset == other.longOffset and self.items == other.items
+ return self.offsetSize == other.offsetSize and self.items == other.items
def _doneWriting(self, internedTables):
# Convert CountData references to data string items
@@ -395,8 +409,8 @@ class OTTableWriter(object):
# interface for gathering data, as used by table.compile()
- def getSubWriter(self):
- subwriter = self.__class__(self.localState, self.tableTag)
+ def getSubWriter(self, offsetSize=2):
+ subwriter = self.__class__(self.localState, self.tableTag, offsetSize=offsetSize)
subwriter.parent = self # because some subtables have idential values, we discard
# the duplicates under the getAllData method. Hence some
# subtable writers can have more than one parent writer.
@@ -520,6 +534,10 @@ def packULong(value):
assert 0 <= value < 0x100000000, value
return struct.pack(">L", value)
+def packUInt24(value):
+ assert 0 <= value < 0x1000000, value
+ return struct.pack(">L", value)[1:]
+
class BaseTable(object):
@@ -811,6 +829,26 @@ class FormatSwitchingBaseTable(BaseTable):
BaseTable.toXML(self, xmlWriter, font, attrs, name)
+class UInt8FormatSwitchingBaseTable(FormatSwitchingBaseTable):
+ def readFormat(self, reader):
+ self.Format = reader.readUInt8()
+
+ def writeFormat(self, writer):
+ writer.writeUInt8(self.Format)
+
+
+formatSwitchingBaseTables = {
+ "uint16": FormatSwitchingBaseTable,
+ "uint8": UInt8FormatSwitchingBaseTable,
+}
+
+def getFormatSwitchingBaseTableClass(formatType):
+ try:
+ return formatSwitchingBaseTables[formatType]
+ except KeyError:
+ raise TypeError(f"Unsupported format type: {formatType!r}")
+
+
#
# Support for ValueRecords
#
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
index a05a7ef8..1b278410 100644
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -14,7 +14,8 @@ from .otBase import (CountReference, FormatSwitchingBaseTable,
from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
ContextualMorphAction, LigatureMorphAction,
InsertionMorphAction, MorxSubtable, VariableFloat,
- VariableInt, ExtendMode as _ExtendMode)
+ VariableInt, ExtendMode as _ExtendMode,
+ CompositeMode as _CompositeMode)
from itertools import zip_longest
from functools import partial
import struct
@@ -525,17 +526,13 @@ class StructWithLength(Struct):
class Table(Struct):
- longOffset = False
staticSize = 2
def readOffset(self, reader):
return reader.readUShort()
def writeNullOffset(self, writer):
- if self.longOffset:
- writer.writeULong(0)
- else:
- writer.writeUShort(0)
+ writer.writeUShort(0)
def read(self, reader, font, tableDict):
offset = self.readOffset(reader)
@@ -554,8 +551,7 @@ class Table(Struct):
if value is None:
self.writeNullOffset(writer)
else:
- subWriter = writer.getSubWriter()
- subWriter.longOffset = self.longOffset
+ subWriter = writer.getSubWriter(offsetSize=self.staticSize)
subWriter.name = self.name
if repeatIndex is not None:
subWriter.repeatIndex = repeatIndex
@@ -564,12 +560,26 @@ class Table(Struct):
class LTable(Table):
- longOffset = True
staticSize = 4
def readOffset(self, reader):
return reader.readULong()
+ def writeNullOffset(self, writer):
+ writer.writeULong(0)
+
+
+# Table pointed to by a 24-bit, 3-byte long offset
+class Table24(Table):
+
+ staticSize = 3
+
+ def readOffset(self, reader):
+ return reader.readUInt24()
+
+ def writeNullOffset(self, writer):
+ writer.writeUInt24(0)
+
# TODO Clean / merge the SubTable and SubStruct
@@ -905,13 +915,11 @@ class AATLookupWithDataOffset(BaseConverter):
offsetByGlyph[glyph] = offset
# For calculating the offsets to our AATLookup and data table,
# we can use the regular OTTableWriter infrastructure.
- lookupWriter = writer.getSubWriter()
- lookupWriter.longOffset = True
+ lookupWriter = writer.getSubWriter(offsetSize=4)
lookup = AATLookup('DataOffsets', None, None, UShort)
lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None)
- dataWriter = writer.getSubWriter()
- dataWriter.longOffset = True
+ dataWriter = writer.getSubWriter(offsetSize=4)
writer.writeSubTable(lookupWriter)
writer.writeSubTable(dataWriter)
for d in compiledData:
@@ -1252,8 +1260,7 @@ class STXHeader(BaseConverter):
(len(table.PerGlyphLookups), numLookups))
writer = OTTableWriter()
for lookup in table.PerGlyphLookups:
- lookupWriter = writer.getSubWriter()
- lookupWriter.longOffset = True
+ lookupWriter = writer.getSubWriter(offsetSize=4)
self.perGlyphLookup.write(lookupWriter, font,
{}, lookup, None)
writer.writeSubTable(lookupWriter)
@@ -1706,15 +1713,25 @@ class VarUInt16(_NamedTupleConverter):
converterClasses = [UShort, ULong]
-class ExtendMode(UShort):
+class _UInt8Enum(UInt8):
+ enumClass = NotImplemented
+
def read(self, reader, font, tableDict):
- return _ExtendMode(super().read(reader, font, tableDict))
- @staticmethod
- def fromString(value):
- return getattr(_ExtendMode, value.upper())
- @staticmethod
- def toString(value):
- return _ExtendMode(value).name.lower()
+ return self.enumClass(super().read(reader, font, tableDict))
+ @classmethod
+ def fromString(cls, value):
+ return getattr(cls.enumClass, value.upper())
+ @classmethod
+ def toString(cls, value):
+ return cls.enumClass(value).name.lower()
+
+
+class ExtendMode(_UInt8Enum):
+ enumClass = _ExtendMode
+
+
+class CompositeMode(_UInt8Enum):
+ enumClass = _CompositeMode
converterMapping = {
@@ -1739,12 +1756,14 @@ converterMapping = {
"struct": Struct,
"Offset": Table,
"LOffset": LTable,
+ "Offset24": Table24,
"ValueRecord": ValueRecord,
"DeltaValue": DeltaValue,
"VarIdxMapValue": VarIdxMapValue,
"VarDataValue": VarDataValue,
"LookupFlag": LookupFlag,
"ExtendMode": ExtendMode,
+ "CompositeMode": CompositeMode,
# AAT
"CIDGlyphMap": CIDGlyphMap,
@@ -1760,6 +1779,7 @@ converterMapping = {
"STXHeader": lambda C: partial(STXHeader, tableClass=C),
"OffsetTo": lambda C: partial(Table, tableClass=C),
"LOffsetTo": lambda C: partial(LTable, tableClass=C),
+ "LOffset24To": lambda C: partial(Table24, tableClass=C),
# Variable types
"VarFixed": VarFixed,
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index 8a12881e..f260a542 100755
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
@@ -1550,6 +1550,7 @@ otData = [
('LOffset', 'LayerRecordArray', None, None, 'Offset (from beginning of COLR table) to Layer Records.'),
('uint16', 'LayerRecordCount', None, None, 'Number of Layer Records.'),
('LOffset', 'BaseGlyphV1List', None, 'Version >= 1', 'Offset (from beginning of COLR table) to array of Version-1 Base Glyph records.'),
+ ('LOffset', 'LayerV1List', None, 'Version >= 1', 'Offset (from beginning of COLR table) to LayerV1List.'),
('LOffset', 'VarStore', None, 'Version >= 1', 'Offset to variation store (may be NULL)'),
]),
@@ -1579,24 +1580,21 @@ otData = [
('BaseGlyphV1Record', [
('GlyphID', 'BaseGlyph', None, None, 'Glyph ID of reference glyph.'),
- ('LOffset', 'LayerV1List', None, None, 'Offset (from beginning of BaseGlyphV1List) to LayerV1List.'),
+ ('LOffset', 'Paint', None, None, 'Offset (from beginning of BaseGlyphV1Record) to Paint, typically a PaintColrLayers.'),
]),
('LayerV1List', [
- ('uint32', 'LayerCount', None, None, 'Number of Version-1 Layer records'),
- ('struct', 'LayerV1Record', 'LayerCount', 0, 'Array of Version-1 Layer records'),
+ ('uint32', 'LayerCount', None, None, 'Number of Version-1 Layers'),
+ ('LOffset', 'Paint', 'LayerCount', 0, 'Array of offsets to Paint tables, from the start of the LayerV1List table.'),
]),
- ('LayerV1Record', [
- ('GlyphID', 'LayerGlyph', None, None, 'Glyph ID of layer glyph (must be in z-order from bottom to top).'),
- ('LOffset', 'Paint', None, None, 'Offset (from beginning of LayerV1List) to Paint subtable.'),
- ]),
-
- ('Affine2x2', [
- ('VarFixed', 'xx', None, None, ''),
- ('VarFixed', 'xy', None, None, ''),
- ('VarFixed', 'yx', None, None, ''),
- ('VarFixed', 'yy', None, None, ''),
+ ('Affine2x3', [
+ ('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'),
+ ('VarFixed', 'yy', None, None, 'y-part of y basis vector'),
+ ('VarFixed', 'dx', None, None, 'Translation in x direction'),
+ ('VarFixed', 'dy', None, None, 'Translation in y direction'),
]),
('ColorIndex', [
@@ -1616,13 +1614,19 @@ otData = [
]),
('PaintFormat1', [
- ('uint16', 'PaintFormat', None, None, 'Format identifier-format = 1'),
- ('ColorIndex', 'Color', None, None, 'A solid color paint.'),
+ ('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.'),
]),
('PaintFormat2', [
- ('uint16', 'PaintFormat', None, None, 'Format identifier-format = 2'),
- ('LOffset', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 2'),
+ ('ColorIndex', 'Color', None, None, 'A solid color paint.'),
+ ]),
+
+ ('PaintFormat3', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 3'),
+ ('Offset24', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
('VarInt16', 'x0', None, None, ''),
('VarInt16', 'y0', None, None, ''),
('VarInt16', 'x1', None, None, ''),
@@ -1631,15 +1635,38 @@ otData = [
('VarInt16', 'y2', None, None, ''),
]),
- ('PaintFormat3', [
- ('uint16', 'PaintFormat', None, None, 'Format identifier-format = 3'),
- ('LOffset', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
+ ('PaintFormat4', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 4'),
+ ('Offset24', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
('VarInt16', 'x0', None, None, ''),
('VarInt16', 'y0', None, None, ''),
('VarUInt16', 'r0', None, None, ''),
('VarInt16', 'x1', None, None, ''),
('VarInt16', 'y1', None, None, ''),
('VarUInt16', 'r1', None, None, ''),
- ('LOffsetTo(Affine2x2)', 'Transform', None, None, 'Offset (from beginning of Paint table) to Affine2x2 subtable.'),
+ ]),
+
+ ('PaintFormat5', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
+ ('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'),
+ ('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.'),
+ ]),
+
+ ('PaintFormat8', [
+ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 8'),
+ ('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 31ff0122..7a04d5aa 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -11,7 +11,10 @@ from collections import namedtuple
from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import otRound
from fontTools.misc.textTools import pad, safeEval
-from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference
+from .otBase import (
+ BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference,
+ getFormatSwitchingBaseTableClass,
+)
from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
import logging
import struct
@@ -1289,6 +1292,69 @@ class ExtendMode(IntEnum):
REFLECT = 2
+# Porter-Duff modes for COLRv1 PaintComposite:
+# https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration
+class CompositeMode(IntEnum):
+ CLEAR = 0
+ SRC = 1
+ DEST = 2
+ SRC_OVER = 3
+ DEST_OVER = 4
+ SRC_IN = 5
+ DEST_IN = 6
+ SRC_OUT = 7
+ DEST_OUT = 8
+ SRC_ATOP = 9
+ DEST_ATOP = 10
+ XOR = 11
+ SCREEN = 12
+ OVERLAY = 13
+ DARKEN = 14
+ LIGHTEN = 15
+ COLOR_DODGE = 16
+ COLOR_BURN = 17
+ HARD_LIGHT = 18
+ SOFT_LIGHT = 19
+ DIFFERENCE = 20
+ EXCLUSION = 21
+ MULTIPLY = 22
+ HSL_HUE = 23
+ HSL_SATURATION = 24
+ HSL_COLOR = 25
+ HSL_LUMINOSITY = 26
+
+
+class Paint(getFormatSwitchingBaseTableClass("uint8")):
+
+ class Format(IntEnum):
+ PaintColrLayers = 1
+ PaintSolid = 2
+ PaintLinearGradient = 3
+ PaintRadialGradient = 4
+ PaintGlyph = 5
+ PaintColrGlyph = 6
+ PaintTransform = 7
+ PaintComposite = 8
+
+ def getFormatName(self):
+ try:
+ return self.__class__.Format(self.Format).name
+ except ValueError:
+ raise NotImplementedError(f"Unknown Paint format: {self.Format}")
+
+ def toXML(self, xmlWriter, font, attrs=None, name=None):
+ tableName = name if name else self.__class__.__name__
+ if attrs is None:
+ attrs = []
+ attrs.append(("Format", self.Format))
+ xmlWriter.begintag(tableName, attrs)
+ xmlWriter.comment(self.getFormatName())
+ xmlWriter.newline()
+ self.toXML2(xmlWriter, font)
+ xmlWriter.endtag(tableName)
+ xmlWriter.newline()
+
+
# 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
# a whole bunch of fields with different names. The following dict is a mapping
@@ -1688,7 +1754,11 @@ def _buildClasses():
if m:
# XxxFormatN subtable, we only add the "base" table
name = m.group(1)
- baseClass = FormatSwitchingBaseTable
+ # the first row of a format-switching otData table describes the Format;
+ # the first column defines the type of the Format field.
+ # Currently this can be either 'uint16' or 'uint8'.
+ formatType = table[0][0]
+ baseClass = getFormatSwitchingBaseTableClass(formatType)
if name not in namespace:
# the class doesn't exist yet, so the base implementation is used.
cls = type(name, (baseClass,), {})
diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py
index e491ce23..605fda2a 100644
--- a/Lib/fontTools/varLib/__init__.py
+++ b/Lib/fontTools/varLib/__init__.py
@@ -43,6 +43,10 @@ from .errors import VarLibError, VarLibValidationError
log = logging.getLogger("fontTools.varLib")
+# This is a lib key for the designspace document. The value should be
+# an OpenType feature tag, to be used as the FeatureVariations feature.
+# If present, the DesignSpace <rules processing="..."> flag is ignored.
+FEAVAR_FEATURETAG_LIB_KEY = "com.github.fonttools.varLib.featureVarsFeatureTag"
#
# Creation routines
@@ -216,6 +220,7 @@ def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):
assert "gvar" not in font
gvar = font["gvar"] = newTable('gvar')
glyf = font['glyf']
+ defaultMasterIndex = masterModel.reverseMapping[0]
# use hhea.ascent of base master as default vertical origin when vmtx is missing
baseAscent = font['hhea'].ascent
@@ -227,6 +232,15 @@ def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):
m["glyf"].getCoordinatesAndControls(glyph, m, defaultVerticalOrigin=baseAscent)
for m in master_ttfs
]
+
+ if allData[defaultMasterIndex][1].numberOfContours != 0:
+ # If the default master is not empty, interpret empty non-default masters
+ # as missing glyphs from a sparse master
+ allData = [
+ d if d is not None and d[1].numberOfContours != 0 else None
+ for d in allData
+ ]
+
model, allData = masterModel.getSubModel(allData)
allCoords = [d[0] for d in allData]
@@ -629,7 +643,7 @@ def _merge_OTL(font, model, master_fonts, axisTags):
font['GPOS'].table.remap_device_varidxes(varidx_map)
-def _add_GSUB_feature_variations(font, axes, internal_axis_supports, rules, rulesProcessingLast):
+def _add_GSUB_feature_variations(font, axes, internal_axis_supports, rules, featureTag):
def normalize(name, value):
return models.normalizeLocation(
@@ -664,10 +678,6 @@ def _add_GSUB_feature_variations(font, axes, internal_axis_supports, rules, rule
conditional_subs.append((region, subs))
- if rulesProcessingLast:
- featureTag = 'rclt'
- else:
- featureTag = 'rvrn'
addFeatureVariations(font, conditional_subs, featureTag)
@@ -682,6 +692,7 @@ _DesignSpaceData = namedtuple(
"instances",
"rules",
"rulesProcessingLast",
+ "lib",
],
)
@@ -811,6 +822,7 @@ def load_designspace(designspace):
instances,
ds.rules,
ds.rulesProcessingLast,
+ ds.lib,
)
@@ -917,7 +929,11 @@ def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
if 'cvar' not in exclude and 'glyf' in vf:
_merge_TTHinting(vf, model, master_fonts)
if 'GSUB' not in exclude and ds.rules:
- _add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules, ds.rulesProcessingLast)
+ featureTag = ds.lib.get(
+ FEAVAR_FEATURETAG_LIB_KEY,
+ "rclt" if ds.rulesProcessingLast else "rvrn"
+ )
+ _add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules, featureTag)
if 'CFF2' not in exclude and ('CFF ' in vf or 'CFF2' in vf):
_add_CFF2(vf, model, master_fonts)
if "post" in vf:
diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py
index 3d9566a1..b28d2a65 100644
--- a/Lib/fontTools/varLib/varStore.py
+++ b/Lib/fontTools/varLib/varStore.py
@@ -68,7 +68,7 @@ class OnlineVarStoreBuilder(object):
self._outer = varDataIdx
self._data = self._store.VarData[varDataIdx]
self._cache = self._varDataCaches[key]
- if len(self._data.Item) == 0xFFF:
+ if len(self._data.Item) == 0xFFFF:
# This is full. Need new one.
varDataIdx = None
diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO
index d3261758..280bf692 100644
--- a/Lib/fonttools.egg-info/PKG-INFO
+++ b/Lib/fonttools.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.16.1
+Version: 4.17.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -264,6 +264,21 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
Changelog
~~~~~~~~~
+ 4.17.0 (released 2020-11-12)
+ ----------------------------
+
+ - [colorLib/otData] Updated to latest draft ``COLR`` v1 spec (#2092).
+ - [svgLib] Fixed parsing error when arc commands' boolean flags are not separated
+ by space or comma (#2094).
+ - [varLib] Interpret empty non-default glyphs as 'missing', if the default glyph is
+ not empty (#2082).
+ - [feaLib.builder] Only stash lookup location for ``Debg`` if ``Builder.buildLookups_``
+ has cooperated (#2065, #2067).
+ - [varLib] Fixed bug in VarStore optimizer (#2073, #2083).
+ - [varLib] Add designspace lib key for custom feavar feature tag (#2080).
+ - Add HashPointPen adapted from psautohint. With this pen, a hash value of a glyph
+ can be computed, which can later be used to detect glyph changes (#2005).
+
4.16.1 (released 2020-10-05)
----------------------------
diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt
index eaf4a9a0..f1e3365b 100644
--- a/Lib/fonttools.egg-info/SOURCES.txt
+++ b/Lib/fonttools.egg-info/SOURCES.txt
@@ -227,6 +227,7 @@ Lib/fontTools/pens/boundsPen.py
Lib/fontTools/pens/cocoaPen.py
Lib/fontTools/pens/cu2quPen.py
Lib/fontTools/pens/filterPen.py
+Lib/fontTools/pens/hashPointPen.py
Lib/fontTools/pens/momentsPen.py
Lib/fontTools/pens/perimeterPen.py
Lib/fontTools/pens/pointInsidePen.py
@@ -873,6 +874,7 @@ Tests/pens/areaPen_test.py
Tests/pens/basePen_test.py
Tests/pens/boundsPen_test.py
Tests/pens/cu2quPen_test.py
+Tests/pens/hashPointPen_test.py
Tests/pens/perimeterPen_test.py
Tests/pens/pointInsidePen_test.py
Tests/pens/pointPen_test.py
@@ -1698,6 +1700,7 @@ Tests/varLib/data/BuildAvarIdentityMaps.designspace
Tests/varLib/data/BuildAvarSingleAxis.designspace
Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace
Tests/varLib/data/FeatureVars.designspace
+Tests/varLib/data/FeatureVarsCustomTag.designspace
Tests/varLib/data/FeatureVarsWholeRange.designspace
Tests/varLib/data/FeatureVarsWholeRangeEmpty.designspace
Tests/varLib/data/InterpolateLayout.designspace
@@ -2040,6 +2043,7 @@ Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx
Tests/varLib/data/test_results/BuildMain.ttx
Tests/varLib/data/test_results/BuildTestCFF2.ttx
Tests/varLib/data/test_results/FeatureVars.ttx
+Tests/varLib/data/test_results/FeatureVarsCustomTag.ttx
Tests/varLib/data/test_results/FeatureVarsWholeRange.ttx
Tests/varLib/data/test_results/FeatureVars_rclt.ttx
Tests/varLib/data/test_results/InterpolateLayout.ttx
diff --git a/METADATA b/METADATA
index d15f6cbd..d965a378 100644
--- a/METADATA
+++ b/METADATA
@@ -7,13 +7,13 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/releases/download/4.16.1/fonttools-4.16.1.zip"
+ value: "https://github.com/fonttools/fonttools/releases/download/4.17.0/fonttools-4.17.0.zip"
}
- version: "4.16.1"
+ version: "4.17.0"
license_type: NOTICE
last_upgrade_date {
year: 2020
- month: 10
- day: 28
+ month: 11
+ day: 12
}
}
diff --git a/NEWS.rst b/NEWS.rst
index 0c4d1853..04798b39 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,18 @@
+4.17.0 (released 2020-11-12)
+----------------------------
+
+- [colorLib/otData] Updated to latest draft ``COLR`` v1 spec (#2092).
+- [svgLib] Fixed parsing error when arc commands' boolean flags are not separated
+ by space or comma (#2094).
+- [varLib] Interpret empty non-default glyphs as 'missing', if the default glyph is
+ not empty (#2082).
+- [feaLib.builder] Only stash lookup location for ``Debg`` if ``Builder.buildLookups_``
+ has cooperated (#2065, #2067).
+- [varLib] Fixed bug in VarStore optimizer (#2073, #2083).
+- [varLib] Add designspace lib key for custom feavar feature tag (#2080).
+- Add HashPointPen adapted from psautohint. With this pen, a hash value of a glyph
+ can be computed, which can later be used to detect glyph changes (#2005).
+
4.16.1 (released 2020-10-05)
----------------------------
diff --git a/PKG-INFO b/PKG-INFO
index d3261758..280bf692 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.16.1
+Version: 4.17.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -264,6 +264,21 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
Changelog
~~~~~~~~~
+ 4.17.0 (released 2020-11-12)
+ ----------------------------
+
+ - [colorLib/otData] Updated to latest draft ``COLR`` v1 spec (#2092).
+ - [svgLib] Fixed parsing error when arc commands' boolean flags are not separated
+ by space or comma (#2094).
+ - [varLib] Interpret empty non-default glyphs as 'missing', if the default glyph is
+ not empty (#2082).
+ - [feaLib.builder] Only stash lookup location for ``Debg`` if ``Builder.buildLookups_``
+ has cooperated (#2065, #2067).
+ - [varLib] Fixed bug in VarStore optimizer (#2073, #2083).
+ - [varLib] Add designspace lib key for custom feavar feature tag (#2080).
+ - Add HashPointPen adapted from psautohint. With this pen, a hash value of a glyph
+ can be computed, which can later be used to detect glyph changes (#2005).
+
4.16.1 (released 2020-10-05)
----------------------------
diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py
index e7ea6b61..41684cfd 100644
--- a/Tests/colorLib/builder_test.py
+++ b/Tests/colorLib/builder_test.py
@@ -1,8 +1,10 @@
from fontTools.ttLib import newTable
from fontTools.ttLib.tables import otTables as ot
from fontTools.colorLib import builder
+from fontTools.colorLib.builder import LayerV1ListBuilder
from fontTools.colorLib.errors import ColorLibError
import pytest
+from typing import List
def test_buildCOLR_v0():
@@ -205,21 +207,23 @@ def test_buildColorIndex():
assert c.Alpha.varIdx == 2
-def test_buildSolidColorPaint():
- p = builder.buildSolidColorPaint(0)
- assert p.Format == 1
+def test_buildPaintSolid():
+ p = LayerV1ListBuilder().buildPaintSolid(0)
+ assert p.Format == ot.Paint.Format.PaintSolid
assert p.Color.PaletteIndex == 0
assert p.Color.Alpha.value == 1.0
assert p.Color.Alpha.varIdx == 0
- p = builder.buildSolidColorPaint(1, alpha=0.5)
- assert p.Format == 1
+ p = LayerV1ListBuilder().buildPaintSolid(1, alpha=0.5)
+ assert p.Format == ot.Paint.Format.PaintSolid
assert p.Color.PaletteIndex == 1
assert p.Color.Alpha.value == 0.5
assert p.Color.Alpha.varIdx == 0
- p = builder.buildSolidColorPaint(3, alpha=builder.VariableFloat(0.5, varIdx=2))
- assert p.Format == 1
+ p = LayerV1ListBuilder().buildPaintSolid(
+ 3, alpha=builder.VariableFloat(0.5, varIdx=2)
+ )
+ assert p.Format == ot.Paint.Format.PaintSolid
assert p.Color.PaletteIndex == 3
assert p.Color.Alpha.value == 0.5
assert p.Color.Alpha.varIdx == 2
@@ -284,15 +288,18 @@ def test_buildColorLine():
] == stops
-def test_buildAffine2x2():
- matrix = builder.buildAffine2x2(1.5, 0, 0.5, 2.0)
+def test_buildAffine2x3():
+ matrix = builder.buildAffine2x3((1.5, 0, 0.5, 2.0, 1.0, -3.0))
assert matrix.xx == builder.VariableFloat(1.5)
- assert matrix.xy == builder.VariableFloat(0.0)
- assert matrix.yx == builder.VariableFloat(0.5)
+ assert matrix.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)
-def test_buildLinearGradientPaint():
+def test_buildPaintLinearGradient():
+ layerBuilder = LayerV1ListBuilder()
color_stops = [
builder.buildColorStop(0.0, 0),
builder.buildColorStop(0.5, 1),
@@ -302,23 +309,24 @@ def test_buildLinearGradientPaint():
p0 = (builder.VariableInt(100), builder.VariableInt(200))
p1 = (builder.VariableInt(150), builder.VariableInt(250))
- gradient = builder.buildLinearGradientPaint(color_line, p0, p1)
- assert gradient.Format == 2
+ 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 = builder.buildLinearGradientPaint({"stops": color_stops}, p0, p1)
+ gradient = layerBuilder.buildPaintLinearGradient({"stops": color_stops}, p0, p1)
assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
assert gradient.ColorLine.ColorStop == color_stops
- gradient = builder.buildLinearGradientPaint(color_line, p0, p1, p2=(150, 230))
+ 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)
-def test_buildRadialGradientPaint():
+def test_buildPaintRadialGradient():
+ layerBuilder = LayerV1ListBuilder()
color_stops = [
builder.buildColorStop(0.0, 0),
builder.buildColorStop(0.5, 1),
@@ -330,49 +338,43 @@ def test_buildRadialGradientPaint():
r0 = builder.VariableInt(10)
r1 = builder.VariableInt(5)
- gradient = builder.buildRadialGradientPaint(color_line, c0, c1, r0, r1)
- assert gradient.Format == 3
+ gradient = layerBuilder.buildPaintRadialGradient(color_line, c0, c1, r0, r1)
+ assert gradient.Format == ot.Paint.Format.PaintRadialGradient
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 gradient.Transform is None
- gradient = builder.buildRadialGradientPaint({"stops": color_stops}, c0, c1, r0, r1)
+ gradient = layerBuilder.buildPaintRadialGradient(
+ {"stops": color_stops}, c0, c1, r0, r1
+ )
assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
assert gradient.ColorLine.ColorStop == color_stops
- matrix = builder.buildAffine2x2(2.0, 0.0, 0.0, 2.0)
- gradient = builder.buildRadialGradientPaint(
- color_line, c0, c1, r0, r1, transform=matrix
- )
- assert gradient.Transform == matrix
-
- gradient = builder.buildRadialGradientPaint(
- color_line, c0, c1, r0, r1, transform=(2.0, 0.0, 0.0, 2.0)
- )
- assert gradient.Transform == matrix
-
-def test_buildLayerV1Record():
- layer = builder.buildLayerV1Record("a", 2)
- assert layer.LayerGlyph == "a"
- assert layer.Paint.Format == 1
+def test_buildPaintGlyph_Solid():
+ layerBuilder = LayerV1ListBuilder()
+ layer = layerBuilder.buildPaintGlyph("a", 2)
+ assert layer.Glyph == "a"
+ assert layer.Paint.Format == ot.Paint.Format.PaintSolid
assert layer.Paint.Color.PaletteIndex == 2
- layer = builder.buildLayerV1Record("a", builder.buildSolidColorPaint(3, 0.9))
- assert layer.Paint.Format == 1
+ layer = layerBuilder.buildPaintGlyph("a", layerBuilder.buildPaintSolid(3, 0.9))
+ assert layer.Paint.Format == ot.Paint.Format.PaintSolid
assert layer.Paint.Color.PaletteIndex == 3
assert layer.Paint.Color.Alpha.value == 0.9
- layer = builder.buildLayerV1Record(
+
+def test_buildPaintGlyph_LinearGradient():
+ layerBuilder = LayerV1ListBuilder()
+ layer = layerBuilder.buildPaintGlyph(
"a",
- builder.buildLinearGradientPaint(
+ layerBuilder.buildPaintLinearGradient(
{"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250)
),
)
- assert layer.Paint.Format == 2
+ assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient
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
@@ -382,9 +384,12 @@ def test_buildLayerV1Record():
assert layer.Paint.x1.value == 150
assert layer.Paint.y1.value == 250
- layer = builder.buildLayerV1Record(
+
+def test_buildPaintGlyph_RadialGradient():
+ layerBuilder = LayerV1ListBuilder()
+ layer = layerBuilder.buildPaintGlyph(
"a",
- builder.buildRadialGradientPaint(
+ layerBuilder.buildPaintRadialGradient(
{
"stops": [
(0.0, 5),
@@ -398,7 +403,7 @@ def test_buildLayerV1Record():
10,
),
)
- assert layer.Paint.Format == 3
+ assert layer.Paint.Format == ot.Paint.Format.PaintRadialGradient
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 5
assert layer.Paint.ColorLine.ColorStop[1].StopOffset.value == 0.5
@@ -414,28 +419,35 @@ def test_buildLayerV1Record():
assert layer.Paint.r1.value == 10
-def test_buildLayerV1Record_from_dict():
- layer = builder.buildLayerV1Record("a", {"format": 1, "paletteIndex": 0})
- assert layer.LayerGlyph == "a"
- assert layer.Paint.Format == 1
+def test_buildPaintGlyph_Dict_Solid():
+ layerBuilder = LayerV1ListBuilder()
+ layer = layerBuilder.buildPaintGlyph("a", {"format": 2, "paletteIndex": 0})
+ assert layer.Glyph == "a"
+ assert layer.Paint.Format == ot.Paint.Format.PaintSolid
assert layer.Paint.Color.PaletteIndex == 0
- layer = builder.buildLayerV1Record(
+
+def test_buildPaintGlyph_Dict_LinearGradient():
+ layerBuilder = LayerV1ListBuilder()
+ layer = layerBuilder.buildPaintGlyph(
"a",
{
- "format": 2,
+ "format": 3,
"colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
"p0": (0, 0),
"p1": (10, 10),
},
)
- assert layer.Paint.Format == 2
+ assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
- layer = builder.buildLayerV1Record(
+
+def test_buildPaintGlyph_Dict_RadialGradient():
+ layerBuilder = LayerV1ListBuilder()
+ layer = layerBuilder.buildPaintGlyph(
"a",
{
- "format": 3,
+ "format": 4,
"colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
"c0": (0, 0),
"c1": (10, 10),
@@ -443,68 +455,103 @@ def test_buildLayerV1Record_from_dict():
"r1": 0,
},
)
- assert layer.Paint.Format == 3
+ assert layer.Paint.Format == ot.Paint.Format.PaintRadialGradient
assert layer.Paint.r0.value == 4
-def test_buildLayerV1List():
- layers = [
- ("a", 1),
- ("b", {"format": 1, "paletteIndex": 2, "alpha": 0.5}),
- (
- "c",
- {
- "format": 2,
- "colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "repeat"},
- "p0": (100, 200),
- "p1": (150, 250),
- },
- ),
- (
- "d",
- {
- "format": 3,
- "colorLine": {
- "stops": [
- {"offset": 0.0, "paletteIndex": 5},
- {"offset": 0.5, "paletteIndex": 6, "alpha": 0.8},
- {"offset": 1.0, "paletteIndex": 7},
- ]
- },
- "c0": (50, 50),
- "c1": (75, 75),
- "r0": 30,
- "r1": 10,
- },
+def test_buildPaintColrGlyph():
+ paint = LayerV1ListBuilder().buildPaintColrGlyph("a")
+ assert paint.Format == ot.Paint.Format.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),
),
- builder.buildLayerV1Record("e", builder.buildSolidColorPaint(8)),
- ]
- layers = builder.buildLayerV1List(layers)
+ )
+
+ 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 layers.LayerCount == len(layers.LayerV1Record)
- assert all(isinstance(l, ot.LayerV1Record) for l in layers.LayerV1Record)
+ 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
+ paint = layerBuilder.buildPaintTransform(
+ (1, 0, 0, 0.3333, 10, 10),
+ {
+ "format": 4,
+ "colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
+ "c0": (100, 100),
+ "c1": (100, 100),
+ "r0": 0,
+ "r1": 50,
+ },
+ )
-def test_buildBaseGlyphV1Record():
- baseGlyphRec = builder.buildBaseGlyphV1Record("a", [("b", 0), ("c", 1)])
- assert baseGlyphRec.BaseGlyph == "a"
- assert isinstance(baseGlyphRec.LayerV1List, ot.LayerV1List)
+ 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
+
+
+def test_buildPaintComposite():
+ layerBuilder = LayerV1ListBuilder()
+ composite = layerBuilder.buildPaintComposite(
+ mode=ot.CompositeMode.SRC_OVER,
+ source={
+ "format": 8,
+ "mode": "src_over",
+ "source": {"format": 5, "glyph": "c", "paint": 2},
+ "backdrop": {"format": 5, "glyph": "b", "paint": 1},
+ },
+ backdrop=layerBuilder.buildPaintGlyph(
+ "a", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=1.0)
+ ),
+ )
- layers = builder.buildLayerV1List([("b", 0), ("c", 1)])
- baseGlyphRec = builder.buildBaseGlyphV1Record("a", layers)
- assert baseGlyphRec.BaseGlyph == "a"
- assert baseGlyphRec.LayerV1List == layers
+ 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.SourcePaint.SourcePaint.Glyph == "c"
+ assert composite.SourcePaint.SourcePaint.Paint.Format == ot.Paint.Format.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.Glyph == "b"
+ assert (
+ composite.SourcePaint.BackdropPaint.Paint.Format == ot.Paint.Format.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.Glyph == "a"
+ assert composite.BackdropPaint.Paint.Format == ot.Paint.Format.PaintSolid
+ assert composite.BackdropPaint.Paint.Color.PaletteIndex == 0
-def test_buildBaseGlyphV1List():
+def test_buildColrV1():
colorGlyphs = {
"a": [("b", 0), ("c", 1)],
"d": [
- ("e", {"format": 1, "paletteIndex": 2, "alpha": 0.8}),
+ ("e", {"format": 2, "paletteIndex": 2, "alpha": 0.8}),
(
"f",
{
- "format": 3,
+ "format": 4,
"colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "reflect"},
"c0": (0, 0),
"c1": (0, 0),
@@ -513,7 +560,7 @@ def test_buildBaseGlyphV1List():
},
),
],
- "g": builder.buildLayerV1List([("h", 5)]),
+ "g": [("h", 5)],
}
glyphMap = {
".notdef": 0,
@@ -527,39 +574,41 @@ def test_buildBaseGlyphV1List():
"h": 8,
}
- baseGlyphs = builder.buildBaseGlyphV1List(colorGlyphs, glyphMap)
+ # TODO(anthrotype) should we split into two tests? - seems two distinct validations
+ layers, baseGlyphs = builder.buildColrV1(colorGlyphs, glyphMap)
assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "d"
assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "a"
assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g"
- baseGlyphs = builder.buildBaseGlyphV1List(colorGlyphs)
+ layers, baseGlyphs = builder.buildColrV1(colorGlyphs)
assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a"
assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "d"
assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g"
-def test_splitSolidAndGradientGlyphs():
+def test_split_color_glyphs_by_version():
+ layerBuilder = LayerV1ListBuilder()
colorGlyphs = {
"a": [
("b", 0),
("c", 1),
- ("d", {"format": 1, "paletteIndex": 2}),
- ("e", builder.buildSolidColorPaint(paletteIndex=3)),
+ ("d", 2),
+ ("e", 3),
]
}
- colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
+ colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs)
assert colorGlyphsV0 == {"a": [("b", 0), ("c", 1), ("d", 2), ("e", 3)]}
assert not colorGlyphsV1
colorGlyphs = {
- "a": [("b", builder.buildSolidColorPaint(paletteIndex=0, alpha=0.0))]
+ "a": [("b", layerBuilder.buildPaintSolid(paletteIndex=0, alpha=0.0))]
}
- colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
+ colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs)
assert not colorGlyphsV0
assert colorGlyphsV1 == colorGlyphs
@@ -571,7 +620,7 @@ def test_splitSolidAndGradientGlyphs():
(
"e",
{
- "format": 2,
+ "format": 3,
"colorLine": {"stops": [(0.0, 2), (1.0, 3)]},
"p0": (0, 0),
"p1": (10, 10),
@@ -580,22 +629,246 @@ def test_splitSolidAndGradientGlyphs():
],
}
- colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
+ colorGlyphsV0, colorGlyphsV1 = builder._split_color_glyphs_by_version(colorGlyphs)
assert colorGlyphsV0 == {"a": [("b", 0)]}
assert "a" not in colorGlyphsV1
assert "c" in colorGlyphsV1
assert len(colorGlyphsV1["c"]) == 2
- layer_d = colorGlyphsV1["c"][0]
- assert layer_d[0] == "d"
- assert isinstance(layer_d[1], ot.Paint)
- assert layer_d[1].Format == 1
- layer_e = colorGlyphsV1["c"][1]
- assert layer_e[0] == "e"
- assert isinstance(layer_e[1], ot.Paint)
- assert layer_e[1].Format == 2
+def assertIsColrV1(colr):
+ assert colr.version == 1
+ assert not hasattr(colr, "ColorLayers")
+ assert hasattr(colr, "table")
+ assert isinstance(colr.table, ot.COLR)
+
+
+def assertNoV0Content(colr):
+ assert colr.table.BaseGlyphRecordCount == 0
+ assert colr.table.BaseGlyphRecordArray is None
+ assert colr.table.LayerRecordCount == 0
+ assert colr.table.LayerRecordArray is None
+
+
+def test_build_layerv1list_empty():
+ # Nobody uses PaintColrLayers (format 8), 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),
+ },
+ "glyph": "bb",
+ }
+ ],
+ }
+ )
+
+ assertIsColrV1(colr)
+ assertNoV0Content(colr)
+
+ # 2 v1 glyphs, none in LayerV1List
+ assert colr.table.BaseGlyphV1List.BaseGlyphCount == 2
+ assert len(colr.table.BaseGlyphV1List.BaseGlyphV1Record) == 2
+ assert colr.table.LayerV1List.LayerCount == 0
+ assert len(colr.table.LayerV1List.Paint) == 0
+
+
+def _paint_names(paints) -> List[str]:
+ # prints a predictable string from a paint list to enable
+ # semi-readable assertions on a LayerV1List order.
+ result = []
+ for paint in paints:
+ if paint.Format == int(ot.Paint.Format.PaintGlyph):
+ result.append(paint.Glyph)
+ elif paint.Format == int(ot.Paint.Format.PaintColrLayers):
+ result.append(
+ f"Layers[{paint.FirstLayerIndex}:{paint.FirstLayerIndex+paint.NumLayers}]"
+ )
+ return result
+
+
+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}
+ backdrop = {
+ "format": 5, # PaintGlyph
+ "paint": solid_paint,
+ "glyph": "back",
+ }
+ a_foreground = {
+ "format": 5, # PaintGlyph
+ "paint": solid_paint,
+ "glyph": "a_fore",
+ }
+ b_foreground = {
+ "format": 5, # PaintGlyph
+ "paint": solid_paint,
+ "glyph": "b_fore",
+ }
+
+ # list => PaintColrLayers, which means contents should be in LayerV1List
+ colr = builder.buildCOLR(
+ {
+ "a": [
+ backdrop,
+ a_foreground,
+ ],
+ "b": [
+ backdrop,
+ b_foreground,
+ ],
+ }
+ )
+
+ assertIsColrV1(colr)
+ assertNoV0Content(colr)
+
+ # 2 v1 glyphs, 4 paints in LayerV1List
+ # A single shared backdrop isn't worth accessing by slice
+ assert colr.table.BaseGlyphV1List.BaseGlyphCount == 2
+ assert len(colr.table.BaseGlyphV1List.BaseGlyphV1Record) == 2
+ assert colr.table.LayerV1List.LayerCount == 4
+ assert _paint_names(colr.table.LayerV1List.Paint) == [
+ "back",
+ "a_fore",
+ "back",
+ "b_fore",
+ ]
+
+
+def test_build_layerv1list_with_sharing():
+ # Three colr glyphs, each with two layers in common
+ solid_paint = {"format": 2, "paletteIndex": 2, "alpha": 0.8}
+ backdrop = [
+ {
+ "format": 5, # PaintGlyph
+ "paint": solid_paint,
+ "glyph": "back1",
+ },
+ {
+ "format": 5, # PaintGlyph
+ "paint": solid_paint,
+ "glyph": "back2",
+ },
+ ]
+ a_foreground = {
+ "format": 5, # PaintGlyph
+ "paint": solid_paint,
+ "glyph": "a_fore",
+ }
+ b_background = {
+ "format": 5, # PaintGlyph
+ "paint": solid_paint,
+ "glyph": "b_back",
+ }
+ b_foreground = {
+ "format": 5, # PaintGlyph
+ "paint": solid_paint,
+ "glyph": "b_fore",
+ }
+ c_background = {
+ "format": 5, # 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,
+ }
+ )
+
+ assertIsColrV1(colr)
+ assertNoV0Content(colr)
+
+ # 2 v1 glyphs, 4 paints in LayerV1List
+ # A single shared backdrop isn't worth accessing by slice
+ baseGlyphs = colr.table.BaseGlyphV1List.BaseGlyphV1Record
+ assert colr.table.BaseGlyphV1List.BaseGlyphCount == 3
+ assert len(baseGlyphs) == 3
+ assert _paint_names([b.Paint for b in baseGlyphs]) == [
+ "Layers[0:3]",
+ "Layers[3:6]",
+ "Layers[6:8]",
+ ]
+ assert _paint_names(colr.table.LayerV1List.Paint) == [
+ "back1",
+ "back2",
+ "a_fore",
+ "b_back",
+ "Layers[0:2]",
+ "b_fore",
+ "c_back",
+ "Layers[0:2]",
+ ]
+ assert colr.table.LayerV1List.LayerCount == 8
+
+
+def test_build_layerv1list_with_overlaps():
+ paints = [
+ {
+ "format": 5, # PaintGlyph
+ "paint": {"format": 2, "paletteIndex": 2, "alpha": 0.8},
+ "glyph": c,
+ }
+ for c in "abcdefghi"
+ ]
+
+ # list => PaintColrLayers, which means contents should be in LayerV1List
+ colr = builder.buildCOLR(
+ {
+ "a": paints[0:4],
+ "b": paints[0:6],
+ "c": paints[2:8],
+ }
+ )
+
+ assertIsColrV1(colr)
+ assertNoV0Content(colr)
+
+ baseGlyphs = colr.table.BaseGlyphV1List.BaseGlyphV1Record
+ # assert colr.table.BaseGlyphV1List.BaseGlyphCount == 2
+
+ assert _paint_names(colr.table.LayerV1List.Paint) == [
+ "a",
+ "b",
+ "c",
+ "d",
+ "Layers[0:4]",
+ "e",
+ "f",
+ "Layers[2:4]",
+ "Layers[5:7]",
+ "g",
+ "h",
+ ]
+ assert _paint_names([b.Paint for b in baseGlyphs]) == [
+ "Layers[0:4]",
+ "Layers[4:7]",
+ "Layers[7:11]",
+ ]
+ assert colr.table.LayerV1List.LayerCount == 11
class BuildCOLRTest(object):
@@ -613,7 +886,7 @@ class BuildCOLRTest(object):
(
"b",
{
- "format": 3,
+ "format": 4,
"colorLine": {
"stops": [(0.0, 0), (1.0, 1)],
"extend": "repeat",
@@ -624,13 +897,13 @@ class BuildCOLRTest(object):
"r1": 2,
},
),
- ("c", {"format": 1, "paletteIndex": 2, "alpha": 0.8}),
+ ("c", {"format": 2, "paletteIndex": 2, "alpha": 0.8}),
],
"d": [
(
"e",
{
- "format": 2,
+ "format": 3,
"colorLine": {
"stops": [(0.0, 2), (1.0, 3)],
"extend": "reflect",
@@ -639,14 +912,11 @@ class BuildCOLRTest(object):
"p1": (3, 4),
"p2": (2, 2),
},
- )
+ ),
],
}
)
- assert colr.version == 1
- assert not hasattr(colr, "ColorLayers")
- assert hasattr(colr, "table")
- assert isinstance(colr.table, ot.COLR)
+ assertIsColrV1(colr)
assert colr.table.BaseGlyphRecordCount == 0
assert colr.table.BaseGlyphRecordArray is None
assert colr.table.LayerRecordCount == 0
@@ -660,20 +930,18 @@ class BuildCOLRTest(object):
(
"e",
{
- "format": 2,
+ "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}),
],
}
)
- assert colr.version == 1
- assert not hasattr(colr, "ColorLayers")
- assert hasattr(colr, "table")
- assert isinstance(colr.table, ot.COLR)
+ assertIsColrV1(colr)
assert colr.table.VarStore is None
assert colr.table.BaseGlyphRecordCount == 1
@@ -687,15 +955,8 @@ class BuildCOLRTest(object):
colr.table.BaseGlyphV1List.BaseGlyphV1Record[0], ot.BaseGlyphV1Record
)
assert colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].BaseGlyph == "d"
- assert isinstance(
- colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].LayerV1List, ot.LayerV1List
- )
- assert (
- colr.table.BaseGlyphV1List.BaseGlyphV1Record[0]
- .LayerV1List.LayerV1Record[0]
- .LayerGlyph
- == "e"
- )
+ assert isinstance(colr.table.LayerV1List, ot.LayerV1List)
+ assert colr.table.LayerV1List.Paint[0].Glyph == "e"
def test_explicit_version_0(self):
colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0)
diff --git a/Tests/pens/hashPointPen_test.py b/Tests/pens/hashPointPen_test.py
new file mode 100644
index 00000000..6b744e66
--- /dev/null
+++ b/Tests/pens/hashPointPen_test.py
@@ -0,0 +1,138 @@
+from fontTools.misc.transform import Identity
+from fontTools.pens.hashPointPen import HashPointPen
+import pytest
+
+
+class _TestGlyph(object):
+ width = 500
+
+ def drawPoints(self, pen):
+ pen.beginPath(identifier="abc")
+ pen.addPoint((0.0, 0.0), "line", False, "start", identifier="0000")
+ pen.addPoint((10, 110), "line", False, None, identifier="0001")
+ pen.addPoint((50.0, 75.0), None, False, None, identifier="0002")
+ pen.addPoint((60.0, 50.0), None, False, None, identifier="0003")
+ pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004")
+ pen.endPath()
+
+
+class _TestGlyph2(_TestGlyph):
+ def drawPoints(self, pen):
+ pen.beginPath(identifier="abc")
+ pen.addPoint((0.0, 0.0), "line", False, "start", identifier="0000")
+ # Minor difference to _TestGlyph() is in the next line:
+ pen.addPoint((101, 10), "line", False, None, identifier="0001")
+ pen.addPoint((50.0, 75.0), None, False, None, identifier="0002")
+ pen.addPoint((60.0, 50.0), None, False, None, identifier="0003")
+ pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004")
+ pen.endPath()
+
+
+class _TestGlyph3(_TestGlyph):
+ def drawPoints(self, pen):
+ pen.beginPath(identifier="abc")
+ pen.addPoint((0.0, 0.0), "line", False, "start", identifier="0000")
+ pen.addPoint((10, 110), "line", False, None, identifier="0001")
+ pen.endPath()
+ # Same segment, but in a different path:
+ pen.beginPath(identifier="pth2")
+ pen.addPoint((50.0, 75.0), None, False, None, identifier="0002")
+ pen.addPoint((60.0, 50.0), None, False, None, identifier="0003")
+ pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004")
+ pen.endPath()
+
+
+class _TestGlyph4(_TestGlyph):
+ def drawPoints(self, pen):
+ pen.beginPath(identifier="abc")
+ pen.addPoint((0.0, 0.0), "move", False, "start", identifier="0000")
+ pen.addPoint((10, 110), "line", False, None, identifier="0001")
+ pen.addPoint((50.0, 75.0), None, False, None, identifier="0002")
+ pen.addPoint((60.0, 50.0), None, False, None, identifier="0003")
+ pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004")
+ pen.endPath()
+
+
+class _TestGlyph5(_TestGlyph):
+ def drawPoints(self, pen):
+ pen.addComponent("b", Identity)
+
+
+class HashPointPenTest(object):
+ def test_addComponent(self):
+ pen = HashPointPen(_TestGlyph().width, {"a": _TestGlyph()})
+ pen.addComponent("a", (2, 0, 0, 3, -10, 5))
+ assert pen.hash == "w500[l0+0l10+110o50+75o60+50c50+0|(+2+0+0+3-10+5)]"
+
+ def test_NestedComponents(self):
+ pen = HashPointPen(
+ _TestGlyph().width, {"a": _TestGlyph5(), "b": _TestGlyph()}
+ ) # "a" contains "b" as a component
+ pen.addComponent("a", (2, 0, 0, 3, -10, 5))
+
+ assert (
+ pen.hash
+ == "w500[[l0+0l10+110o50+75o60+50c50+0|(+1+0+0+1+0+0)](+2+0+0+3-10+5)]"
+ )
+
+ def test_outlineAndComponent(self):
+ pen = HashPointPen(_TestGlyph().width, {"a": _TestGlyph()})
+ glyph = _TestGlyph()
+ glyph.drawPoints(pen)
+ pen.addComponent("a", (2, 0, 0, 2, -10, 5))
+
+ assert (
+ pen.hash
+ == "w500l0+0l10+110o50+75o60+50c50+0|[l0+0l10+110o50+75o60+50c50+0|(+2+0+0+2-10+5)]"
+ )
+
+ def test_addComponent_missing_raises(self):
+ pen = HashPointPen(_TestGlyph().width, dict())
+ with pytest.raises(KeyError) as excinfo:
+ pen.addComponent("a", Identity)
+ assert excinfo.value.args[0] == "a"
+
+ def test_similarGlyphs(self):
+ pen = HashPointPen(_TestGlyph().width)
+ glyph = _TestGlyph()
+ glyph.drawPoints(pen)
+
+ pen2 = HashPointPen(_TestGlyph2().width)
+ glyph = _TestGlyph2()
+ glyph.drawPoints(pen2)
+
+ assert pen.hash != pen2.hash
+
+ def test_similarGlyphs2(self):
+ pen = HashPointPen(_TestGlyph().width)
+ glyph = _TestGlyph()
+ glyph.drawPoints(pen)
+
+ pen2 = HashPointPen(_TestGlyph3().width)
+ glyph = _TestGlyph3()
+ glyph.drawPoints(pen2)
+
+ assert pen.hash != pen2.hash
+
+ def test_similarGlyphs3(self):
+ pen = HashPointPen(_TestGlyph().width)
+ glyph = _TestGlyph()
+ glyph.drawPoints(pen)
+
+ pen2 = HashPointPen(_TestGlyph4().width)
+ glyph = _TestGlyph4()
+ glyph.drawPoints(pen2)
+
+ assert pen.hash != pen2.hash
+
+ def test_glyphVsComposite(self):
+ # If a glyph contains a component, the decomposed glyph should still
+ # compare false
+ pen = HashPointPen(_TestGlyph().width, {"a": _TestGlyph()})
+ pen.addComponent("a", Identity)
+
+ pen2 = HashPointPen(_TestGlyph().width)
+ glyph = _TestGlyph()
+ glyph.drawPoints(pen2)
+
+ assert pen.hash != pen2.hash
diff --git a/Tests/svgLib/path/parser_test.py b/Tests/svgLib/path/parser_test.py
index 3b130bea..d76e9952 100644
--- a/Tests/svgLib/path/parser_test.py
+++ b/Tests/svgLib/path/parser_test.py
@@ -353,3 +353,86 @@ def test_arc_pen_with_arcTo():
]
assert pen.value == expected
+
+
+@pytest.mark.parametrize(
+ "path, expected",
+ [
+ (
+ "M1-2A3-4-1.0 01.5.7",
+ [
+ ("moveTo", ((1.0, -2.0),)),
+ ("arcTo", (3.0, -4.0, -1.0, False, True, (0.5, 0.7))),
+ ("endPath", ()),
+ ],
+ ),
+ (
+ "M21.58 7.19a2.51 2.51 0 10-1.77-1.77",
+ [
+ ("moveTo", ((21.58, 7.19),)),
+ ("arcTo", (2.51, 2.51, 0.0, True, False, (19.81, 5.42))),
+ ("endPath", ()),
+ ],
+ ),
+ (
+ "M22 12a25.87 25.87 0 00-.42-4.81",
+ [
+ ("moveTo", ((22.0, 12.0),)),
+ ("arcTo", (25.87, 25.87, 0.0, False, False, (21.58, 7.19))),
+ ("endPath", ()),
+ ],
+ ),
+ (
+ "M0,0 A1.2 1.2 0 012 15.8",
+ [
+ ("moveTo", ((0.0, 0.0),)),
+ ("arcTo", (1.2, 1.2, 0.0, False, True, (2.0, 15.8))),
+ ("endPath", ()),
+ ],
+ ),
+ (
+ "M12 7a5 5 0 105 5 5 5 0 00-5-5",
+ [
+
+ ("moveTo", ((12.0, 7.0),)),
+ ("arcTo", (5.0, 5.0, 0.0, True, False, (17.0, 12.0))),
+ ("arcTo", (5.0, 5.0, 0.0, False, False, (12.0, 7.0))),
+ ("endPath", ()),
+ ],
+ )
+ ],
+)
+def test_arc_flags_without_spaces(path, expected):
+ pen = ArcRecordingPen()
+ parse_path(path, pen)
+ assert pen.value == expected
+
+
+@pytest.mark.parametrize(
+ "path", ["A", "A0,0,0,0,0,0", "A 0 0 0 0 0 0 0 0 0 0 0 0 0"]
+)
+def test_invalid_arc_not_enough_args(path):
+ pen = ArcRecordingPen()
+ with pytest.raises(ValueError, match="Invalid arc command") as e:
+ parse_path(path, pen)
+
+ assert isinstance(e.value.__cause__, ValueError)
+ assert "Not enough arguments" in str(e.value.__cause__)
+
+
+def test_invalid_arc_argument_value():
+ pen = ArcRecordingPen()
+ with pytest.raises(ValueError, match="Invalid arc command") as e:
+ parse_path("M0,0 A0,0,0,2,0,0,0", pen)
+
+ cause = e.value.__cause__
+ assert isinstance(cause, ValueError)
+ assert "Invalid argument for 'large-arc-flag' parameter: '2'" in str(cause)
+
+ pen = ArcRecordingPen()
+ with pytest.raises(ValueError, match="Invalid arc command") as e:
+ parse_path("M0,0 A0,0,0,0,-2.0,0,0", pen)
+
+ cause = e.value.__cause__
+ assert isinstance(cause, ValueError)
+ assert "Invalid argument for 'sweep-flag' parameter: '-2.0'" in str(cause)
diff --git a/Tests/ttLib/tables/C_O_L_R_test.py b/Tests/ttLib/tables/C_O_L_R_test.py
index 5fb9e4c2..0a1d9df9 100644
--- a/Tests/ttLib/tables/C_O_L_R_test.py
+++ b/Tests/ttLib/tables/C_O_L_R_test.py
@@ -68,10 +68,11 @@ class COLR_V0_Test(object):
COLR_V1_DATA = (
b"\x00\x01" # Version (1)
b"\x00\x01" # BaseGlyphRecordCount (1)
- b"\x00\x00\x00\x16" # Offset to BaseGlyphRecordArray from beginning of table (22)
- b"\x00\x00\x00\x1c" # Offset to LayerRecordArray from beginning of table (28)
+ b"\x00\x00\x00\x1a" # Offset to BaseGlyphRecordArray from beginning of table (26)
+ 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 (40)
+ 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\x00" # Offset to VarStore (NULL)
b"\x00\x06" # BaseGlyphRecord[0].BaseGlyph (6)
b"\x00\x00" # BaseGlyphRecord[0].FirstLayerIndex (0)
@@ -82,22 +83,50 @@ COLR_V1_DATA = (
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\x01" # BaseGlyphV1List.BaseGlyphCount (1)
+ b"\x00\x00\x00\x02" # BaseGlyphV1List.BaseGlyphCount (2)
b"\x00\n" # BaseGlyphV1List.BaseGlyphV1Record[0].BaseGlyph (10)
- b"\x00\x00\x00\n" # Offset to LayerV1List from beginning of BaseGlyphV1List (10)
+ b"\x00\x00\x00\x10" # Offset to Paint table from beginning of BaseGlyphV1List (16)
+ b"\x00\x0e" # BaseGlyphV1List.BaseGlyphV1Record[1].BaseGlyph (14)
+ b"\x00\x00\x00\x16" # Offset to Paint table from beginning of BaseGlyphV1List (22)
+ b"\x01" # BaseGlyphV1Record[0].Paint.Format (1)
+ b"\x03" # BaseGlyphV1Record[0].Paint.NumLayers (3)
+ b"\x00\x00\x00\x00" # BaseGlyphV1Record[0].Paint.FirstLayerIndex (0)
+ b"\x08" # BaseGlyphV1Record[1].Paint.Format (8)
+ 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\x004" # Offset to Paint from beginning of PaintTransform (52)
+ b"\x00\x01\x00\x00" # Affine2x3.xx.value (1.0)
+ b"\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00" # Affine2x3.xy.value (0.0)
+ b"\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00" # Affine2x3.yx.value (0.0)
+ b"\x00\x00\x00\x00"
+ b"\x00\x01\x00\x00" # Affine2x3.yy.value (1.0)
+ b"\x00\x00\x00\x00"
+ b"\x01,\x00\x00" # Affine2x3.dx.value (300.0)
+ b"\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00" # Affine2x3.dy.value (0.0)
+ b"\x00\x00\x00\x00"
+ b"\x06" # BaseGlyphV1Record[1].Paint.SourcePaint.Format (6)
+ b"\x00\n" # BaseGlyphV1Record[1].Paint.SourcePaint.Glyph (10)
b"\x00\x00\x00\x03" # LayerV1List.LayerCount (3)
- b"\x00\x0b" # LayerV1List.LayerV1Record[0].LayerGlyph (11)
- b"\x00\x00\x00\x16" # Offset to Paint from beginning of LayerV1List (22)
- b"\x00\x0c" # LayerV1List.LayerV1Record[1].LayerGlyph (12)
- b"\x00\x00\x00 " # Offset to Paint from beginning of LayerV1List (32)
- b"\x00\r" # LayerV1List.LayerV1Record[2].LayerGlyph (13)
- b"\x00\x00\x00x" # Offset to Paint from beginning of LayerV1List (120)
- b"\x00\x01" # Paint.Format (1)
+ b"\x00\x00\x00\x10" # Offset to Paint table from beginning of LayerV1List (16)
+ b"\x00\x00\x00\x1f" # Offset to Paint table from beginning of LayerV1List (31)
+ b"\x00\x00\x00z" # Offset to Paint table from beginning of LayerV1List (122)
+ b"\x05" # LayerV1List.Paint[0].Format (5)
+ b"\x00\x00\x06" # Offset to Paint subtable from beginning of PaintGlyph (6)
+ b"\x00\x0b" # LayerV1List.Paint[0].Glyph (11)
+ b"\x02" # LayerV1List.Paint[0].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\x02" # Paint.Format (2)
- b"\x00\x00\x00*" # Offset to ColorLine from beginning of Paint (42)
+ b"\x05" # LayerV1List.Paint[1].Format (5)
+ b"\x00\x00\x06" # Offset to Paint subtable from beginning of PaintGlyph (6)
+ b"\x00\x0c" # LayerV1List.Paint[1].Glyph (12)
+ 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)
@@ -108,9 +137,9 @@ COLR_V1_DATA = (
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 (5)
+ b"\x00\x06" # Paint.y2.value (6)
b"\x00\x00\x00\x00" # Paint.y2.varIdx (0)
- b"\x00\x01" # ColorLine.Extend (1 or "repeat")
+ b"\x01" # ColorLine.Extend (1 or "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)
@@ -127,8 +156,25 @@ COLR_V1_DATA = (
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\x03" # Paint.Format (3)
- b"\x00\x00\x00." # Offset to ColorLine from beginning of Paint (46)
+ b"\x05" # LayerV1List.Paint[2].Format (5)
+ 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\x004" # Offset to Paint subtable from beginning of PaintTransform (52)
+ b"\xff\xf3\x00\x00" # Affine2x3.xx.value (-13)
+ b"\x00\x00\x00\x00"
+ b"\x00\x0e\x00\x00" # Affine2x3.xy.value (14)
+ b"\x00\x00\x00\x00"
+ b"\x00\x0f\x00\x00" # Affine2x3.yx.value (15)
+ b"\x00\x00\x00\x00"
+ b"\xff\xef\x00\x00" # Affine2x3.yy.value (-17)
+ b"\x00\x00\x00\x00"
+ b"\x00\x12\x00\x00" # Affine2x3.yy.value (18)
+ b"\x00\x00\x00\x00"
+ b"\x00\x13\x00\x00" # Affine2x3.yy.value (19)
+ b"\x00\x00\x00\x00"
+ b"\x04" # LayerV1List.Paint[2].Paint.Paint.Format (4)
+ b"\x00\x00(" # Offset to ColorLine from beginning of PaintRadialGradient (40)
b"\x00\x07" # Paint.x0.value (7)
b"\x00\x00\x00\x00"
b"\x00\x08" # Paint.y0.value (8)
@@ -141,27 +187,18 @@ COLR_V1_DATA = (
b"\x00\x00\x00\x00"
b"\x00\x0c" # Paint.r1.value (12)
b"\x00\x00\x00\x00"
- b"\x00\x00\x00N" # Offset to Affine2x2 from beginning of Paint (78)
- b"\x00\x00" # ColorLine.Extend (0 or "pad")
+ b"\x00" # ColorLine.Extend (0 or "pad")
b"\x00\x02" # ColorLine.StopCount (2)
b"\x00\x00" # ColorLine.ColorStop[0].StopOffset.value (0.0)
b"\x00\x00\x00\x00"
b"\x00\x06" # ColorLine.ColorStop[0].Color.PaletteIndex (6)
- b"@\x00" # ColorLine.ColorStop[0].Color.Alpha.value (1.0)
+ b"@\x00" # ColorLine.ColorStop[0].Color.Alpha.value (1.0)
b"\x00\x00\x00\x00"
b"@\x00" # ColorLine.ColorStop[1].StopOffset.value (1.0)
b"\x00\x00\x00\x00"
b"\x00\x07" # ColorLine.ColorStop[1].Color.PaletteIndex (7)
b"\x19\x9a" # ColorLine.ColorStop[1].Color.Alpha.value (0.4)
b"\x00\x00\x00\x00"
- b"\xff\xf3\x00\x00" # Affine2x2.xx.value (-13)
- b"\x00\x00\x00\x00"
- b"\x00\x0e\x00\x00" # Affine2x2.xy.value (14)
- b"\x00\x00\x00\x00"
- b"\x00\x0f\x00\x00" # Affine2x2.yx.value (15)
- b"\x00\x00\x00\x00"
- b"\xff\xef\x00\x00" # Affine2x2.yy.value (-17)
- b"\x00\x00\x00\x00"
)
@@ -191,94 +228,124 @@ COLR_V1_XML = [
"</LayerRecordArray>",
"<!-- LayerRecordCount=3 -->",
"<BaseGlyphV1List>",
- " <!-- BaseGlyphCount=1 -->",
+ " <!-- BaseGlyphCount=2 -->",
' <BaseGlyphV1Record index="0">',
' <BaseGlyph value="glyph00010"/>',
- " <LayerV1List>",
- " <!-- LayerCount=3 -->",
- ' <LayerV1Record index="0">',
- ' <LayerGlyph value="glyph00011"/>',
- ' <Paint Format="1">',
- " <Color>",
- ' <PaletteIndex value="2"/>',
- ' <Alpha value="0.5"/>',
- " </Color>",
- " </Paint>",
- " </LayerV1Record>",
- ' <LayerV1Record index="1">',
- ' <LayerGlyph value="glyph00012"/>',
- ' <Paint Format="2">',
- " <ColorLine>",
- ' <Extend value="repeat"/>',
- " <!-- StopCount=3 -->",
- ' <ColorStop index="0">',
- ' <StopOffset value="0.0"/>',
- " <Color>",
- ' <PaletteIndex value="3"/>',
- ' <Alpha value="1.0"/>',
- " </Color>",
- " </ColorStop>",
- ' <ColorStop index="1">',
- ' <StopOffset value="0.5"/>',
- " <Color>",
- ' <PaletteIndex value="4"/>',
- ' <Alpha value="1.0"/>',
- " </Color>",
- " </ColorStop>",
- ' <ColorStop index="2">',
- ' <StopOffset value="1.0"/>',
- " <Color>",
- ' <PaletteIndex value="5"/>',
- ' <Alpha value="1.0"/>',
- " </Color>",
- " </ColorStop>",
- " </ColorLine>",
- ' <x0 value="1"/>',
- ' <y0 value="2"/>',
- ' <x1 value="-3"/>',
- ' <y1 value="-4"/>',
- ' <x2 value="5"/>',
- ' <y2 value="6"/>',
- " </Paint>",
- " </LayerV1Record>",
- ' <LayerV1Record index="2">',
- ' <LayerGlyph value="glyph00013"/>',
- ' <Paint Format="3">',
- " <ColorLine>",
- ' <Extend value="pad"/>',
- " <!-- StopCount=2 -->",
- ' <ColorStop index="0">',
- ' <StopOffset value="0.0"/>',
- " <Color>",
- ' <PaletteIndex value="6"/>',
- ' <Alpha value="1.0"/>',
- " </Color>",
- " </ColorStop>",
- ' <ColorStop index="1">',
- ' <StopOffset value="1.0"/>',
- " <Color>",
- ' <PaletteIndex value="7"/>',
- ' <Alpha value="0.4"/>',
- " </Color>",
- " </ColorStop>",
- " </ColorLine>",
- ' <x0 value="7"/>',
- ' <y0 value="8"/>',
- ' <r0 value="9"/>',
- ' <x1 value="10"/>',
- ' <y1 value="11"/>',
- ' <r1 value="12"/>',
- " <Transform>",
- ' <xx value="-13.0"/>',
- ' <xy value="14.0"/>',
- ' <yx value="15.0"/>',
- ' <yy value="-17.0"/>',
- " </Transform>",
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="3"/>',
+ ' <FirstLayerIndex value="0"/>',
+ " </Paint>",
+ " </BaseGlyphV1Record>",
+ ' <BaseGlyphV1Record index="1">',
+ ' <BaseGlyph value="glyph00014"/>',
+ ' <Paint Format="8"><!-- PaintComposite -->',
+ ' <SourcePaint Format="6"><!-- PaintColrGlyph -->',
+ ' <Glyph value="glyph00010"/>',
+ " </SourcePaint>",
+ ' <CompositeMode value="src_over"/>',
+ ' <BackdropPaint Format="7"><!-- PaintTransform -->',
+ ' <Paint Format="6"><!-- PaintColrGlyph -->',
+ ' <Glyph value="glyph00010"/>',
" </Paint>",
- " </LayerV1Record>",
- " </LayerV1List>",
+ " <Transform>",
+ ' <xx value="1.0"/>',
+ ' <yx value="0.0"/>',
+ ' <xy value="0.0"/>',
+ ' <yy value="1.0"/>',
+ ' <dx value="300.0"/>',
+ ' <dy value="0.0"/>',
+ " </Transform>",
+ " </BackdropPaint>",
+ " </Paint>",
" </BaseGlyphV1Record>",
"</BaseGlyphV1List>",
+ "<LayerV1List>",
+ " <!-- LayerCount=3 -->",
+ ' <Paint index="0" Format="5"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ " <Color>",
+ ' <PaletteIndex value="2"/>',
+ ' <Alpha value="0.5"/>',
+ " </Color>",
+ " </Paint>",
+ ' <Glyph value="glyph00011"/>',
+ " </Paint>",
+ ' <Paint index="1" Format="5"><!-- PaintGlyph -->',
+ ' <Paint Format="3"><!-- PaintLinearGradient -->',
+ " <ColorLine>",
+ ' <Extend value="repeat"/>',
+ " <!-- StopCount=3 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ " <Color>",
+ ' <PaletteIndex value="3"/>',
+ ' <Alpha value="1.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="0.5"/>',
+ " <Color>",
+ ' <PaletteIndex value="4"/>',
+ ' <Alpha value="1.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ ' <ColorStop index="2">',
+ ' <StopOffset value="1.0"/>',
+ " <Color>",
+ ' <PaletteIndex value="5"/>',
+ ' <Alpha value="1.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <x0 value="1"/>',
+ ' <y0 value="2"/>',
+ ' <x1 value="-3"/>',
+ ' <y1 value="-4"/>',
+ ' <x2 value="5"/>',
+ ' <y2 value="6"/>',
+ " </Paint>",
+ ' <Glyph value="glyph00012"/>',
+ " </Paint>",
+ ' <Paint index="2" Format="5"><!-- PaintGlyph -->',
+ ' <Paint Format="7"><!-- PaintTransform -->',
+ ' <Paint Format="4"><!-- PaintRadialGradient -->',
+ " <ColorLine>",
+ ' <Extend value="pad"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ " <Color>",
+ ' <PaletteIndex value="6"/>',
+ ' <Alpha value="1.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ " <Color>",
+ ' <PaletteIndex value="7"/>',
+ ' <Alpha value="0.4"/>',
+ " </Color>",
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <x0 value="7"/>',
+ ' <y0 value="8"/>',
+ ' <r0 value="9"/>',
+ ' <x1 value="10"/>',
+ ' <y1 value="11"/>',
+ ' <r1 value="12"/>',
+ " </Paint>",
+ " <Transform>",
+ ' <xx value="-13.0"/>',
+ ' <yx value="14.0"/>',
+ ' <xy value="15.0"/>',
+ ' <yy value="-17.0"/>',
+ ' <dx value="18.0"/>',
+ ' <dy value="19.0"/>',
+ " </Transform>",
+ " </Paint>",
+ ' <Glyph value="glyph00013"/>',
+ " </Paint>",
+ "</LayerV1List>",
]
diff --git a/Tests/varLib/data/FeatureVarsCustomTag.designspace b/Tests/varLib/data/FeatureVarsCustomTag.designspace
new file mode 100644
index 00000000..45b06f30
--- /dev/null
+++ b/Tests/varLib/data/FeatureVarsCustomTag.designspace
@@ -0,0 +1,77 @@
+<?xml version='1.0' encoding='utf-8'?>
+<designspace format="3">
+ <axes>
+ <axis default="368.0" maximum="1000.0" minimum="0.0" name="weight" tag="wght" />
+ <axis default="0.0" maximum="100.0" minimum="0.0" name="contrast" tag="cntr">
+ <labelname xml:lang="en">Contrast</labelname>
+ </axis>
+ </axes>
+ <rules processing="last">
+ <rule name="dollar-stroke">
+ <conditionset>
+ <condition name="weight" minimum="500" /> <!-- intentionally omitted maximum -->
+ </conditionset>
+ <sub name="uni0024" with="uni0024.nostroke" />
+ </rule>
+ <rule name="to-lowercase">
+ <conditionset>
+ <condition name="contrast" minimum="75" maximum="100" />
+ </conditionset>
+ <sub name="uni0041" with="uni0061" />
+ </rule>
+ <rule name="to-uppercase">
+ <conditionset>
+ <condition name="weight" minimum="0" maximum="200" />
+ <condition name="contrast" minimum="0" maximum="25" />
+ </conditionset>
+ <sub name="uni0061" with="uni0041" />
+ </rule>
+ </rules>
+ <sources>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master0.ufo" name="master_0" stylename="Master0">
+ <location>
+ <dimension name="weight" xvalue="0" />
+ <dimension name="contrast" xvalue="0" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master1.ufo" name="master_1" stylename="Master1">
+ <lib copy="1" />
+ <groups copy="1" />
+ <info copy="1" />
+ <location>
+ <dimension name="weight" xvalue="368" />
+ <dimension name="contrast" xvalue="0" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master2.ufo" name="master_2" stylename="Master2">
+ <location>
+ <dimension name="weight" xvalue="1000" />
+ <dimension name="contrast" xvalue="0" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master3.ufo" name="master_3" stylename="Master3">
+ <location>
+ <dimension name="weight" xvalue="1000" />
+ <dimension name="contrast" xvalue="100" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master0.ufo" name="master_0" stylename="Master0">
+ <location>
+ <dimension name="weight" xvalue="0" />
+ <dimension name="contrast" xvalue="100" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master4.ufo" name="master_4" stylename="Master4">
+ <location>
+ <dimension name="weight" xvalue="368" />
+ <dimension name="contrast" xvalue="100" />
+ </location>
+ </source>
+ </sources>
+ <lib>
+ <dict>
+ <key>com.github.fonttools.varLib.featureVarsFeatureTag</key>
+ <string>calt</string>
+ </dict>
+ </lib>
+</designspace>
diff --git a/Tests/varLib/data/test_results/FeatureVarsCustomTag.ttx b/Tests/varLib/data/test_results/FeatureVarsCustomTag.ttx
new file mode 100644
index 00000000..f50ef785
--- /dev/null
+++ b/Tests/varLib/data/test_results/FeatureVarsCustomTag.ttx
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.29">
+
+ <fvar>
+
+ <!-- Weight -->
+ <Axis>
+ <AxisTag>wght</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>368.0</DefaultValue>
+ <MaxValue>1000.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+
+ <!-- Contrast -->
+ <Axis>
+ <AxisTag>cntr</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>100.0</MaxValue>
+ <AxisNameID>257</AxisNameID>
+ </Axis>
+ </fvar>
+
+ <GSUB>
+ <Version value="0x00010001"/>
+ <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="calt"/>
+ <Feature>
+ <!-- LookupCount=0 -->
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=3 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="uni0024" out="uni0024.nostroke"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="uni0041" out="uni0061"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="2">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="uni0061" out="uni0041"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ <FeatureVariations>
+ <Version value="0x00010000"/>
+ <!-- FeatureVariationCount=4 -->
+ <FeatureVariationRecord index="0">
+ <ConditionSet>
+ <!-- ConditionCount=2 -->
+ <ConditionTable index="0" Format="1">
+ <AxisIndex value="1"/>
+ <FilterRangeMinValue value="0.75"/>
+ <FilterRangeMaxValue value="1.0"/>
+ </ConditionTable>
+ <ConditionTable index="1" Format="1">
+ <AxisIndex value="0"/>
+ <FilterRangeMinValue value="0.20886"/>
+ <FilterRangeMaxValue value="1.0"/>
+ </ConditionTable>
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010000"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=2 -->
+ <LookupListIndex index="0" value="0"/>
+ <LookupListIndex index="1" value="1"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ <FeatureVariationRecord index="1">
+ <ConditionSet>
+ <!-- ConditionCount=2 -->
+ <ConditionTable index="0" Format="1">
+ <AxisIndex value="1"/>
+ <FilterRangeMinValue value="0.0"/>
+ <FilterRangeMaxValue value="0.25"/>
+ </ConditionTable>
+ <ConditionTable index="1" Format="1">
+ <AxisIndex value="0"/>
+ <FilterRangeMinValue value="-1.0"/>
+ <FilterRangeMaxValue value="-0.45654"/>
+ </ConditionTable>
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010000"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="2"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ <FeatureVariationRecord index="2">
+ <ConditionSet>
+ <!-- ConditionCount=1 -->
+ <ConditionTable index="0" Format="1">
+ <AxisIndex value="1"/>
+ <FilterRangeMinValue value="0.75"/>
+ <FilterRangeMaxValue value="1.0"/>
+ </ConditionTable>
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010000"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ <FeatureVariationRecord index="3">
+ <ConditionSet>
+ <!-- ConditionCount=1 -->
+ <ConditionTable index="0" Format="1">
+ <AxisIndex value="0"/>
+ <FilterRangeMinValue value="0.20886"/>
+ <FilterRangeMaxValue value="1.0"/>
+ </ConditionTable>
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010000"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ </FeatureVariations>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py
index 42e04bd9..dfc10c34 100644
--- a/Tests/varLib/varLib_test.py
+++ b/Tests/varLib/varLib_test.py
@@ -221,6 +221,18 @@ class BuildTest(unittest.TestCase):
save_before_dump=True,
)
+ def test_varlib_build_feature_variations_custom_tag(self):
+ """Designspace file contains <rules> element, used to build
+ GSUB FeatureVariations table.
+ """
+ self._run_varlib_build_test(
+ designspace_name="FeatureVarsCustomTag",
+ font_name="TestFamily",
+ tables=["fvar", "GSUB"],
+ expected_ttx_name="FeatureVarsCustomTag",
+ save_before_dump=True,
+ )
+
def test_varlib_build_feature_variations_whole_range(self):
"""Designspace file contains <rules> element specifying the entire design
space, used to build GSUB FeatureVariations table.
diff --git a/requirements.txt b/requirements.txt
index a409e4ee..940e2d4a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,10 +3,10 @@
brotli==1.0.9; platform_python_implementation != "PyPy"
brotlipy==0.7.0; platform_python_implementation == "PyPy"
unicodedata2==13.0.0.post2; python_version < '3.9' and platform_python_implementation != "PyPy"
-scipy==1.5.2; platform_python_implementation != "PyPy"
-munkres==1.1.2; platform_python_implementation == "PyPy"
+scipy==1.5.4; platform_python_implementation != "PyPy"
+munkres==1.1.4; platform_python_implementation == "PyPy"
zopfli==0.1.6
fs==2.4.11
-skia-pathops==0.5.0; platform_python_implementation != "PyPy"
+skia-pathops==0.5.1.post1; platform_python_implementation != "PyPy"
# this is only required to run Tests/cu2qu/{ufo,cli}_test.py
ufoLib2==0.6.2
diff --git a/setup.cfg b/setup.cfg
index 012642c8..d831bdc3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.16.1
+current_version = 4.17.0
commit = True
tag = False
tag_name = {new_version}
diff --git a/setup.py b/setup.py
index efa4028e..3e43fd31 100755
--- a/setup.py
+++ b/setup.py
@@ -441,7 +441,7 @@ if ext_modules:
setup_params = dict(
name="fonttools",
- version="4.16.1",
+ version="4.17.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",