Upgrade fonttools to 4.8.0
Test: None
Change-Id: I089b1e10b6820bcaf96d9b17827944d8f5d97e4e
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 469a650..7759df7 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -4,6 +4,6 @@
log = logging.getLogger(__name__)
-version = __version__ = "4.7.0"
+version = __version__ = "4.8.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py
index 486909e..9fe7f20 100644
--- a/Lib/fontTools/colorLib/builder.py
+++ b/Lib/fontTools/colorLib/builder.py
@@ -1,31 +1,148 @@
+import collections
+import copy
import enum
-from typing import Dict, Iterable, List, Optional, Tuple, Union
-from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord, table_C_O_L_R_
-from fontTools.ttLib.tables.C_P_A_L_ import Color, table_C_P_A_L_
-from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e
+from functools import partial
+from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
+from fontTools.ttLib.tables import C_O_L_R_
+from fontTools.ttLib.tables import C_P_A_L_
+from fontTools.ttLib.tables import _n_a_m_e
+from fontTools.ttLib.tables import otTables as ot
+from fontTools.ttLib.tables.otTables import (
+ ExtendMode,
+ VariableValue,
+ VariableFloat,
+ VariableInt,
+)
from .errors import ColorLibError
-def buildCOLR(colorLayers: Dict[str, List[Tuple[str, int]]]) -> table_C_O_L_R_:
+# TODO move type aliases to colorLib.types?
+_Kwargs = Mapping[str, Any]
+_PaintInput = Union[int, _Kwargs, ot.Paint]
+_LayerTuple = Tuple[str, _PaintInput]
+_LayersList = Sequence[_LayerTuple]
+_ColorGlyphsDict = Dict[str, _LayersList]
+_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
+_Number = Union[int, float]
+_ScalarInput = Union[_Number, VariableValue, Tuple[_Number, int]]
+_ColorStopTuple = Tuple[_ScalarInput, int]
+_ColorStopInput = Union[_ColorStopTuple, _Kwargs, ot.ColorStop]
+_ColorStopsList = Sequence[_ColorStopInput]
+_ExtendInput = Union[int, str, ExtendMode]
+_ColorLineInput = Union[_Kwargs, ot.ColorLine]
+_PointTuple = Tuple[_ScalarInput, _ScalarInput]
+_PointInput = Union[_PointTuple, ot.Point]
+_AffineTuple = Tuple[_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput]
+_AffineInput = Union[_AffineTuple, ot.Affine2x2]
+
+
+def populateCOLRv0(
+ table: ot.COLR,
+ colorGlyphsV0: _ColorGlyphsV0Dict,
+ glyphMap: Optional[Mapping[str, int]] = None,
+):
+ """Build v0 color layers and add to existing COLR table.
+
+ Args:
+ table: a raw otTables.COLR() object (not ttLib's table_C_O_L_R_).
+ colorGlyphsV0: map of base glyph names to lists of (layer glyph names,
+ color palette index) tuples.
+ glyphMap: a map from glyph names to glyph indices, as returned from
+ TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
+ """
+ if glyphMap is not None:
+ colorGlyphItems = sorted(
+ colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
+ )
+ else:
+ colorGlyphItems = colorGlyphsV0.items()
+ baseGlyphRecords = []
+ layerRecords = []
+ for baseGlyph, layers in colorGlyphItems:
+ baseRec = ot.BaseGlyphRecord()
+ baseRec.BaseGlyph = baseGlyph
+ baseRec.FirstLayerIndex = len(layerRecords)
+ baseRec.NumLayers = len(layers)
+ baseGlyphRecords.append(baseRec)
+
+ for layerGlyph, paletteIndex in layers:
+ layerRec = ot.LayerRecord()
+ layerRec.LayerGlyph = layerGlyph
+ layerRec.PaletteIndex = paletteIndex
+ layerRecords.append(layerRec)
+
+ table.BaseGlyphRecordCount = len(baseGlyphRecords)
+ table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray()
+ table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords
+ table.LayerRecordArray = ot.LayerRecordArray()
+ table.LayerRecordArray.LayerRecord = layerRecords
+ table.LayerRecordCount = len(layerRecords)
+
+
+def buildCOLR(
+ colorGlyphs: _ColorGlyphsDict,
+ version: Optional[int] = None,
+ glyphMap: Optional[Mapping[str, int]] = None,
+ varStore: Optional[ot.VarStore] = None,
+) -> C_O_L_R_.table_C_O_L_R_:
"""Build COLR table from color layers mapping.
Args:
- colorLayers: : map of base glyph names to lists of (layer glyph names,
- palette indices) tuples.
+ 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.
+ 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.
+ glyphMap: a map from glyph names to glyph indices, as returned from
+ TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
+ varStore: Optional ItemVarationStore for deltas associated with v1 layer.
Return:
- A new COLRv0 table.
+ A new COLR table.
"""
- colorLayerLists = {}
- for baseGlyphName, layers in colorLayers.items():
- colorLayerLists[baseGlyphName] = [
- LayerRecord(layerGlyphName, colorID) for layerGlyphName, colorID in layers
- ]
+ self = C_O_L_R_.table_C_O_L_R_()
- colr = table_C_O_L_R_()
- colr.version = 0
- colr.ColorLayers = colorLayerLists
- return colr
+ if varStore is not None and version == 0:
+ raise ValueError("Can't add VarStore to COLRv0")
+
+ if version in (None, 0) and not varStore:
+ # split color glyphs into v0 and v1 and encode separately
+ colorGlyphsV0, colorGlyphsV1 = _splitSolidAndGradientGlyphs(colorGlyphs)
+ if version == 0 and colorGlyphsV1:
+ # TODO Derive "average" solid color from gradients?
+ raise ValueError("Can't encode gradients in COLRv0")
+ else:
+ # unless explicitly requested for v1 or have variations, in which case
+ # we encode all color glyph as v1
+ colorGlyphsV0, colorGlyphsV1 = None, colorGlyphs
+
+ colr = ot.COLR()
+
+ if colorGlyphsV0:
+ populateCOLRv0(colr, colorGlyphsV0, glyphMap)
+ else:
+ colr.BaseGlyphRecordCount = colr.LayerRecordCount = 0
+ colr.BaseGlyphRecordArray = colr.LayerRecordArray = None
+
+ if colorGlyphsV1:
+ colr.BaseGlyphV1Array = buildBaseGlyphV1Array(colorGlyphsV1, glyphMap)
+
+ if version is None:
+ version = 1 if (varStore or colorGlyphsV1) else 0
+ elif version not in (0, 1):
+ raise NotImplementedError(version)
+ self.version = colr.Version = version
+
+ if version == 0:
+ self._fromOTTable(colr)
+ else:
+ colr.VarStore = varStore
+ self.table = colr
+
+ return self
class ColorPaletteType(enum.IntFlag):
@@ -45,12 +162,12 @@
def buildPaletteLabels(
- labels: List[_OptionalLocalizedString], nameTable: table__n_a_m_e
+ labels: Iterable[_OptionalLocalizedString], nameTable: _n_a_m_e.table__n_a_m_e
) -> List[Optional[int]]:
return [
nameTable.addMultilingualName(l, mac=False)
if isinstance(l, dict)
- else table_C_P_A_L_.NO_NAME_ID
+ else C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
if l is None
else nameTable.addMultilingualName({"en": l}, mac=False)
for l in labels
@@ -58,12 +175,12 @@
def buildCPAL(
- palettes: List[List[Tuple[float, float, float, float]]],
- paletteTypes: Optional[List[ColorPaletteType]] = None,
- paletteLabels: Optional[List[_OptionalLocalizedString]] = None,
- paletteEntryLabels: Optional[List[_OptionalLocalizedString]] = None,
- nameTable: Optional[table__n_a_m_e] = None,
-) -> table_C_P_A_L_:
+ palettes: Sequence[Sequence[Tuple[float, float, float, float]]],
+ paletteTypes: Optional[Sequence[ColorPaletteType]] = None,
+ paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
+ paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
+ nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None,
+) -> C_P_A_L_.table_C_P_A_L_:
"""Build CPAL table from list of color palettes.
Args:
@@ -89,7 +206,7 @@
"nameTable is required if palette or palette entries have labels"
)
- cpal = table_C_P_A_L_()
+ cpal = C_P_A_L_.table_C_P_A_L_()
cpal.numPaletteEntries = len(palettes[0])
cpal.palettes = []
@@ -106,7 +223,9 @@
)
# input colors are RGBA, CPAL encodes them as BGRA
red, green, blue, alpha = color
- colors.append(Color(*(round(v * 255) for v in (blue, green, red, alpha))))
+ colors.append(
+ C_P_A_L_.Color(*(round(v * 255) for v in (blue, green, red, alpha)))
+ )
cpal.palettes.append(colors)
if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
@@ -119,7 +238,9 @@
)
cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes]
else:
- cpal.paletteTypes = [table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(palettes)
+ cpal.paletteTypes = [C_P_A_L_.table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(
+ palettes
+ )
if paletteLabels is not None:
if len(paletteLabels) != len(palettes):
@@ -128,7 +249,7 @@
)
cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
else:
- cpal.paletteLabels = [table_C_P_A_L_.NO_NAME_ID] * len(palettes)
+ cpal.paletteLabels = [C_P_A_L_.table_C_P_A_L_.NO_NAME_ID] * len(palettes)
if paletteEntryLabels is not None:
if len(paletteEntryLabels) != cpal.numPaletteEntries:
@@ -139,9 +260,275 @@
cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
else:
cpal.paletteEntryLabels = [
- table_C_P_A_L_.NO_NAME_ID
+ C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
] * cpal.numPaletteEntries
else:
cpal.version = 0
return cpal
+
+
+# COLR v1 tables
+# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
+
+_DEFAULT_TRANSPARENCY = VariableFloat(0.0)
+
+
+def _splitSolidAndGradientGlyphs(
+ colorGlyphs: _ColorGlyphsDict,
+) -> Tuple[Dict[str, List[Tuple[str, int]]], Dict[str, List[Tuple[str, ot.Paint]]]]:
+ 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.Transparency.value != _DEFAULT_TRANSPARENCY.value
+ ):
+ allSolidColors = False
+ newLayers.append((layerGlyph, paint))
+ if allSolidColors:
+ colorGlyphsV0[baseGlyph] = [
+ (layerGlyph, paint.Color.PaletteIndex)
+ for layerGlyph, paint in newLayers
+ ]
+ else:
+ colorGlyphsV1[baseGlyph] = newLayers
+
+ # sanity check
+ assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
+
+ 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 buildColor(
+ paletteIndex: int, transparency: _ScalarInput = _DEFAULT_TRANSPARENCY
+) -> ot.Color:
+ self = ot.Color()
+ self.PaletteIndex = int(paletteIndex)
+ self.Transparency = _to_variable_float(transparency)
+ return self
+
+
+def buildSolidColorPaint(
+ paletteIndex: int, transparency: _ScalarInput = _DEFAULT_TRANSPARENCY
+) -> ot.Paint:
+ self = ot.Paint()
+ self.Format = 1
+ self.Color = buildColor(paletteIndex, transparency)
+ return self
+
+
+def buildColorStop(
+ offset: _ScalarInput,
+ paletteIndex: int,
+ transparency: _ScalarInput = _DEFAULT_TRANSPARENCY,
+) -> ot.ColorStop:
+ self = ot.ColorStop()
+ self.StopOffset = _to_variable_float(offset)
+ self.Color = buildColor(paletteIndex, transparency)
+ return self
+
+
+def _to_extend_mode(v: _ExtendInput) -> ExtendMode:
+ if isinstance(v, ExtendMode):
+ return v
+ elif isinstance(v, str):
+ try:
+ return getattr(ExtendMode, v.upper())
+ except AttributeError:
+ raise ValueError(f"{v!r} is not a valid ExtendMode")
+ return ExtendMode(v)
+
+
+def buildColorLine(
+ stops: _ColorStopsList, extend: _ExtendInput = ExtendMode.PAD
+) -> ot.ColorLine:
+ self = ot.ColorLine()
+ self.Extend = _to_extend_mode(extend)
+ self.StopCount = len(stops)
+ self.ColorStop = [
+ stop
+ if isinstance(stop, ot.ColorStop)
+ else buildColorStop(**stop)
+ if isinstance(stop, collections.abc.Mapping)
+ else buildColorStop(*stop)
+ for stop in stops
+ ]
+ return self
+
+
+def buildPoint(x: _ScalarInput, y: _ScalarInput) -> ot.Point:
+ self = ot.Point()
+ # positions are encoded as Int16 so round to int
+ self.x = _to_variable_int(x)
+ self.y = _to_variable_int(y)
+ return self
+
+
+def _to_variable_point(pt: _PointInput) -> ot.Point:
+ if isinstance(pt, ot.Point):
+ return pt
+ return buildPoint(*pt)
+
+
+def _to_color_line(obj):
+ if isinstance(obj, ot.ColorLine):
+ return obj
+ elif isinstance(obj, collections.abc.Mapping):
+ return buildColorLine(**obj)
+ raise TypeError(obj)
+
+
+def buildLinearGradientPaint(
+ colorLine: _ColorLineInput,
+ p0: _PointInput,
+ p1: _PointInput,
+ p2: Optional[_PointInput] = 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, pt in enumerate((p0, p1, p2)):
+ setattr(self, f"p{i}", _to_variable_point(pt))
+
+ 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: _PointInput,
+ c1: _PointInput,
+ r0: _ScalarInput,
+ r1: _ScalarInput,
+ affine: Optional[_AffineInput] = None,
+) -> ot.Paint:
+
+ self = ot.Paint()
+ self.Format = 3
+ self.ColorLine = _to_color_line(colorLine)
+
+ for i, pt in [(0, c0), (1, c1)]:
+ setattr(self, f"c{i}", _to_variable_point(pt))
+
+ for i, r in [(0, r0), (1, r1)]:
+ # distances are encoded as UShort so we round to int
+ setattr(self, f"r{i}", _to_variable_int(r))
+
+ if affine is not None and not isinstance(affine, ot.Affine2x2):
+ affine = buildAffine2x2(*affine)
+ self.Affine = affine
+
+ 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 buildLayerV1Record(layerGlyph: str, paint: _PaintInput) -> ot.LayerV1Record:
+ self = ot.LayerV1Record()
+ self.LayerGlyph = layerGlyph
+ self.Paint = _to_ot_paint(paint)
+ return self
+
+
+def buildLayerV1Array(
+ layers: Sequence[Union[_LayerTuple, ot.LayerV1Record]]
+) -> ot.LayerV1Array:
+ self = ot.LayerV1Array()
+ 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
+ return self
+
+
+def buildBaseGlyphV1Record(
+ baseGlyph: str, layers: Union[_LayersList, ot.LayerV1Array]
+) -> ot.BaseGlyphV1Array:
+ self = ot.BaseGlyphV1Record()
+ self.BaseGlyph = baseGlyph
+ if not isinstance(layers, ot.LayerV1Array):
+ layers = buildLayerV1Array(layers)
+ self.LayerV1Array = layers
+ return self
+
+
+def buildBaseGlyphV1Array(
+ colorGlyphs: Union[_ColorGlyphsDict, Dict[str, ot.LayerV1Array]],
+ glyphMap: Optional[Mapping[str, int]] = None,
+) -> ot.BaseGlyphV1Array:
+ 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.BaseGlyphV1Array()
+ self.BaseGlyphCount = len(records)
+ self.BaseGlyphV1Record = records
+ return self
+
+
+_PAINT_BUILDERS = {
+ 1: buildSolidColorPaint,
+ 2: buildLinearGradientPaint,
+ 3: buildRadialGradientPaint,
+}
+
+
+def buildPaint(format: int, **kwargs) -> ot.Paint:
+ try:
+ return _PAINT_BUILDERS[format](**kwargs)
+ except KeyError:
+ raise NotImplementedError(format)
diff --git a/Lib/fontTools/cu2qu/ufo.py b/Lib/fontTools/cu2qu/ufo.py
index 63d5799..447de7b 100644
--- a/Lib/fontTools/cu2qu/ufo.py
+++ b/Lib/fontTools/cu2qu/ufo.py
@@ -37,6 +37,9 @@
__all__ = ['fonts_to_quadratic', 'font_to_quadratic']
+# The default approximation error below is a relative value (1/1000 of the EM square).
+# Later on, we convert it to absolute font units by multiplying it by a font's UPEM
+# (see fonts_to_quadratic).
DEFAULT_MAX_ERR = 0.001
CURVE_TYPE_LIB_KEY = "com.github.googlei18n.cu2qu.curve_type"
diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index b0f805a..e416c0c 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -109,7 +109,7 @@
if hasattr(g, 'asFea'):
return g.asFea()
elif isinstance(g, tuple) and len(g) == 2:
- return asFea(g[0]) + "-" + asFea(g[1]) # a range
+ return asFea(g[0]) + " - " + asFea(g[1]) # a range
elif g.lower() in fea_keywords:
return "\\" + g
else:
@@ -197,7 +197,7 @@
def add_cid_range(self, start, end, glyphs):
if self.curr < len(self.glyphs):
self.original.extend(self.glyphs[self.curr:])
- self.original.append(("cid{:05d}".format(start), "cid{:05d}".format(end)))
+ self.original.append(("\\{}".format(start), "\\{}".format(end)))
self.glyphs.extend(glyphs)
self.curr = len(self.glyphs)
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index bda34e2..a3eaf62 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -299,7 +299,7 @@
if self.next_token_type_ is Lexer.NAME:
glyph = self.expect_glyph_()
location = self.cur_token_location_
- if '-' in glyph and glyph not in self.glyphNames_:
+ if '-' in glyph and self.glyphNames_ and glyph not in self.glyphNames_:
start, limit = self.split_glyph_range_(glyph, location)
self.check_glyph_name_in_glyph_set(start, limit)
glyphs.add_range(
@@ -314,6 +314,11 @@
start, limit,
self.make_glyph_range_(location, start, limit))
else:
+ if '-' in glyph and not self.glyphNames_:
+ log.warning(str(FeatureLibError(
+ f"Ambiguous glyph name that looks like a range: {glyph!r}",
+ location
+ )))
self.check_glyph_name_in_glyph_set(glyph)
glyphs.append(glyph)
elif self.next_token_type_ is Lexer.CID:
@@ -351,7 +356,7 @@
else:
raise FeatureLibError(
"Expected glyph name, glyph range, "
- "or glyph class reference",
+ f"or glyph class reference, found {self.next_token_!r}",
self.next_token_location_)
self.expect_symbol_("]")
return glyphs
@@ -826,8 +831,14 @@
'is not supported',
location)
+ # If there are remaining glyphs to parse, this is an invalid GSUB statement
+ if len(new) != 0:
+ raise FeatureLibError(
+ 'Invalid substitution statement',
+ location
+ )
+
# GSUB lookup type 6: Chaining contextual substitution.
- assert len(new) == 0, new
rule = self.ast.ChainContextSubstStatement(
old_prefix, old, old_suffix, lookups, location=location)
return rule
diff --git a/Lib/fontTools/pens/cu2quPen.py b/Lib/fontTools/pens/cu2quPen.py
index cf98b22..497585b 100644
--- a/Lib/fontTools/pens/cu2quPen.py
+++ b/Lib/fontTools/pens/cu2quPen.py
@@ -24,7 +24,9 @@
using the FontTools SegmentPen protocol.
other_pen: another SegmentPen used to draw the transformed outline.
- max_err: maximum approximation error in font units.
+ max_err: maximum approximation error in font units. For optimal results,
+ if you know the UPEM of the font, we recommend setting this to a
+ value equal, or close to UPEM / 1000.
reverse_direction: flip the contours' direction but keep starting point.
stats: a dictionary counting the point numbers of quadratic segments.
ignore_single_points: don't emit contours containing only a single point
@@ -137,7 +139,9 @@
using the RoboFab PointPen protocol.
other_point_pen: another PointPen used to draw the transformed outline.
- max_err: maximum approximation error in font units.
+ max_err: maximum approximation error in font units. For optimal results,
+ if you know the UPEM of the font, we recommend setting this to a
+ value equal, or close to UPEM / 1000.
reverse_direction: reverse the winding direction of all contours.
stats: a dictionary counting the point numbers of quadratic segments.
"""
diff --git a/Lib/fontTools/ttLib/tables/C_O_L_R_.py b/Lib/fontTools/ttLib/tables/C_O_L_R_.py
index 064aa27..2366c5e 100644
--- a/Lib/fontTools/ttLib/tables/C_O_L_R_.py
+++ b/Lib/fontTools/ttLib/tables/C_O_L_R_.py
@@ -5,7 +5,6 @@
from fontTools.misc.py23 import *
from fontTools.misc.textTools import safeEval
from . import DefaultTable
-import struct
class table_C_O_L_R_(DefaultTable.DefaultTable):
@@ -15,91 +14,91 @@
ttFont['COLR'][<glyphName>] = <value> will set the color layers for any glyph.
"""
- def decompile(self, data, ttFont):
- self.getGlyphName = ttFont.getGlyphName # for use in get/set item functions, for access by GID
- self.version, numBaseGlyphRecords, offsetBaseGlyphRecord, offsetLayerRecord, numLayerRecords = struct.unpack(">HHLLH", data[:14])
- assert (self.version == 0), "Version of COLR table is higher than I know how to handle"
- glyphOrder = ttFont.getGlyphOrder()
- gids = []
- layerLists = []
- glyphPos = offsetBaseGlyphRecord
- for i in range(numBaseGlyphRecords):
- gid, firstLayerIndex, numLayers = struct.unpack(">HHH", data[glyphPos:glyphPos+6])
- glyphPos += 6
- gids.append(gid)
- assert (firstLayerIndex + numLayers <= numLayerRecords)
- layerPos = offsetLayerRecord + firstLayerIndex * 4
- layers = []
- for j in range(numLayers):
- layerGid, colorID = struct.unpack(">HH", data[layerPos:layerPos+4])
- try:
- layerName = glyphOrder[layerGid]
- except IndexError:
- layerName = self.getGlyphName(layerGid)
- layerPos += 4
- layers.append(LayerRecord(layerName, colorID))
- layerLists.append(layers)
-
+ def _fromOTTable(self, table):
+ self.version = 0
self.ColorLayers = colorLayerLists = {}
- try:
- names = [glyphOrder[gid] for gid in gids]
- except IndexError:
- getGlyphName = self.getGlyphName
- names = map(getGlyphName, gids)
+ layerRecords = table.LayerRecordArray.LayerRecord
+ numLayerRecords = len(layerRecords)
+ for baseRec in table.BaseGlyphRecordArray.BaseGlyphRecord:
+ baseGlyph = baseRec.BaseGlyph
+ firstLayerIndex = baseRec.FirstLayerIndex
+ numLayers = baseRec.NumLayers
+ assert (firstLayerIndex + numLayers <= numLayerRecords)
+ layers = []
+ for i in range(firstLayerIndex, firstLayerIndex+numLayers):
+ layerRec = layerRecords[i]
+ layers.append(
+ LayerRecord(layerRec.LayerGlyph, layerRec.PaletteIndex)
+ )
+ colorLayerLists[baseGlyph] = layers
- for name, layerList in zip(names, layerLists):
- colorLayerLists[name] = layerList
+ def _toOTTable(self, ttFont):
+ from . import otTables
+ from fontTools.colorLib.builder import populateCOLRv0
+
+ tableClass = getattr(otTables, self.tableTag)
+ table = tableClass()
+ table.Version = self.version
+
+ populateCOLRv0(
+ table,
+ {
+ baseGlyph: [(layer.name, layer.colorID) for layer in layers]
+ for baseGlyph, layers in self.ColorLayers.items()
+ },
+ glyphMap=ttFont.getReverseGlyphMap(rebuild=True),
+ )
+ return table
+
+ def decompile(self, data, ttFont):
+ from .otBase import OTTableReader
+ from . import otTables
+
+ # We use otData to decompile, but we adapt the decompiled otTables to the
+ # existing COLR v0 API for backward compatibility.
+ reader = OTTableReader(data, tableTag=self.tableTag)
+ tableClass = getattr(otTables, self.tableTag)
+ table = tableClass()
+ table.decompile(reader, ttFont)
+
+ if table.Version == 0:
+ self._fromOTTable(table)
+ else:
+ # for new versions, keep the raw otTables around
+ self.table = table
def compile(self, ttFont):
- ordered = []
- ttFont.getReverseGlyphMap(rebuild=True)
- glyphNames = self.ColorLayers.keys()
- for glyphName in glyphNames:
- try:
- gid = ttFont.getGlyphID(glyphName)
- except:
- assert 0, "COLR table contains a glyph name not in ttFont.getGlyphNames(): " + str(glyphName)
- ordered.append([gid, glyphName, self.ColorLayers[glyphName]])
- ordered.sort()
+ from .otBase import OTTableWriter
- glyphMap = []
- layerMap = []
- for (gid, glyphName, layers) in ordered:
- glyphMap.append(struct.pack(">HHH", gid, len(layerMap), len(layers)))
- for layer in layers:
- layerMap.append(struct.pack(">HH", ttFont.getGlyphID(layer.name), layer.colorID))
+ if hasattr(self, "table"):
+ table = self.table
+ else:
+ table = self._toOTTable(ttFont)
- dataList = [struct.pack(">HHLLH", self.version, len(glyphMap), 14, 14+6*len(glyphMap), len(layerMap))]
- dataList.extend(glyphMap)
- dataList.extend(layerMap)
- data = bytesjoin(dataList)
- return data
+ writer = OTTableWriter(tableTag=self.tableTag)
+ table.compile(writer, ttFont)
+ return writer.getAllData()
def toXML(self, writer, ttFont):
- writer.simpletag("version", value=self.version)
- writer.newline()
- ordered = []
- glyphNames = self.ColorLayers.keys()
- for glyphName in glyphNames:
- try:
- gid = ttFont.getGlyphID(glyphName)
- except:
- assert 0, "COLR table contains a glyph name not in ttFont.getGlyphNames(): " + str(glyphName)
- ordered.append([gid, glyphName, self.ColorLayers[glyphName]])
- ordered.sort()
- for entry in ordered:
- writer.begintag("ColorGlyph", name=entry[1])
+ if hasattr(self, "table"):
+ self.table.toXML2(writer, ttFont)
+ else:
+ writer.simpletag("version", value=self.version)
writer.newline()
- for layer in entry[2]:
- layer.toXML(writer, ttFont)
- writer.endtag("ColorGlyph")
- writer.newline()
+ for baseGlyph in sorted(self.ColorLayers.keys(), key=ttFont.getGlyphID):
+ writer.begintag("ColorGlyph", name=baseGlyph)
+ writer.newline()
+ for layer in self.ColorLayers[baseGlyph]:
+ layer.toXML(writer, ttFont)
+ writer.endtag("ColorGlyph")
+ writer.newline()
def fromXML(self, name, attrs, content, ttFont):
- if not hasattr(self, "ColorLayers"):
- self.ColorLayers = {}
- self.getGlyphName = ttFont.getGlyphName # for use in get/set item functions, for access by GID
- if name == "ColorGlyph":
+ if name == "version": # old COLR v0 API
+ setattr(self, name, safeEval(attrs["value"]))
+ elif name == "ColorGlyph":
+ if not hasattr(self, "ColorLayers"):
+ self.ColorLayers = {}
glyphName = attrs["name"]
for element in content:
if isinstance(element, basestring):
@@ -111,32 +110,31 @@
layer = LayerRecord()
layer.fromXML(element[0], element[1], element[2], ttFont)
layers.append (layer)
- self[glyphName] = layers
- elif "value" in attrs:
- setattr(self, name, safeEval(attrs["value"]))
+ self.ColorLayers[glyphName] = layers
+ else: # new COLR v1 API
+ from . import otTables
- def __getitem__(self, glyphSelector):
- if isinstance(glyphSelector, int):
- # its a gid, convert to glyph name
- glyphSelector = self.getGlyphName(glyphSelector)
+ if not hasattr(self, "table"):
+ tableClass = getattr(otTables, self.tableTag)
+ self.table = tableClass()
+ self.table.fromXML(name, attrs, content, ttFont)
+ self.table.populateDefaults()
- if glyphSelector not in self.ColorLayers:
- return None
+ def __getitem__(self, glyphName):
+ if not isinstance(glyphName, str):
+ raise TypeError(f"expected str, found {type(glyphName).__name__}")
+ return self.ColorLayers[glyphName]
- return self.ColorLayers[glyphSelector]
+ def __setitem__(self, glyphName, value):
+ if not isinstance(glyphName, str):
+ raise TypeError(f"expected str, found {type(glyphName).__name__}")
+ if value is not None:
+ self.ColorLayers[glyphName] = value
+ elif glyphName in self.ColorLayers:
+ del self.ColorLayers[glyphName]
- def __setitem__(self, glyphSelector, value):
- if isinstance(glyphSelector, int):
- # its a gid, convert to glyph name
- glyphSelector = self.getGlyphName(glyphSelector)
-
- if value:
- self.ColorLayers[glyphSelector] = value
- elif glyphSelector in self.ColorLayers:
- del self.ColorLayers[glyphSelector]
-
- def __delitem__(self, glyphSelector):
- del self.ColorLayers[glyphSelector]
+ def __delitem__(self, glyphName):
+ del self.ColorLayers[glyphName]
class LayerRecord(object):
@@ -151,8 +149,6 @@
def fromXML(self, eltname, attrs, content, ttFont):
for (name, value) in attrs.items():
if name == "name":
- if isinstance(value, int):
- value = ttFont.getGlyphName(value)
setattr(self, name, value)
else:
setattr(self, name, safeEval(value))
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
index 6182134..786238a 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -652,6 +652,15 @@
else:
table = self.__dict__.copy()
+ # some count references may have been initialized in a custom preWrite; we set
+ # these in the writer's state beforehand (instead of sequentially) so they will
+ # be propagated to all nested subtables even if the count appears in the current
+ # table only *after* the offset to the subtable that it is counting.
+ for conv in self.getConverters():
+ if conv.isCount and conv.isPropagated:
+ value = table.get(conv.name)
+ if isinstance(value, CountReference):
+ writer[conv.name] = value
if hasattr(self, 'sortCoverageLast'):
writer.sortCoverageLast = 1
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
index 6f02050..b737e5c 100644
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -13,7 +13,9 @@
OTTableReader, OTTableWriter, ValueRecordFactory)
from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
ContextualMorphAction, LigatureMorphAction,
- InsertionMorphAction, MorxSubtable)
+ InsertionMorphAction, MorxSubtable, VariableFloat,
+ VariableInt, ExtendMode as _ExtendMode)
+from itertools import zip_longest
from functools import partial
import struct
import logging
@@ -134,7 +136,22 @@
self.tableClass = tableClass
self.isCount = name.endswith("Count") or name in ['DesignAxisRecordSize', 'ValueRecordSize']
self.isLookupType = name.endswith("LookupType") or name == "MorphType"
- self.isPropagated = name in ["ClassCount", "Class2Count", "FeatureTag", "SettingsCount", "VarRegionCount", "MappingCount", "RegionAxisCount", 'DesignAxisCount', 'DesignAxisRecordSize', 'AxisValueCount', 'ValueRecordSize', 'AxisCount']
+ self.isPropagated = name in [
+ "ClassCount",
+ "Class2Count",
+ "FeatureTag",
+ "SettingsCount",
+ "VarRegionCount",
+ "MappingCount",
+ "RegionAxisCount",
+ "DesignAxisCount",
+ "DesignAxisRecordSize",
+ "AxisValueCount",
+ "ValueRecordSize",
+ "AxisCount",
+ "BaseGlyphRecordCount",
+ "LayerRecordCount",
+ ]
def readArray(self, reader, font, tableDict, count):
"""Read an array of values from the reader."""
@@ -185,15 +202,22 @@
class SimpleValue(BaseConverter):
+ @staticmethod
+ def toString(value):
+ return value
+ @staticmethod
+ def fromString(value):
+ return value
def xmlWrite(self, xmlWriter, font, value, name, attrs):
- xmlWriter.simpletag(name, attrs + [("value", value)])
+ xmlWriter.simpletag(name, attrs + [("value", self.toString(value))])
xmlWriter.newline()
def xmlRead(self, attrs, content, font):
- return attrs["value"]
+ return self.fromString(attrs["value"])
class IntValue(SimpleValue):
- def xmlRead(self, attrs, content, font):
- return int(attrs["value"], 0)
+ @staticmethod
+ def fromString(value):
+ return int(value, 0)
class Long(IntValue):
staticSize = 4
@@ -210,9 +234,9 @@
writer.writeULong(value)
class Flags32(ULong):
- def xmlWrite(self, xmlWriter, font, value, name, attrs):
- xmlWriter.simpletag(name, attrs + [("value", "0x%08X" % value)])
- xmlWriter.newline()
+ @staticmethod
+ def toString(value):
+ return "0x%08X" % value
class Short(IntValue):
staticSize = 2
@@ -303,8 +327,9 @@
class FloatValue(SimpleValue):
- def xmlRead(self, attrs, content, font):
- return float(attrs["value"])
+ @staticmethod
+ def fromString(value):
+ return float(value)
class DeciPoints(FloatValue):
staticSize = 2
@@ -320,11 +345,12 @@
return fi2fl(reader.readLong(), 16)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeLong(fl2fi(value, 16))
- def xmlRead(self, attrs, content, font):
- return str2fl(attrs["value"], 16)
- def xmlWrite(self, xmlWriter, font, value, name, attrs):
- xmlWriter.simpletag(name, attrs + [("value", fl2str(value, 16))])
- xmlWriter.newline()
+ @staticmethod
+ def fromString(value):
+ return str2fl(value, 16)
+ @staticmethod
+ def toString(value):
+ return fl2str(value, 16)
class F2Dot14(FloatValue):
staticSize = 2
@@ -332,13 +358,14 @@
return fi2fl(reader.readShort(), 14)
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeShort(fl2fi(value, 14))
- def xmlRead(self, attrs, content, font):
- return str2fl(attrs["value"], 14)
- def xmlWrite(self, xmlWriter, font, value, name, attrs):
- xmlWriter.simpletag(name, attrs + [("value", fl2str(value, 14))])
- xmlWriter.newline()
+ @staticmethod
+ def fromString(value):
+ return str2fl(value, 14)
+ @staticmethod
+ def toString(value):
+ return fl2str(value, 14)
-class Version(BaseConverter):
+class Version(SimpleValue):
staticSize = 4
def read(self, reader, font, tableDict):
value = reader.readLong()
@@ -348,16 +375,12 @@
value = fi2ve(value)
assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
writer.writeLong(value)
- def xmlRead(self, attrs, content, font):
- value = attrs["value"]
- value = ve2fi(value)
- return value
- def xmlWrite(self, xmlWriter, font, value, name, attrs):
- value = fi2ve(value)
- value = "0x%08x" % value
- xmlWriter.simpletag(name, attrs + [("value", value)])
- xmlWriter.newline()
-
+ @staticmethod
+ def fromString(value):
+ return ve2fi(value)
+ @staticmethod
+ def toString(value):
+ return "0x%08x" % value
@staticmethod
def fromFloat(v):
return fl2fi(v, 16)
@@ -1583,6 +1606,111 @@
xmlWriter.comment(" ".join(flags))
xmlWriter.newline()
+def _issubclass_namedtuple(x):
+ return (
+ issubclass(x, tuple)
+ and getattr(x, "_fields", None) is not None
+ )
+
+
+class _NamedTupleConverter(BaseConverter):
+ # subclasses must override this
+ tupleClass = NotImplemented
+ # List[SimpleValue]
+ converterClasses = NotImplemented
+
+ def __init__(self, name, repeat, aux, tableClass=None):
+ # we expect all converters to be subclasses of SimpleValue
+ assert all(issubclass(klass, SimpleValue) for klass in self.converterClasses)
+ assert _issubclass_namedtuple(self.tupleClass), repr(self.tupleClass)
+ assert len(self.tupleClass._fields) == len(self.converterClasses)
+ assert tableClass is None # tableClass is unused by SimplValues
+ BaseConverter.__init__(self, name, repeat, aux)
+ self.converters = [
+ klass(name=name, repeat=None, aux=None)
+ for name, klass in zip(self.tupleClass._fields, self.converterClasses)
+ ]
+ self.convertersByName = {conv.name: conv for conv in self.converters}
+ # returned by getRecordSize method
+ self.staticSize = sum(c.staticSize for c in self.converters)
+
+ def read(self, reader, font, tableDict):
+ kwargs = {
+ conv.name: conv.read(reader, font, tableDict)
+ for conv in self.converters
+ }
+ return self.tupleClass(**kwargs)
+
+ def write(self, writer, font, tableDict, value, repeatIndex=None):
+ for conv in self.converters:
+ v = getattr(value, conv.name)
+ # repeatIndex is unused for SimpleValues
+ conv.write(writer, font, tableDict, v, repeatIndex=None)
+
+ def xmlWrite(self, xmlWriter, font, value, name, attrs):
+ assert value is not None
+ defaults = value.__new__.__defaults__ or ()
+ assert len(self.converters) >= len(defaults)
+ values = {}
+ required = object()
+ for conv, default in zip_longest(
+ reversed(self.converters),
+ reversed(defaults),
+ fillvalue=required,
+ ):
+ v = getattr(value, conv.name)
+ if default is required or v != default:
+ values[conv.name] = conv.toString(v)
+ if attrs is None:
+ attrs = []
+ attrs.extend(
+ (conv.name, values[conv.name])
+ for conv in self.converters
+ if conv.name in values
+ )
+ xmlWriter.simpletag(name, attrs)
+ xmlWriter.newline()
+
+ def xmlRead(self, attrs, content, font):
+ converters = self.convertersByName
+ kwargs = {
+ k: converters[k].fromString(v)
+ for k, v in attrs.items()
+ }
+ return self.tupleClass(**kwargs)
+
+
+class VariableScalar(_NamedTupleConverter):
+ tupleClass = VariableFloat
+ converterClasses = [Fixed, ULong]
+
+
+class VariableNormalizedScalar(_NamedTupleConverter):
+ tupleClass = VariableFloat
+ converterClasses = [F2Dot14, ULong]
+
+
+class VariablePosition(_NamedTupleConverter):
+ tupleClass = VariableInt
+ converterClasses = [Short, ULong]
+
+
+class VariableDistance(_NamedTupleConverter):
+ tupleClass = VariableInt
+ converterClasses = [UShort, ULong]
+
+
+class ExtendMode(UShort):
+ 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()
+
+
converterMapping = {
# type class
"int8": Int8,
@@ -1609,6 +1737,7 @@
"VarIdxMapValue": VarIdxMapValue,
"VarDataValue": VarDataValue,
"LookupFlag": LookupFlag,
+ "ExtendMode": ExtendMode,
# AAT
"CIDGlyphMap": CIDGlyphMap,
@@ -1624,4 +1753,10 @@
"STXHeader": lambda C: partial(STXHeader, tableClass=C),
"OffsetTo": lambda C: partial(Table, tableClass=C),
"LOffsetTo": lambda C: partial(LTable, tableClass=C),
+
+ # Variable types
+ "VariableScalar": VariableScalar,
+ "VariableNormalizedScalar": VariableNormalizedScalar,
+ "VariablePosition": VariablePosition,
+ "VariableDistance": VariableDistance,
}
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index 1fa9156..b893a7b 100755
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
@@ -1539,4 +1539,107 @@
('int16', 'CVTValueArray', 'NumCVTEntries', 0, 'CVT value'),
]),
+ #
+ # COLR
+ #
+
+ ('COLR', [
+ ('uint16', 'Version', None, None, 'Table version number (starts at 0).'),
+ ('uint16', 'BaseGlyphRecordCount', None, None, 'Number of Base Glyph Records.'),
+ ('LOffset', 'BaseGlyphRecordArray', None, None, 'Offset (from beginning of COLR table) to Base Glyph records.'),
+ ('LOffset', 'LayerRecordArray', None, None, 'Offset (from beginning of COLR table) to Layer Records.'),
+ ('uint16', 'LayerRecordCount', None, None, 'Number of Layer Records.'),
+ ('LOffset', 'BaseGlyphV1Array', None, 'Version >= 1', 'Offset (from beginning of COLR table) to array of Version-1 Base Glyph records.'),
+ ('LOffset', 'VarStore', None, 'Version >= 1', 'Offset to variation store (may be NULL)'),
+ ]),
+
+ ('BaseGlyphRecordArray', [
+ ('BaseGlyphRecord', 'BaseGlyphRecord', 'BaseGlyphRecordCount', 0, 'Base Glyph records.'),
+ ]),
+
+ ('BaseGlyphRecord', [
+ ('GlyphID', 'BaseGlyph', None, None, 'Glyph ID of reference glyph. This glyph is for reference only and is not rendered for color.'),
+ ('uint16', 'FirstLayerIndex', None, None, 'Index (from beginning of the Layer Records) to the layer record. There will be numLayers consecutive entries for this base glyph.'),
+ ('uint16', 'NumLayers', None, None, 'Number of color layers associated with this glyph.'),
+ ]),
+
+ ('LayerRecordArray', [
+ ('LayerRecord', 'LayerRecord', 'LayerRecordCount', 0, 'Layer records.'),
+ ]),
+
+ ('LayerRecord', [
+ ('GlyphID', 'LayerGlyph', None, None, 'Glyph ID of layer glyph (must be in z-order from bottom to top).'),
+ ('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
+ ]),
+
+ ('BaseGlyphV1Array', [
+ ('uint32', 'BaseGlyphCount', None, None, 'Number of Version-1 Base Glyph records'),
+ ('struct', 'BaseGlyphV1Record', 'BaseGlyphCount', 0, 'Array of Version-1 Base Glyph records'),
+ ]),
+
+ ('BaseGlyphV1Record', [
+ ('GlyphID', 'BaseGlyph', None, None, 'Glyph ID of reference glyph.'),
+ ('LOffset', 'LayerV1Array', None, None, 'Offset (from beginning of BaseGlyphV1Array) to LayerV1Array.'),
+ ]),
+
+ ('LayerV1Array', [
+ ('uint32', 'LayerCount', None, None, 'Number of Version-1 Layer records'),
+ ('struct', 'LayerV1Record', 'LayerCount', 0, 'Array of Version-1 Layer records'),
+ ]),
+
+ ('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 LayerV1Array) to Paint subtable.'),
+ ]),
+
+ ('Affine2x2', [
+ ('VariableScalar', 'xx', None, None, ''),
+ ('VariableScalar', 'xy', None, None, ''),
+ ('VariableScalar', 'yx', None, None, ''),
+ ('VariableScalar', 'yy', None, None, ''),
+ ]),
+
+ ('Point', [
+ ('VariablePosition', 'x', None, None, ''),
+ ('VariablePosition', 'y', None, None, ''),
+ ]),
+
+ ('Color', [
+ ('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
+ ('VariableNormalizedScalar', 'Transparency', None, None, 'Values outsided [0.,1.] reserved'),
+ ]),
+
+ ('ColorStop', [
+ ('VariableNormalizedScalar', 'StopOffset', None, None, ''),
+ ('Color', 'Color', None, None, ''),
+ ]),
+
+ ('ColorLine', [
+ ('ExtendMode', 'Extend', None, None, 'Enum {PAD = 0, REPEAT = 1, REFLECT = 2}'),
+ ('uint16', 'StopCount', None, None, 'Number of Color stops.'),
+ ('ColorStop', 'ColorStop', 'StopCount', 0, 'Array of Color stops.'),
+ ]),
+
+ ('PaintFormat1', [
+ ('uint16', 'PaintFormat', None, None, 'Format identifier-format = 1'),
+ ('Color', 'Color', None, None, 'A solid color paint.'),
+ ]),
+
+ ('PaintFormat2', [
+ ('uint16', 'PaintFormat', None, None, 'Format identifier-format = 2'),
+ ('LOffset', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
+ ('Point', 'p0', None, None, ''),
+ ('Point', 'p1', None, None, ''),
+ ('Point', 'p2', None, None, 'Normal; equal to p1 in simple cases.'),
+ ]),
+
+ ('PaintFormat3', [
+ ('uint16', 'PaintFormat', None, None, 'Format identifier-format = 3'),
+ ('LOffset', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
+ ('Point', 'c0', None, None, ''),
+ ('Point', 'c1', None, None, ''),
+ ('VariableDistance', 'r0', None, None, ''),
+ ('VariableDistance', 'r1', None, None, ''),
+ ('LOffsetTo(Affine2x2)', 'Affine', None, None, 'Offset (from beginning of Paint table) to Affine2x2 subtable.'),
+ ]),
]
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index 302c9b2..fa8638a 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -5,7 +5,11 @@
Most are constructed upon import from data in otData.py, all are populated with
converter objects from otConverters.py.
"""
+from enum import IntEnum
+import itertools
+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
import logging
@@ -1152,6 +1156,100 @@
ligs.append(lig)
+class COLR(BaseTable):
+
+ def decompile(self, reader, font):
+ # COLRv0 is exceptional in that LayerRecordCount appears *after* the
+ # LayerRecordArray it counts, but the parser logic expects Count fields
+ # to always precede the arrays. Here we work around this by parsing the
+ # LayerRecordCount before the rest of the table, and storing it in
+ # the reader's local state.
+ subReader = reader.getSubReader(offset=0)
+ for conv in self.getConverters():
+ if conv.name != "LayerRecordCount":
+ subReader.advance(conv.staticSize)
+ continue
+ conv = self.getConverterByName("LayerRecordCount")
+ reader[conv.name] = conv.read(subReader, font, tableDict={})
+ break
+ else:
+ raise AssertionError("LayerRecordCount converter not found")
+ return BaseTable.decompile(self, reader, font)
+
+ def preWrite(self, font):
+ # The writer similarly assumes Count values precede the things counted,
+ # thus here we pre-initialize a CountReference; the actual count value
+ # will be set to the lenght of the array by the time this is assembled.
+ self.LayerRecordCount = None
+ return {
+ **self.__dict__,
+ "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount")
+ }
+
+
+class BaseGlyphRecordArray(BaseTable):
+
+ def preWrite(self, font):
+ self.BaseGlyphRecord = sorted(
+ self.BaseGlyphRecord,
+ key=lambda rec: font.getGlyphID(rec.BaseGlyph)
+ )
+ return self.__dict__.copy()
+
+
+class BaseGlyphV1Array(BaseTable):
+
+ def preWrite(self, font):
+ self.BaseGlyphV1Record = sorted(
+ self.BaseGlyphV1Record,
+ key=lambda rec: font.getGlyphID(rec.BaseGlyph)
+ )
+ return self.__dict__.copy()
+
+
+
+class VariableValue(namedtuple("VariableValue", ["value", "varIdx"])):
+ __slots__ = ()
+
+ _value_mapper = None
+
+ def __new__(cls, value, varIdx=0):
+ return super().__new__(
+ cls,
+ cls._value_mapper(value) if cls._value_mapper else value,
+ varIdx
+ )
+
+ @classmethod
+ def _make(cls, iterable):
+ if cls._value_mapper:
+ it = iter(iterable)
+ try:
+ value = next(it)
+ except StopIteration:
+ pass
+ else:
+ value = cls._value_mapper(value)
+ iterable = itertools.chain((value,), it)
+ return super()._make(iterable)
+
+
+class VariableFloat(VariableValue):
+ __slots__ = ()
+ _value_mapper = float
+
+
+class VariableInt(VariableValue):
+ __slots__ = ()
+ _value_mapper = otRound
+
+
+class ExtendMode(IntEnum):
+ PAD = 0
+ REPEAT = 1
+ REFLECT = 2
+
+
# 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
diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO
index 59a6af8..ba60e7c 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.7.0
+Version: 4.8.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -193,7 +193,7 @@
The module exports a ElementTree-like API for reading/writing XML files, and
allows to use as the backend either the built-in ``xml.etree`` module or
- `lxml <https://http://lxml.de>`__. The latter is preferred whenever present,
+ `lxml <https://lxml.de>`__. The latter is preferred whenever present,
as it is generally faster and more secure.
*Extra:* ``lxml``
@@ -414,13 +414,29 @@
Changelog
~~~~~~~~~
+ 4.8.0 (released 2020-04-16)
+ ---------------------------
+
+ - [feaLib] If Parser is initialized without a ``glyphNames`` parameter, it cannot
+ distinguish between a glyph name containing an hyphen, or a range of glyph names;
+ instead of raising an error, it now interprets them as literal glyph names, while
+ also outputting a logging warning to alert user about the ambiguity (#1768, #1870).
+ - [feaLib] When serializing AST to string, emit spaces around hyphens that denote
+ ranges. Also, fixed an issue with CID ranges when round-tripping AST->string->AST
+ (#1872).
+ - [Snippets/otf2ttf] In otf2ttf.py script update LSB in hmtx to match xMin (#1873).
+ - [colorLib] Added experimental support for building ``COLR`` v1 tables as per
+ the `colr-gradients-spec <https://github.com/googlefonts/colr-gradients-spec/blob/master/colr-gradients-spec.md>`__
+ draft proposal. **NOTE**: both the API and the XML dump of ``COLR`` v1 are
+ susceptible to change while the proposal is being discussed and formalized (#1822).
+
4.7.0 (released 2020-04-03)
---------------------------
- [cu2qu] Added ``fontTools.cu2qu`` package, imported from the original
`cu2qu <https://github.com/googlefonts/cu2qu>`__ project. The ``cu2qu.pens`` module
was moved to ``fontTools.pens.cu2quPen``. The optional cu2qu extension module
- can be compiled by installing `Cython <https://cython.org/>` before installing
+ can be compiled by installing `Cython <https://cython.org/>`__ before installing
fonttools from source (i.e. git repo or sdist tarball). The wheel package that
is published on PyPI (i.e. the one ``pip`` downloads, unless ``--no-binary``
option is used), will continue to be pure-Python for now (#1868).
@@ -2031,13 +2047,13 @@
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.6
-Provides-Extra: unicode
-Provides-Extra: ufo
-Provides-Extra: woff
-Provides-Extra: type1
Provides-Extra: graphite
+Provides-Extra: interpolatable
Provides-Extra: all
+Provides-Extra: unicode
+Provides-Extra: symfont
Provides-Extra: lxml
Provides-Extra: plot
-Provides-Extra: symfont
-Provides-Extra: interpolatable
+Provides-Extra: type1
+Provides-Extra: ufo
+Provides-Extra: woff
diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt
index 2924cc2..acf50be 100644
--- a/Lib/fonttools.egg-info/SOURCES.txt
+++ b/Lib/fonttools.egg-info/SOURCES.txt
@@ -519,6 +519,7 @@
Tests/feaLib/data/GSUB_6.ttx
Tests/feaLib/data/GSUB_8.fea
Tests/feaLib/data/GSUB_8.ttx
+Tests/feaLib/data/GSUB_error.fea
Tests/feaLib/data/GlyphClassDef.fea
Tests/feaLib/data/GlyphClassDef.ttx
Tests/feaLib/data/LigatureCaretByIndex.fea
@@ -577,6 +578,8 @@
Tests/feaLib/data/bug568.ttx
Tests/feaLib/data/bug633.fea
Tests/feaLib/data/bug633.ttx
+Tests/feaLib/data/cid_range.fea
+Tests/feaLib/data/cid_range.ttx
Tests/feaLib/data/enum.fea
Tests/feaLib/data/enum.ttx
Tests/feaLib/data/feature_aalt.fea
@@ -873,6 +876,7 @@
Tests/ttLib/tables/C_B_L_C_test.py
Tests/ttLib/tables/C_F_F__2_test.py
Tests/ttLib/tables/C_F_F_test.py
+Tests/ttLib/tables/C_O_L_R_test.py
Tests/ttLib/tables/C_P_A_L_test.py
Tests/ttLib/tables/M_V_A_R_test.py
Tests/ttLib/tables/O_S_2f_2_test.py
diff --git a/METADATA b/METADATA
index 639b948..70ad5b8 100644
--- a/METADATA
+++ b/METADATA
@@ -7,12 +7,12 @@
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/releases/download/4.7.0/fonttools-4.7.0.zip"
+ value: "https://github.com/fonttools/fonttools/releases/download/4.8.0/fonttools-4.8.0.zip"
}
- version: "4.7.0"
+ version: "4.8.0"
last_upgrade_date {
year: 2020
month: 4
- day: 3
+ day: 16
}
}
diff --git a/NEWS.rst b/NEWS.rst
index f292a61..f771591 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,10 +1,26 @@
+4.8.0 (released 2020-04-16)
+---------------------------
+
+- [feaLib] If Parser is initialized without a ``glyphNames`` parameter, it cannot
+ distinguish between a glyph name containing an hyphen, or a range of glyph names;
+ instead of raising an error, it now interprets them as literal glyph names, while
+ also outputting a logging warning to alert user about the ambiguity (#1768, #1870).
+- [feaLib] When serializing AST to string, emit spaces around hyphens that denote
+ ranges. Also, fixed an issue with CID ranges when round-tripping AST->string->AST
+ (#1872).
+- [Snippets/otf2ttf] In otf2ttf.py script update LSB in hmtx to match xMin (#1873).
+- [colorLib] Added experimental support for building ``COLR`` v1 tables as per
+ the `colr-gradients-spec <https://github.com/googlefonts/colr-gradients-spec/blob/master/colr-gradients-spec.md>`__
+ draft proposal. **NOTE**: both the API and the XML dump of ``COLR`` v1 are
+ susceptible to change while the proposal is being discussed and formalized (#1822).
+
4.7.0 (released 2020-04-03)
---------------------------
- [cu2qu] Added ``fontTools.cu2qu`` package, imported from the original
`cu2qu <https://github.com/googlefonts/cu2qu>`__ project. The ``cu2qu.pens`` module
was moved to ``fontTools.pens.cu2quPen``. The optional cu2qu extension module
- can be compiled by installing `Cython <https://cython.org/>` before installing
+ can be compiled by installing `Cython <https://cython.org/>`__ before installing
fonttools from source (i.e. git repo or sdist tarball). The wheel package that
is published on PyPI (i.e. the one ``pip`` downloads, unless ``--no-binary``
option is used), will continue to be pure-Python for now (#1868).
diff --git a/PKG-INFO b/PKG-INFO
index 59a6af8..ba60e7c 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.7.0
+Version: 4.8.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -193,7 +193,7 @@
The module exports a ElementTree-like API for reading/writing XML files, and
allows to use as the backend either the built-in ``xml.etree`` module or
- `lxml <https://http://lxml.de>`__. The latter is preferred whenever present,
+ `lxml <https://lxml.de>`__. The latter is preferred whenever present,
as it is generally faster and more secure.
*Extra:* ``lxml``
@@ -414,13 +414,29 @@
Changelog
~~~~~~~~~
+ 4.8.0 (released 2020-04-16)
+ ---------------------------
+
+ - [feaLib] If Parser is initialized without a ``glyphNames`` parameter, it cannot
+ distinguish between a glyph name containing an hyphen, or a range of glyph names;
+ instead of raising an error, it now interprets them as literal glyph names, while
+ also outputting a logging warning to alert user about the ambiguity (#1768, #1870).
+ - [feaLib] When serializing AST to string, emit spaces around hyphens that denote
+ ranges. Also, fixed an issue with CID ranges when round-tripping AST->string->AST
+ (#1872).
+ - [Snippets/otf2ttf] In otf2ttf.py script update LSB in hmtx to match xMin (#1873).
+ - [colorLib] Added experimental support for building ``COLR`` v1 tables as per
+ the `colr-gradients-spec <https://github.com/googlefonts/colr-gradients-spec/blob/master/colr-gradients-spec.md>`__
+ draft proposal. **NOTE**: both the API and the XML dump of ``COLR`` v1 are
+ susceptible to change while the proposal is being discussed and formalized (#1822).
+
4.7.0 (released 2020-04-03)
---------------------------
- [cu2qu] Added ``fontTools.cu2qu`` package, imported from the original
`cu2qu <https://github.com/googlefonts/cu2qu>`__ project. The ``cu2qu.pens`` module
was moved to ``fontTools.pens.cu2quPen``. The optional cu2qu extension module
- can be compiled by installing `Cython <https://cython.org/>` before installing
+ can be compiled by installing `Cython <https://cython.org/>`__ before installing
fonttools from source (i.e. git repo or sdist tarball). The wheel package that
is published on PyPI (i.e. the one ``pip`` downloads, unless ``--no-binary``
option is used), will continue to be pure-Python for now (#1868).
@@ -2031,13 +2047,13 @@
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.6
-Provides-Extra: unicode
-Provides-Extra: ufo
-Provides-Extra: woff
-Provides-Extra: type1
Provides-Extra: graphite
+Provides-Extra: interpolatable
Provides-Extra: all
+Provides-Extra: unicode
+Provides-Extra: symfont
Provides-Extra: lxml
Provides-Extra: plot
-Provides-Extra: symfont
-Provides-Extra: interpolatable
+Provides-Extra: type1
+Provides-Extra: ufo
+Provides-Extra: woff
diff --git a/README.rst b/README.rst
index a6a3533..05b44b7 100644
--- a/README.rst
+++ b/README.rst
@@ -183,7 +183,7 @@
The module exports a ElementTree-like API for reading/writing XML files, and
allows to use as the backend either the built-in ``xml.etree`` module or
- `lxml <https://http://lxml.de>`__. The latter is preferred whenever present,
+ `lxml <https://lxml.de>`__. The latter is preferred whenever present,
as it is generally faster and more secure.
*Extra:* ``lxml``
diff --git a/Snippets/otf2ttf.py b/Snippets/otf2ttf.py
index 7a6bfe8..b925b33 100755
--- a/Snippets/otf2ttf.py
+++ b/Snippets/otf2ttf.py
@@ -38,6 +38,13 @@
return quadGlyphs
+def update_hmtx(ttFont, glyf):
+ hmtx = ttFont["hmtx"]
+ for glyphName, glyph in glyf.glyphs.items():
+ if hasattr(glyph, 'xMin'):
+ hmtx[glyphName] = (hmtx[glyphName][0], glyph.xMin)
+
+
def otf_to_ttf(ttFont, post_format=POST_FORMAT, **kwargs):
assert ttFont.sfntVersion == "OTTO"
assert "CFF " in ttFont
@@ -50,6 +57,7 @@
glyf.glyphs = glyphs_to_quadratic(ttFont.getGlyphSet(), **kwargs)
del ttFont["CFF "]
glyf.compile(ttFont)
+ update_hmtx(ttFont, glyf)
ttFont["maxp"] = maxp = newTable("maxp")
maxp.tableVersion = 0x00010000
diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py
index c517187..24cac8b 100644
--- a/Tests/colorLib/builder_test.py
+++ b/Tests/colorLib/builder_test.py
@@ -1,4 +1,5 @@
from fontTools.ttLib import newTable
+from fontTools.ttLib.tables import otTables as ot
from fontTools.colorLib import builder
from fontTools.colorLib.errors import ColorLibError
import pytest
@@ -185,3 +186,552 @@
),
):
builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]])
+
+
+def test_buildColor():
+ c = builder.buildColor(0)
+ assert c.PaletteIndex == 0
+ assert c.Transparency.value == 0.0
+ assert c.Transparency.varIdx == 0
+
+ c = builder.buildColor(1, transparency=0.5)
+ assert c.PaletteIndex == 1
+ assert c.Transparency.value == 0.5
+ assert c.Transparency.varIdx == 0
+
+ c = builder.buildColor(3, transparency=builder.VariableFloat(0.5, varIdx=2))
+ assert c.PaletteIndex == 3
+ assert c.Transparency.value == 0.5
+ assert c.Transparency.varIdx == 2
+
+
+def test_buildSolidColorPaint():
+ p = builder.buildSolidColorPaint(0)
+ assert p.Format == 1
+ assert p.Color.PaletteIndex == 0
+ assert p.Color.Transparency.value == 0.0
+ assert p.Color.Transparency.varIdx == 0
+
+ p = builder.buildSolidColorPaint(1, transparency=0.5)
+ assert p.Format == 1
+ assert p.Color.PaletteIndex == 1
+ assert p.Color.Transparency.value == 0.5
+ assert p.Color.Transparency.varIdx == 0
+
+ p = builder.buildSolidColorPaint(
+ 3, transparency=builder.VariableFloat(0.5, varIdx=2)
+ )
+ assert p.Format == 1
+ assert p.Color.PaletteIndex == 3
+ assert p.Color.Transparency.value == 0.5
+ assert p.Color.Transparency.varIdx == 2
+
+
+def test_buildColorStop():
+ s = builder.buildColorStop(0.1, 2)
+ assert s.StopOffset == builder.VariableFloat(0.1)
+ assert s.Color.PaletteIndex == 2
+ assert s.Color.Transparency == builder._DEFAULT_TRANSPARENCY
+
+ s = builder.buildColorStop(offset=0.2, paletteIndex=3, transparency=0.4)
+ assert s.StopOffset == builder.VariableFloat(0.2)
+ assert s.Color == builder.buildColor(3, transparency=0.4)
+
+ s = builder.buildColorStop(
+ offset=builder.VariableFloat(0.0, varIdx=1),
+ paletteIndex=0,
+ transparency=builder.VariableFloat(0.3, varIdx=2),
+ )
+ assert s.StopOffset == builder.VariableFloat(0.0, varIdx=1)
+ assert s.Color.PaletteIndex == 0
+ assert s.Color.Transparency == builder.VariableFloat(0.3, varIdx=2)
+
+
+def test_buildColorLine():
+ stops = [(0.0, 0), (0.5, 1), (1.0, 2)]
+
+ cline = builder.buildColorLine(stops)
+ assert cline.Extend == builder.ExtendMode.PAD
+ assert cline.StopCount == 3
+ assert [
+ (cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop
+ ] == stops
+
+ cline = builder.buildColorLine(stops, extend="pad")
+ assert cline.Extend == builder.ExtendMode.PAD
+
+ cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REPEAT)
+ assert cline.Extend == builder.ExtendMode.REPEAT
+
+ cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REFLECT)
+ assert cline.Extend == builder.ExtendMode.REFLECT
+
+ cline = builder.buildColorLine([builder.buildColorStop(*s) for s in stops])
+ assert [
+ (cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop
+ ] == stops
+
+ stops = [
+ {"offset": (0.0, 1), "paletteIndex": 0, "transparency": (0.5, 2)},
+ {"offset": (1.0, 3), "paletteIndex": 1, "transparency": (0.3, 4)},
+ ]
+ cline = builder.buildColorLine(stops)
+ assert [
+ {
+ "offset": cs.StopOffset,
+ "paletteIndex": cs.Color.PaletteIndex,
+ "transparency": cs.Color.Transparency,
+ }
+ for cs in cline.ColorStop
+ ] == stops
+
+
+def test_buildPoint():
+ pt = builder.buildPoint(0, 1)
+ assert pt.x == builder.VariableInt(0)
+ assert pt.y == builder.VariableInt(1)
+
+ pt = builder.buildPoint(
+ builder.VariableInt(2, varIdx=1), builder.VariableInt(3, varIdx=2)
+ )
+ assert pt.x == builder.VariableInt(2, varIdx=1)
+ assert pt.y == builder.VariableInt(3, varIdx=2)
+
+ # float coords are rounded
+ pt = builder.buildPoint(x=-2.5, y=3.5)
+ assert pt.x == builder.VariableInt(-2)
+ assert pt.y == builder.VariableInt(4)
+
+ # tuple args are cast to VariableInt namedtuple
+ pt = builder.buildPoint((1, 2), (3, 4))
+ assert pt.x == builder.VariableInt(1, varIdx=2)
+ assert pt.y == builder.VariableInt(3, varIdx=4)
+
+
+def test_buildAffine2x2():
+ matrix = builder.buildAffine2x2(1.5, 0, 0.5, 2.0)
+ assert matrix.xx == builder.VariableFloat(1.5)
+ assert matrix.xy == builder.VariableFloat(0.0)
+ assert matrix.yx == builder.VariableFloat(0.5)
+ assert matrix.yy == builder.VariableFloat(2.0)
+
+
+def test_buildLinearGradientPaint():
+ color_stops = [
+ builder.buildColorStop(0.0, 0),
+ builder.buildColorStop(0.5, 1),
+ builder.buildColorStop(1.0, 2, transparency=0.8),
+ ]
+ color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT)
+ p0 = builder.buildPoint(x=100, y=200)
+ p1 = builder.buildPoint(x=150, y=250)
+
+ gradient = builder.buildLinearGradientPaint(color_line, p0, p1)
+ assert gradient.Format == 2
+ assert gradient.ColorLine == color_line
+ assert gradient.p0 == p0
+ assert gradient.p1 == p1
+ assert gradient.p2 == gradient.p1
+ assert gradient.p2 is not gradient.p1
+
+ gradient = builder.buildLinearGradientPaint({"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))
+ assert gradient.p2 == builder.buildPoint(x=150, y=230)
+ assert gradient.p2 != gradient.p1
+
+
+def test_buildRadialGradientPaint():
+ color_stops = [
+ builder.buildColorStop(0.0, 0),
+ builder.buildColorStop(0.5, 1),
+ builder.buildColorStop(1.0, 2, transparency=0.8),
+ ]
+ color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT)
+ c0 = builder.buildPoint(x=100, y=200)
+ c1 = builder.buildPoint(x=150, y=250)
+ r0 = builder.VariableInt(10)
+ r1 = builder.VariableInt(5)
+
+ gradient = builder.buildRadialGradientPaint(color_line, c0, c1, r0, r1)
+ assert gradient.Format == 3
+ assert gradient.ColorLine == color_line
+ assert gradient.c0 == c0
+ assert gradient.c1 == c1
+ assert gradient.r0 == r0
+ assert gradient.r1 == r1
+ assert gradient.Affine is None
+
+ gradient = builder.buildRadialGradientPaint({"stops": color_stops}, c0, c1, r0, r1)
+ assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
+ assert gradient.ColorLine.ColorStop == color_stops
+
+ matrix = builder.buildAffine2x2(2.0, 0.0, 0.0, 2.0)
+ gradient = builder.buildRadialGradientPaint(
+ color_line, c0, c1, r0, r1, affine=matrix
+ )
+ assert gradient.Affine == matrix
+
+ gradient = builder.buildRadialGradientPaint(
+ color_line, c0, c1, r0, r1, affine=(2.0, 0.0, 0.0, 2.0)
+ )
+ assert gradient.Affine == matrix
+
+
+def test_buildLayerV1Record():
+ layer = builder.buildLayerV1Record("a", 2)
+ assert layer.LayerGlyph == "a"
+ assert layer.Paint.Format == 1
+ assert layer.Paint.Color.PaletteIndex == 2
+
+ layer = builder.buildLayerV1Record("a", builder.buildSolidColorPaint(3, 0.9))
+ assert layer.Paint.Format == 1
+ assert layer.Paint.Color.PaletteIndex == 3
+ assert layer.Paint.Color.Transparency.value == 0.9
+
+ layer = builder.buildLayerV1Record(
+ "a",
+ builder.buildLinearGradientPaint(
+ {"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250)
+ ),
+ )
+ assert layer.Paint.Format == 2
+ 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
+ assert layer.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 4
+ assert layer.Paint.p0.x.value == 100
+ assert layer.Paint.p0.y.value == 200
+ assert layer.Paint.p1.x.value == 150
+ assert layer.Paint.p1.y.value == 250
+
+ layer = builder.buildLayerV1Record(
+ "a",
+ builder.buildRadialGradientPaint(
+ {
+ "stops": [
+ (0.0, 5),
+ {"offset": 0.5, "paletteIndex": 6, "transparency": 0.8},
+ (1.0, 7),
+ ]
+ },
+ (50, 50),
+ (75, 75),
+ 30,
+ 10,
+ ),
+ )
+ assert layer.Paint.Format == 3
+ 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
+ assert layer.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 6
+ assert layer.Paint.ColorLine.ColorStop[1].Color.Transparency.value == 0.8
+ assert layer.Paint.ColorLine.ColorStop[2].StopOffset.value == 1.0
+ assert layer.Paint.ColorLine.ColorStop[2].Color.PaletteIndex == 7
+ assert layer.Paint.c0.x.value == 50
+ assert layer.Paint.c0.y.value == 50
+ assert layer.Paint.c1.x.value == 75
+ assert layer.Paint.c1.y.value == 75
+ assert layer.Paint.r0.value == 30
+ 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
+ assert layer.Paint.Color.PaletteIndex == 0
+
+ layer = builder.buildLayerV1Record(
+ "a",
+ {
+ "format": 2,
+ "colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
+ "p0": (0, 0),
+ "p1": (10, 10),
+ },
+ )
+ assert layer.Paint.Format == 2
+ assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
+
+ layer = builder.buildLayerV1Record(
+ "a",
+ {
+ "format": 3,
+ "colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
+ "c0": (0, 0),
+ "c1": (10, 10),
+ "r0": 4,
+ "r1": 0,
+ },
+ )
+ assert layer.Paint.Format == 3
+ assert layer.Paint.r0.value == 4
+
+
+def test_buildLayerV1Array():
+ layers = [
+ ("a", 1),
+ ("b", {"format": 1, "paletteIndex": 2, "transparency": 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, "transparency": 0.8},
+ {"offset": 1.0, "paletteIndex": 7},
+ ]
+ },
+ "c0": (50, 50),
+ "c1": (75, 75),
+ "r0": 30,
+ "r1": 10,
+ },
+ ),
+ builder.buildLayerV1Record("e", builder.buildSolidColorPaint(8)),
+ ]
+ layersArray = builder.buildLayerV1Array(layers)
+
+ assert layersArray.LayerCount == len(layersArray.LayerV1Record)
+ assert all(isinstance(l, ot.LayerV1Record) for l in layersArray.LayerV1Record)
+
+
+def test_buildBaseGlyphV1Record():
+ baseGlyphRec = builder.buildBaseGlyphV1Record("a", [("b", 0), ("c", 1)])
+ assert baseGlyphRec.BaseGlyph == "a"
+ assert isinstance(baseGlyphRec.LayerV1Array, ot.LayerV1Array)
+
+ layerArray = builder.buildLayerV1Array([("b", 0), ("c", 1)])
+ baseGlyphRec = builder.buildBaseGlyphV1Record("a", layerArray)
+ assert baseGlyphRec.BaseGlyph == "a"
+ assert baseGlyphRec.LayerV1Array == layerArray
+
+
+def test_buildBaseGlyphV1Array():
+ colorGlyphs = {
+ "a": [("b", 0), ("c", 1)],
+ "d": [
+ ("e", {"format": 1, "paletteIndex": 2, "transparency": 0.8}),
+ (
+ "f",
+ {
+ "format": 3,
+ "colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "reflect"},
+ "c0": (0, 0),
+ "c1": (0, 0),
+ "r0": 10,
+ "r1": 0,
+ },
+ ),
+ ],
+ "g": builder.buildLayerV1Array([("h", 5)]),
+ }
+ glyphMap = {
+ ".notdef": 0,
+ "a": 4,
+ "b": 3,
+ "c": 2,
+ "d": 1,
+ "e": 5,
+ "f": 6,
+ "g": 7,
+ "h": 8,
+ }
+
+ baseGlyphArray = builder.buildBaseGlyphV1Array(colorGlyphs, glyphMap)
+ assert baseGlyphArray.BaseGlyphCount == len(colorGlyphs)
+ assert baseGlyphArray.BaseGlyphV1Record[0].BaseGlyph == "d"
+ assert baseGlyphArray.BaseGlyphV1Record[1].BaseGlyph == "a"
+ assert baseGlyphArray.BaseGlyphV1Record[2].BaseGlyph == "g"
+
+ baseGlyphArray = builder.buildBaseGlyphV1Array(colorGlyphs)
+ assert baseGlyphArray.BaseGlyphCount == len(colorGlyphs)
+ assert baseGlyphArray.BaseGlyphV1Record[0].BaseGlyph == "a"
+ assert baseGlyphArray.BaseGlyphV1Record[1].BaseGlyph == "d"
+ assert baseGlyphArray.BaseGlyphV1Record[2].BaseGlyph == "g"
+
+
+def test_splitSolidAndGradientGlyphs():
+ colorGlyphs = {
+ "a": [
+ ("b", 0),
+ ("c", 1),
+ ("d", {"format": 1, "paletteIndex": 2}),
+ ("e", builder.buildSolidColorPaint(paletteIndex=3)),
+ ]
+ }
+
+ colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
+
+ assert colorGlyphsV0 == {"a": [("b", 0), ("c", 1), ("d", 2), ("e", 3)]}
+ assert not colorGlyphsV1
+
+ colorGlyphs = {
+ "a": [("b", builder.buildSolidColorPaint(paletteIndex=0, transparency=1.0))]
+ }
+
+ colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
+
+ assert not colorGlyphsV0
+ assert colorGlyphsV1 == colorGlyphs
+
+ colorGlyphs = {
+ "a": [("b", 0)],
+ "c": [
+ ("d", 1),
+ (
+ "e",
+ {
+ "format": 2,
+ "colorLine": {"stops": [(0.0, 2), (1.0, 3)]},
+ "p0": (0, 0),
+ "p1": (10, 10),
+ },
+ ),
+ ],
+ }
+
+ colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
+
+ assert colorGlyphsV0 == {"a": [("b", 0)]}
+ assert "a" not in colorGlyphsV1
+ assert "c" in colorGlyphsV1
+ assert len(colorGlyphsV1["c"]) == 2
+
+ layer_d = colorGlyphsV1["c"][0]
+ assert layer_d[0] == "d"
+ assert isinstance(layer_d[1], ot.Paint)
+ assert layer_d[1].Format == 1
+
+ layer_e = colorGlyphsV1["c"][1]
+ assert layer_e[0] == "e"
+ assert isinstance(layer_e[1], ot.Paint)
+ assert layer_e[1].Format == 2
+
+
+class BuildCOLRTest(object):
+ def test_automatic_version_all_solid_color_glyphs(self):
+ colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]})
+ assert colr.version == 0
+ assert hasattr(colr, "ColorLayers")
+ assert colr.ColorLayers["a"][0].name == "b"
+ assert colr.ColorLayers["a"][1].name == "c"
+
+ def test_automatic_version_no_solid_color_glyphs(self):
+ colr = builder.buildCOLR(
+ {
+ "a": [
+ (
+ "b",
+ {
+ "format": 3,
+ "colorLine": {
+ "stops": [(0.0, 0), (1.0, 1)],
+ "extend": "repeat",
+ },
+ "c0": (1, 0),
+ "c1": (10, 0),
+ "r0": 4,
+ "r1": 2,
+ },
+ ),
+ ("c", {"format": 1, "paletteIndex": 2, "transparency": 0.8}),
+ ],
+ "d": [
+ (
+ "e",
+ {
+ "format": 2,
+ "colorLine": {
+ "stops": [(0.0, 2), (1.0, 3)],
+ "extend": "reflect",
+ },
+ "p0": (1, 2),
+ "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)
+ assert colr.table.BaseGlyphRecordCount == 0
+ assert colr.table.BaseGlyphRecordArray is None
+ assert colr.table.LayerRecordCount == 0
+ assert colr.table.LayerRecordArray is None
+
+ def test_automatic_version_mixed_solid_and_gradient_glyphs(self):
+ colr = builder.buildCOLR(
+ {
+ "a": [("b", 0), ("c", 1)],
+ "d": [
+ (
+ "e",
+ {
+ "format": 2,
+ "colorLine": {"stops": [(0.0, 2), (1.0, 3)]},
+ "p0": (1, 2),
+ "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)
+ assert colr.table.VarStore is None
+
+ assert colr.table.BaseGlyphRecordCount == 1
+ assert isinstance(colr.table.BaseGlyphRecordArray, ot.BaseGlyphRecordArray)
+ assert colr.table.LayerRecordCount == 2
+ assert isinstance(colr.table.LayerRecordArray, ot.LayerRecordArray)
+
+ assert isinstance(colr.table.BaseGlyphV1Array, ot.BaseGlyphV1Array)
+ assert colr.table.BaseGlyphV1Array.BaseGlyphCount == 1
+ assert isinstance(
+ colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0], ot.BaseGlyphV1Record
+ )
+ assert colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0].BaseGlyph == "d"
+ assert isinstance(
+ colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0].LayerV1Array,
+ ot.LayerV1Array,
+ )
+ assert (
+ colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0]
+ .LayerV1Array.LayerV1Record[0]
+ .LayerGlyph
+ == "e"
+ )
+
+ def test_explicit_version_0(self):
+ colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0)
+ assert colr.version == 0
+ assert hasattr(colr, "ColorLayers")
+
+ def test_explicit_version_1(self):
+ colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=1)
+ assert colr.version == 1
+ assert not hasattr(colr, "ColorLayers")
+ assert hasattr(colr, "table")
+ assert isinstance(colr.table, ot.COLR)
+ assert colr.table.VarStore is None
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 0400210..8120d25 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -43,6 +43,7 @@
damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial
by feature lookup sub table uni0327 uni0328 e.fina
""".split()
+ glyphs.extend("cid{:05d}".format(cid) for cid in range(800, 1001 + 1))
font = TTFont()
font.setGlyphOrder(glyphs)
return font
@@ -51,7 +52,7 @@
class BuilderTest(unittest.TestCase):
# Feature files in data/*.fea; output gets compared to data/*.ttx.
TEST_FEATURE_FILES = """
- Attach enum markClass language_required
+ Attach cid_range enum markClass language_required
GlyphClassDef LigatureCaretByIndex LigatureCaretByPos
lookup lookupflag feature_aalt ignore_pos
GPOS_1 GPOS_1_zero GPOS_2 GPOS_2b GPOS_3 GPOS_4 GPOS_5 GPOS_6 GPOS_8
diff --git a/Tests/feaLib/data/ChainPosSubtable.fea b/Tests/feaLib/data/ChainPosSubtable.fea
index 4650622..e64f886 100644
--- a/Tests/feaLib/data/ChainPosSubtable.fea
+++ b/Tests/feaLib/data/ChainPosSubtable.fea
@@ -1,7 +1,7 @@
feature test {
- pos X [A-B]' -40 B' -40 A' -40 Y;
+ pos X [A - B]' -40 B' -40 A' -40 Y;
subtable;
pos X A' -111 Y;
subtable;
- pos X B' -40 A' -111 [A-C]' -40 Y;
+ pos X B' -40 A' -111 [A - C]' -40 Y;
} test;
diff --git a/Tests/feaLib/data/GSUB_6.fea b/Tests/feaLib/data/GSUB_6.fea
index 82fdac2..2230670 100644
--- a/Tests/feaLib/data/GSUB_6.fea
+++ b/Tests/feaLib/data/GSUB_6.fea
@@ -1,10 +1,10 @@
lookup ChainedSingleSubst {
sub [one two] three A' by A.sc;
- sub [B-D]' seven [eight nine] by [B.sc-D.sc];
+ sub [B - D]' seven [eight nine] by [B.sc - D.sc];
} ChainedSingleSubst;
lookup ChainedMultipleSubst {
- sub [A-C a-c] [D d] E c_t' V [W w] [X-Z x-z] by c t;
+ sub [A - C a - c] [D d] E c_t' V [W w] [X - Z x - z] by c t;
} ChainedMultipleSubst;
lookup ChainedAlternateSubst {
diff --git a/Tests/feaLib/data/GSUB_8.fea b/Tests/feaLib/data/GSUB_8.fea
index 62c7bee..8f41749 100644
--- a/Tests/feaLib/data/GSUB_8.fea
+++ b/Tests/feaLib/data/GSUB_8.fea
@@ -2,7 +2,7 @@
feature test {
rsub [a A] [b B] [c C] q' [d D] [e E] [f F] by Q;
- rsub [a A] [b B] [c C] [s-z]' [d D] [e E] [f F] by [S-Z];
+ rsub [a A] [b B] [c C] [s - z]' [d D] [e E] [f F] by [S - Z];
# Having no context for a reverse chaining substitution rule
# is a little degenerate (we define a chain without linking it
diff --git a/Tests/feaLib/data/GSUB_error.fea b/Tests/feaLib/data/GSUB_error.fea
new file mode 100644
index 0000000..ae175c5
--- /dev/null
+++ b/Tests/feaLib/data/GSUB_error.fea
@@ -0,0 +1,13 @@
+# Trigger a parser error in the function parse_substitute_ in order to improve the error message.
+# Note that this is not a valid substitution, this test is made to trigger an error.
+
+languagesystem latn dflt;
+
+@base = [a e];
+@accents = [acute grave];
+
+feature abvs {
+ lookup lookup1 {
+ sub @base @accents by @base;
+ } lookup1;
+} abvs;
diff --git a/Tests/feaLib/data/bug514.fea b/Tests/feaLib/data/bug514.fea
index 26da865..1ef5af6 100644
--- a/Tests/feaLib/data/bug514.fea
+++ b/Tests/feaLib/data/bug514.fea
@@ -5,7 +5,7 @@
# makeotf produces {A:-40, B:-40, C:-40} and {A:-111, B:-40} which
# is redundant. https://github.com/adobe-type-tools/afdko/issues/169
feature test {
- pos X [A-B]' -40 B' -40 A' -40 Y;
+ pos X [A - B]' -40 B' -40 A' -40 Y;
pos X A' -111 Y;
- pos X B' -40 A' -111 [A-C]' -40 Y;
+ pos X B' -40 A' -111 [A - C]' -40 Y;
} test;
diff --git a/Tests/feaLib/data/cid_range.fea b/Tests/feaLib/data/cid_range.fea
new file mode 100644
index 0000000..7a17aed
--- /dev/null
+++ b/Tests/feaLib/data/cid_range.fea
@@ -0,0 +1,6 @@
+# A CID range can be valid even if it is invalid as a glyph name range.
+# For example, [cid00800 - cid01001] is invalid.
+
+feature zero {
+ sub [\800 - \1001] by zero;
+} zero;
diff --git a/Tests/feaLib/data/cid_range.ttx b/Tests/feaLib/data/cid_range.ttx
new file mode 100644
index 0000000..48b502b
--- /dev/null
+++ b/Tests/feaLib/data/cid_range.ttx
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="zero"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="cid00800" out="zero"/>
+ <Substitution in="cid00801" out="zero"/>
+ <Substitution in="cid00802" out="zero"/>
+ <Substitution in="cid00803" out="zero"/>
+ <Substitution in="cid00804" out="zero"/>
+ <Substitution in="cid00805" out="zero"/>
+ <Substitution in="cid00806" out="zero"/>
+ <Substitution in="cid00807" out="zero"/>
+ <Substitution in="cid00808" out="zero"/>
+ <Substitution in="cid00809" out="zero"/>
+ <Substitution in="cid00810" out="zero"/>
+ <Substitution in="cid00811" out="zero"/>
+ <Substitution in="cid00812" out="zero"/>
+ <Substitution in="cid00813" out="zero"/>
+ <Substitution in="cid00814" out="zero"/>
+ <Substitution in="cid00815" out="zero"/>
+ <Substitution in="cid00816" out="zero"/>
+ <Substitution in="cid00817" out="zero"/>
+ <Substitution in="cid00818" out="zero"/>
+ <Substitution in="cid00819" out="zero"/>
+ <Substitution in="cid00820" out="zero"/>
+ <Substitution in="cid00821" out="zero"/>
+ <Substitution in="cid00822" out="zero"/>
+ <Substitution in="cid00823" out="zero"/>
+ <Substitution in="cid00824" out="zero"/>
+ <Substitution in="cid00825" out="zero"/>
+ <Substitution in="cid00826" out="zero"/>
+ <Substitution in="cid00827" out="zero"/>
+ <Substitution in="cid00828" out="zero"/>
+ <Substitution in="cid00829" out="zero"/>
+ <Substitution in="cid00830" out="zero"/>
+ <Substitution in="cid00831" out="zero"/>
+ <Substitution in="cid00832" out="zero"/>
+ <Substitution in="cid00833" out="zero"/>
+ <Substitution in="cid00834" out="zero"/>
+ <Substitution in="cid00835" out="zero"/>
+ <Substitution in="cid00836" out="zero"/>
+ <Substitution in="cid00837" out="zero"/>
+ <Substitution in="cid00838" out="zero"/>
+ <Substitution in="cid00839" out="zero"/>
+ <Substitution in="cid00840" out="zero"/>
+ <Substitution in="cid00841" out="zero"/>
+ <Substitution in="cid00842" out="zero"/>
+ <Substitution in="cid00843" out="zero"/>
+ <Substitution in="cid00844" out="zero"/>
+ <Substitution in="cid00845" out="zero"/>
+ <Substitution in="cid00846" out="zero"/>
+ <Substitution in="cid00847" out="zero"/>
+ <Substitution in="cid00848" out="zero"/>
+ <Substitution in="cid00849" out="zero"/>
+ <Substitution in="cid00850" out="zero"/>
+ <Substitution in="cid00851" out="zero"/>
+ <Substitution in="cid00852" out="zero"/>
+ <Substitution in="cid00853" out="zero"/>
+ <Substitution in="cid00854" out="zero"/>
+ <Substitution in="cid00855" out="zero"/>
+ <Substitution in="cid00856" out="zero"/>
+ <Substitution in="cid00857" out="zero"/>
+ <Substitution in="cid00858" out="zero"/>
+ <Substitution in="cid00859" out="zero"/>
+ <Substitution in="cid00860" out="zero"/>
+ <Substitution in="cid00861" out="zero"/>
+ <Substitution in="cid00862" out="zero"/>
+ <Substitution in="cid00863" out="zero"/>
+ <Substitution in="cid00864" out="zero"/>
+ <Substitution in="cid00865" out="zero"/>
+ <Substitution in="cid00866" out="zero"/>
+ <Substitution in="cid00867" out="zero"/>
+ <Substitution in="cid00868" out="zero"/>
+ <Substitution in="cid00869" out="zero"/>
+ <Substitution in="cid00870" out="zero"/>
+ <Substitution in="cid00871" out="zero"/>
+ <Substitution in="cid00872" out="zero"/>
+ <Substitution in="cid00873" out="zero"/>
+ <Substitution in="cid00874" out="zero"/>
+ <Substitution in="cid00875" out="zero"/>
+ <Substitution in="cid00876" out="zero"/>
+ <Substitution in="cid00877" out="zero"/>
+ <Substitution in="cid00878" out="zero"/>
+ <Substitution in="cid00879" out="zero"/>
+ <Substitution in="cid00880" out="zero"/>
+ <Substitution in="cid00881" out="zero"/>
+ <Substitution in="cid00882" out="zero"/>
+ <Substitution in="cid00883" out="zero"/>
+ <Substitution in="cid00884" out="zero"/>
+ <Substitution in="cid00885" out="zero"/>
+ <Substitution in="cid00886" out="zero"/>
+ <Substitution in="cid00887" out="zero"/>
+ <Substitution in="cid00888" out="zero"/>
+ <Substitution in="cid00889" out="zero"/>
+ <Substitution in="cid00890" out="zero"/>
+ <Substitution in="cid00891" out="zero"/>
+ <Substitution in="cid00892" out="zero"/>
+ <Substitution in="cid00893" out="zero"/>
+ <Substitution in="cid00894" out="zero"/>
+ <Substitution in="cid00895" out="zero"/>
+ <Substitution in="cid00896" out="zero"/>
+ <Substitution in="cid00897" out="zero"/>
+ <Substitution in="cid00898" out="zero"/>
+ <Substitution in="cid00899" out="zero"/>
+ <Substitution in="cid00900" out="zero"/>
+ <Substitution in="cid00901" out="zero"/>
+ <Substitution in="cid00902" out="zero"/>
+ <Substitution in="cid00903" out="zero"/>
+ <Substitution in="cid00904" out="zero"/>
+ <Substitution in="cid00905" out="zero"/>
+ <Substitution in="cid00906" out="zero"/>
+ <Substitution in="cid00907" out="zero"/>
+ <Substitution in="cid00908" out="zero"/>
+ <Substitution in="cid00909" out="zero"/>
+ <Substitution in="cid00910" out="zero"/>
+ <Substitution in="cid00911" out="zero"/>
+ <Substitution in="cid00912" out="zero"/>
+ <Substitution in="cid00913" out="zero"/>
+ <Substitution in="cid00914" out="zero"/>
+ <Substitution in="cid00915" out="zero"/>
+ <Substitution in="cid00916" out="zero"/>
+ <Substitution in="cid00917" out="zero"/>
+ <Substitution in="cid00918" out="zero"/>
+ <Substitution in="cid00919" out="zero"/>
+ <Substitution in="cid00920" out="zero"/>
+ <Substitution in="cid00921" out="zero"/>
+ <Substitution in="cid00922" out="zero"/>
+ <Substitution in="cid00923" out="zero"/>
+ <Substitution in="cid00924" out="zero"/>
+ <Substitution in="cid00925" out="zero"/>
+ <Substitution in="cid00926" out="zero"/>
+ <Substitution in="cid00927" out="zero"/>
+ <Substitution in="cid00928" out="zero"/>
+ <Substitution in="cid00929" out="zero"/>
+ <Substitution in="cid00930" out="zero"/>
+ <Substitution in="cid00931" out="zero"/>
+ <Substitution in="cid00932" out="zero"/>
+ <Substitution in="cid00933" out="zero"/>
+ <Substitution in="cid00934" out="zero"/>
+ <Substitution in="cid00935" out="zero"/>
+ <Substitution in="cid00936" out="zero"/>
+ <Substitution in="cid00937" out="zero"/>
+ <Substitution in="cid00938" out="zero"/>
+ <Substitution in="cid00939" out="zero"/>
+ <Substitution in="cid00940" out="zero"/>
+ <Substitution in="cid00941" out="zero"/>
+ <Substitution in="cid00942" out="zero"/>
+ <Substitution in="cid00943" out="zero"/>
+ <Substitution in="cid00944" out="zero"/>
+ <Substitution in="cid00945" out="zero"/>
+ <Substitution in="cid00946" out="zero"/>
+ <Substitution in="cid00947" out="zero"/>
+ <Substitution in="cid00948" out="zero"/>
+ <Substitution in="cid00949" out="zero"/>
+ <Substitution in="cid00950" out="zero"/>
+ <Substitution in="cid00951" out="zero"/>
+ <Substitution in="cid00952" out="zero"/>
+ <Substitution in="cid00953" out="zero"/>
+ <Substitution in="cid00954" out="zero"/>
+ <Substitution in="cid00955" out="zero"/>
+ <Substitution in="cid00956" out="zero"/>
+ <Substitution in="cid00957" out="zero"/>
+ <Substitution in="cid00958" out="zero"/>
+ <Substitution in="cid00959" out="zero"/>
+ <Substitution in="cid00960" out="zero"/>
+ <Substitution in="cid00961" out="zero"/>
+ <Substitution in="cid00962" out="zero"/>
+ <Substitution in="cid00963" out="zero"/>
+ <Substitution in="cid00964" out="zero"/>
+ <Substitution in="cid00965" out="zero"/>
+ <Substitution in="cid00966" out="zero"/>
+ <Substitution in="cid00967" out="zero"/>
+ <Substitution in="cid00968" out="zero"/>
+ <Substitution in="cid00969" out="zero"/>
+ <Substitution in="cid00970" out="zero"/>
+ <Substitution in="cid00971" out="zero"/>
+ <Substitution in="cid00972" out="zero"/>
+ <Substitution in="cid00973" out="zero"/>
+ <Substitution in="cid00974" out="zero"/>
+ <Substitution in="cid00975" out="zero"/>
+ <Substitution in="cid00976" out="zero"/>
+ <Substitution in="cid00977" out="zero"/>
+ <Substitution in="cid00978" out="zero"/>
+ <Substitution in="cid00979" out="zero"/>
+ <Substitution in="cid00980" out="zero"/>
+ <Substitution in="cid00981" out="zero"/>
+ <Substitution in="cid00982" out="zero"/>
+ <Substitution in="cid00983" out="zero"/>
+ <Substitution in="cid00984" out="zero"/>
+ <Substitution in="cid00985" out="zero"/>
+ <Substitution in="cid00986" out="zero"/>
+ <Substitution in="cid00987" out="zero"/>
+ <Substitution in="cid00988" out="zero"/>
+ <Substitution in="cid00989" out="zero"/>
+ <Substitution in="cid00990" out="zero"/>
+ <Substitution in="cid00991" out="zero"/>
+ <Substitution in="cid00992" out="zero"/>
+ <Substitution in="cid00993" out="zero"/>
+ <Substitution in="cid00994" out="zero"/>
+ <Substitution in="cid00995" out="zero"/>
+ <Substitution in="cid00996" out="zero"/>
+ <Substitution in="cid00997" out="zero"/>
+ <Substitution in="cid00998" out="zero"/>
+ <Substitution in="cid00999" out="zero"/>
+ <Substitution in="cid01000" out="zero"/>
+ <Substitution in="cid01001" out="zero"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/feaLib/data/language_required.fea b/Tests/feaLib/data/language_required.fea
index 4005a78..687c48a 100644
--- a/Tests/feaLib/data/language_required.fea
+++ b/Tests/feaLib/data/language_required.fea
@@ -18,5 +18,5 @@
} liga;
feature scmp {
- sub [a-z] by [A.sc-Z.sc];
+ sub [a - z] by [A.sc - Z.sc];
} scmp;
diff --git a/Tests/feaLib/data/spec4h1.fea b/Tests/feaLib/data/spec4h1.fea
index b43e13b..a3d2494 100644
--- a/Tests/feaLib/data/spec4h1.fea
+++ b/Tests/feaLib/data/spec4h1.fea
@@ -8,7 +8,7 @@
languagesystem cyrl dflt;
feature smcp {
- sub [a-z] by [A.sc-Z.sc];
+ sub [a - z] by [A.sc - Z.sc];
# Since all the rules in this feature are of the same type, they
# will be grouped in a single lookup. Since no script or language
diff --git a/Tests/feaLib/data/spec5f_ii_2.fea b/Tests/feaLib/data/spec5f_ii_2.fea
index b20a74c..916f797 100644
--- a/Tests/feaLib/data/spec5f_ii_2.fea
+++ b/Tests/feaLib/data/spec5f_ii_2.fea
@@ -3,7 +3,7 @@
# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
feature test {
- @LETTER = [a-z];
+ @LETTER = [a - z];
ignore sub @LETTER f' i';
sub f' i' by f_i.begin;
} test;
diff --git a/Tests/feaLib/data/spec5f_ii_3.fea b/Tests/feaLib/data/spec5f_ii_3.fea
index 5fd1991..af06770 100644
--- a/Tests/feaLib/data/spec5f_ii_3.fea
+++ b/Tests/feaLib/data/spec5f_ii_3.fea
@@ -3,7 +3,7 @@
# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
feature test {
- @LETTER = [a-z];
+ @LETTER = [a - z];
ignore sub @LETTER a' n' d', a' n' d' @LETTER;
sub a' n' d' by a_n_d;
} test;
diff --git a/Tests/feaLib/data/spec5f_ii_4.fea b/Tests/feaLib/data/spec5f_ii_4.fea
index 731a1f6..bc6fda4 100644
--- a/Tests/feaLib/data/spec5f_ii_4.fea
+++ b/Tests/feaLib/data/spec5f_ii_4.fea
@@ -2,13 +2,13 @@
# "Specifying exceptions to the Chain Sub rule"
# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
-@LETTER = [A-Z a-z];
+@LETTER = [A - Z a - z];
feature cswh {
# --- Glyph classes used in this feature:
- @BEGINNINGS = [A-N P-Z T_h m];
- @BEGINNINGS_SWASH = [A.swash-N.swash P.swash-Z.swash T_h.swash m.begin];
+ @BEGINNINGS = [A - N P - Z T_h m];
+ @BEGINNINGS_SWASH = [A.swash - N.swash P.swash - Z.swash T_h.swash m.begin];
@ENDINGS = [a e z];
@ENDINGS_SWASH = [a.end e.end z.end];
diff --git a/Tests/feaLib/data/spec5fi3.fea b/Tests/feaLib/data/spec5fi3.fea
index e47a6f8..e44a732 100644
--- a/Tests/feaLib/data/spec5fi3.fea
+++ b/Tests/feaLib/data/spec5fi3.fea
@@ -5,5 +5,5 @@
languagesystem latn dflt;
feature test {
- sub [A-Z] [A.sc-Z.sc]' by [a-z];
+ sub [A - Z] [A.sc - Z.sc]' by [a - z];
} test;
diff --git a/Tests/feaLib/data/spec8a.fea b/Tests/feaLib/data/spec8a.fea
index b4d7dd2..4054821 100644
--- a/Tests/feaLib/data/spec8a.fea
+++ b/Tests/feaLib/data/spec8a.fea
@@ -10,7 +10,7 @@
} aalt;
feature smcp {
- sub [a-c] by [A.sc-C.sc];
+ sub [a - c] by [A.sc - C.sc];
sub f i by f_i; # not considered for aalt
} smcp;
diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py
index cb4e689..d05a824 100644
--- a/Tests/feaLib/parser_test.py
+++ b/Tests/feaLib/parser_test.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
+from fontTools.misc.loggingTools import CapturingLogHandler
from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.parser import Parser, SymbolTable
-from fontTools.misc.py23 import *
+from io import StringIO
import warnings
import fontTools.feaLib.ast as ast
import os
@@ -62,7 +63,7 @@
glyphMap = {'a': 0, 'b': 1, 'c': 2}
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
- parser = Parser(UnicodeIO(), glyphMap=glyphMap)
+ parser = Parser(StringIO(), glyphMap=glyphMap)
self.assertEqual(len(w), 1)
self.assertEqual(w[-1].category, UserWarning)
@@ -71,11 +72,11 @@
self.assertRaisesRegex(
TypeError, "mutually exclusive",
- Parser, UnicodeIO(), ("a",), glyphMap={"a": 0})
+ Parser, StringIO(), ("a",), glyphMap={"a": 0})
self.assertRaisesRegex(
TypeError, "unsupported keyword argument",
- Parser, UnicodeIO(), foo="bar")
+ Parser, StringIO(), foo="bar")
def test_comments(self):
doc = self.parse(
@@ -346,6 +347,19 @@
[gc] = self.parse("@range = [A-foo.sc - C-foo.sc];", gn).statements
self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C-foo.sc"))
+ def test_glyphclass_ambiguous_dash_no_glyph_names(self):
+ # If Parser is initialized without a glyphNames parameter (or with empty one)
+ # it cannot distinguish between a glyph name containing an hyphen, or a
+ # range of glyph names; thus it will interpret them as literal glyph names
+ # while also outputting a logging warning to alert user about the ambiguity.
+ # https://github.com/fonttools/fonttools/issues/1768
+ glyphNames = ()
+ with CapturingLogHandler("fontTools.feaLib.parser", level="WARNING") as caplog:
+ [gc] = self.parse("@class = [A-foo.sc B-foo.sc C D];", glyphNames).statements
+ self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C", "D"))
+ self.assertEqual(len(caplog.records), 2)
+ caplog.assertRegex("Ambiguous glyph name that looks like a range:")
+
def test_glyphclass_glyph_name_should_win_over_range(self):
# The OpenType Feature File Specification v1.20 makes it clear
# that if a dashed name could be interpreted either as a glyph name
@@ -1502,6 +1516,13 @@
FeatureLibError,
'Expected "by", "from" or explicit lookup references',
self.parse, "feature liga {substitute f f i;} liga;")
+
+ def test_substitute_invalid_statement(self):
+ self.assertRaisesRegex(
+ FeatureLibError,
+ "Invalid substitution statement",
+ Parser(self.getpath("GSUB_error.fea"), GLYPHNAMES).parse
+ )
def test_subtable(self):
doc = self.parse("feature test {subtable;} test;")
@@ -1720,7 +1741,7 @@
self.assertEqual(doc.statements[0].statements, [])
def parse(self, text, glyphNames=GLYPHNAMES, followIncludes=True):
- featurefile = UnicodeIO(text)
+ featurefile = StringIO(text)
p = Parser(featurefile, glyphNames, followIncludes=followIncludes)
return p.parse()
diff --git a/Tests/ttLib/tables/C_O_L_R_test.py b/Tests/ttLib/tables/C_O_L_R_test.py
new file mode 100644
index 0000000..52346e1
--- /dev/null
+++ b/Tests/ttLib/tables/C_O_L_R_test.py
@@ -0,0 +1,313 @@
+from fontTools import ttLib
+from fontTools.misc.testTools import getXML, parseXML
+from fontTools.ttLib.tables.C_O_L_R_ import table_C_O_L_R_
+
+import pytest
+
+
+COLR_V0_DATA = (
+ b"\x00\x00" # Version (0)
+ b"\x00\x01" # BaseGlyphRecordCount (1)
+ b"\x00\x00\x00\x0e" # Offset to BaseGlyphRecordArray
+ b"\x00\x00\x00\x14" # Offset to LayerRecordArray
+ b"\x00\x03" # LayerRecordCount (3)
+ b"\x00\x06" # BaseGlyphRecord[0].BaseGlyph (6)
+ b"\x00\x00" # BaseGlyphRecord[0].FirstLayerIndex (0)
+ b"\x00\x03" # BaseGlyphRecord[0].NumLayers (3)
+ b"\x00\x07" # LayerRecord[0].LayerGlyph (7)
+ b"\x00\x00" # LayerRecord[0].PaletteIndex (0)
+ b"\x00\x08" # LayerRecord[1].LayerGlyph (8)
+ b"\x00\x01" # LayerRecord[1].PaletteIndex (1)
+ b"\x00\t" # LayerRecord[2].LayerGlyph (9)
+ b"\x00\x02" # LayerRecord[3].PaletteIndex (2)
+)
+
+
+COLR_V0_XML = [
+ '<version value="0"/>',
+ '<ColorGlyph name="glyph00006">',
+ ' <layer colorID="0" name="glyph00007"/>',
+ ' <layer colorID="1" name="glyph00008"/>',
+ ' <layer colorID="2" name="glyph00009"/>',
+ "</ColorGlyph>",
+]
+
+
+def dump(table, ttFont=None):
+ print("\n".join(getXML(table.toXML, ttFont)))
+
+
+@pytest.fixture
+def font():
+ font = ttLib.TTFont()
+ font.setGlyphOrder(["glyph%05d" % i for i in range(30)])
+ return font
+
+
+class COLR_V0_Test(object):
+ def test_decompile_and_compile(self, font):
+ colr = table_C_O_L_R_()
+ colr.decompile(COLR_V0_DATA, font)
+ assert colr.compile(font) == COLR_V0_DATA
+
+ def test_decompile_and_dump_xml(self, font):
+ colr = table_C_O_L_R_()
+ colr.decompile(COLR_V0_DATA, font)
+
+ dump(colr, font)
+ assert getXML(colr.toXML, font) == COLR_V0_XML
+
+ def test_load_from_xml_and_compile(self, font):
+ colr = table_C_O_L_R_()
+ for name, attrs, content in parseXML(COLR_V0_XML):
+ colr.fromXML(name, attrs, content, font)
+
+ assert colr.compile(font) == COLR_V0_DATA
+
+
+COLR_V1_DATA = (
+ b"\x00\x01" # Version (1)
+ b"\x00\x01" # BaseGlyphRecordCount (1)
+ b"\x00\x00\x00\x16" # Offset to BaseGlyphRecordArray
+ b"\x00\x00\x00\x1c" # Offset to LayerRecordArray
+ b"\x00\x03" # LayerRecordCount (3)
+ b"\x00\x00\x00(" # Offset to BaseGlyphV1Array
+ b"\x00\x00\x00\x00" # Offset to VarStore (NULL)
+ b"\x00\x06" # BaseGlyphRecord[0].BaseGlyph (6)
+ b"\x00\x00" # BaseGlyphRecord[0].FirstLayerIndex (0)
+ b"\x00\x03" # BaseGlyphRecord[0].NumLayers (3)
+ b"\x00\x07" # LayerRecord[0].LayerGlyph (7)
+ b"\x00\x00" # LayerRecord[0].PaletteIndex (0)
+ b"\x00\x08" # LayerRecord[1].LayerGlyph (8)
+ b"\x00\x01" # LayerRecord[1].PaletteIndex (1)
+ b"\x00\t" # LayerRecord[2].LayerGlyph (9)
+ b"\x00\x02" # LayerRecord[3].PaletteIndex (2)
+ b"\x00\x00\x00\x01" # BaseGlyphV1Array.BaseGlyphCount (1)
+ b"\x00\n" # BaseGlyphV1Array.BaseGlyphV1Record[0].BaseGlyph (10)
+ b"\x00\x00\x00\n" # Offset to LayerV1Array
+ b"\x00\x00\x00\x03" # LayerV1Array.LayerCount (3)
+ b"\x00\x0b" # LayerV1Array.LayerV1Record[0].LayerGlyph (11)
+ b"\x00\x00\x00\x16" # Offset to Paint
+ b"\x00\x0c" # LayerV1Array.LayerV1Record[1].LayerGlyph (12)
+ b"\x00\x00\x00 " # Offset to Paint
+ b"\x00\r" # LayerV1Array.LayerV1Record[2].LayerGlyph (13)
+ b"\x00\x00\x00x" # Offset to Paint
+ b"\x00\x01" # Paint.Format (1)
+ b"\x00\x02" # Paint.Color.PaletteIndex (2)
+ b" \x00" # Paint.Color.Transparency.value (0.5)
+ b"\x00\x00\x00\x00" # Paint.Color.Transparency.varIdx (0)
+ b"\x00\x02" # Paint.Format (2)
+ b"\x00\x00\x00*" # Offset to ColorLine
+ b"\x00\x01" # Paint.p0.x.value (1)
+ b"\x00\x00\x00\x00" # Paint.p0.x.varIdx (0)
+ b"\x00\x02" # Paint.p0.y.value (2)
+ b"\x00\x00\x00\x00" # Paint.p0.y.varIdx (0)
+ b"\xff\xfd" # Paint.p1.x.value (-3)
+ b"\x00\x00\x00\x00" # Paint.p1.x.varIdx (0)
+ b"\xff\xfc" # Paint.p1.y.value (-4)
+ b"\x00\x00\x00\x00" # Paint.p1.y.varIdx (0)
+ b"\x00\x05" # Paint.p2.x.value (5)
+ b"\x00\x00\x00\x00" # Paint.p2.y.varIdx (0)
+ b"\x00\x06" # Paint.p2.y.value (5)
+ b"\x00\x00\x00\x00" # Paint.p2.y.varIdx (0)
+ b"\x00\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)
+ b"\x00\x03" # ColorLine.ColorStop[0].Color.PaletteIndex (3)
+ b"\x00\x00" # ColorLine.ColorStop[0].Color.Transparency.value (0.0)
+ b"\x00\x00\x00\x00" # ColorLine.ColorStop[0].Color.Transparency.varIdx (0)
+ b" \x00" # ColorLine.ColorStop[1].StopOffset.value (0.5)
+ b"\x00\x00\x00\x00" # ColorLine.ColorStop[1].StopOffset.varIdx (0)
+ b"\x00\x04" # ColorLine.ColorStop[1].Color.PaletteIndex (4)
+ b"\x00\x00" # ColorLine.ColorStop[1].Color.Transparency.value (0.0)
+ b"\x00\x00\x00\x00" # ColorLine.ColorStop[1].Color.Transparency.varIdx (0)
+ b"@\x00" # ColorLine.ColorStop[2].StopOffset.value (1.0)
+ b"\x00\x00\x00\x00" # ColorLine.ColorStop[2].StopOffset.varIdx (0)
+ b"\x00\x05" # ColorLine.ColorStop[2].Color.PaletteIndex (5)
+ b"\x00\x00" # ColorLine.ColorStop[2].Color.Transparency.value (0.0)
+ b"\x00\x00\x00\x00" # ColorLine.ColorStop[2].Color.Transparency.varIdx (0)
+ b"\x00\x03" # Paint.Format (3)
+ b"\x00\x00\x00." # Offset to ColorLine
+ b"\x00\x07" # Paint.c0.x.value (7)
+ b"\x00\x00\x00\x00"
+ b"\x00\x08" # Paint.c0.y.value (8)
+ b"\x00\x00\x00\x00"
+ b"\x00\t" # Paint.c1.x.value (9)
+ b"\x00\x00\x00\x00"
+ b"\x00\n" # Paint.c1.y.value (10)
+ b"\x00\x00\x00\x00"
+ b"\x00\x0b" # Paint.r0.value (11)
+ b"\x00\x00\x00\x00"
+ b"\x00\x0c" # Paint.r1.value (12)
+ b"\x00\x00\x00\x00"
+ b"\x00\x00\x00N" # Offset to Affine2x2
+ b"\x00\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\x00"
+ 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.Transparency.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"
+)
+
+
+COLR_V1_XML = [
+ '<Version value="1"/>',
+ "<!-- BaseGlyphRecordCount=1 -->",
+ "<BaseGlyphRecordArray>",
+ ' <BaseGlyphRecord index="0">',
+ ' <BaseGlyph value="glyph00006"/>',
+ ' <FirstLayerIndex value="0"/>',
+ ' <NumLayers value="3"/>',
+ " </BaseGlyphRecord>",
+ "</BaseGlyphRecordArray>",
+ "<LayerRecordArray>",
+ ' <LayerRecord index="0">',
+ ' <LayerGlyph value="glyph00007"/>',
+ ' <PaletteIndex value="0"/>',
+ " </LayerRecord>",
+ ' <LayerRecord index="1">',
+ ' <LayerGlyph value="glyph00008"/>',
+ ' <PaletteIndex value="1"/>',
+ " </LayerRecord>",
+ ' <LayerRecord index="2">',
+ ' <LayerGlyph value="glyph00009"/>',
+ ' <PaletteIndex value="2"/>',
+ " </LayerRecord>",
+ "</LayerRecordArray>",
+ "<!-- LayerRecordCount=3 -->",
+ "<BaseGlyphV1Array>",
+ " <!-- BaseGlyphCount=1 -->",
+ ' <BaseGlyphV1Record index="0">',
+ ' <BaseGlyph value="glyph00010"/>',
+ " <LayerV1Array>",
+ " <!-- LayerCount=3 -->",
+ ' <LayerV1Record index="0">',
+ ' <LayerGlyph value="glyph00011"/>',
+ ' <Paint Format="1">',
+ " <Color>",
+ ' <PaletteIndex value="2"/>',
+ ' <Transparency 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"/>',
+ ' <Transparency value="0.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="0.5"/>',
+ " <Color>",
+ ' <PaletteIndex value="4"/>',
+ ' <Transparency value="0.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ ' <ColorStop index="2">',
+ ' <StopOffset value="1.0"/>',
+ " <Color>",
+ ' <PaletteIndex value="5"/>',
+ ' <Transparency value="0.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ " </ColorLine>",
+ " <p0>",
+ ' <x value="1"/>',
+ ' <y value="2"/>',
+ " </p0>",
+ " <p1>",
+ ' <x value="-3"/>',
+ ' <y value="-4"/>',
+ " </p1>",
+ " <p2>",
+ ' <x value="5"/>',
+ ' <y value="6"/>',
+ " </p2>",
+ " </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"/>',
+ ' <Transparency value="0.0"/>',
+ " </Color>",
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ " <Color>",
+ ' <PaletteIndex value="7"/>',
+ ' <Transparency value="0.4"/>',
+ " </Color>",
+ " </ColorStop>",
+ " </ColorLine>",
+ " <c0>",
+ ' <x value="7"/>',
+ ' <y value="8"/>',
+ " </c0>",
+ " <c1>",
+ ' <x value="9"/>',
+ ' <y value="10"/>',
+ " </c1>",
+ ' <r0 value="11"/>',
+ ' <r1 value="12"/>',
+ " <Affine>",
+ ' <xx value="-13.0"/>',
+ ' <xy value="14.0"/>',
+ ' <yx value="15.0"/>',
+ ' <yy value="-17.0"/>',
+ " </Affine>",
+ " </Paint>",
+ " </LayerV1Record>",
+ " </LayerV1Array>",
+ " </BaseGlyphV1Record>",
+ "</BaseGlyphV1Array>",
+]
+
+
+class COLR_V1_Test(object):
+ def test_decompile_and_compile(self, font):
+ colr = table_C_O_L_R_()
+ colr.decompile(COLR_V1_DATA, font)
+ assert colr.compile(font) == COLR_V1_DATA
+
+ def test_decompile_and_dump_xml(self, font):
+ colr = table_C_O_L_R_()
+ colr.decompile(COLR_V1_DATA, font)
+
+ dump(colr, font)
+ assert getXML(colr.toXML, font) == COLR_V1_XML
+
+ def test_load_from_xml_and_compile(self, font):
+ colr = table_C_O_L_R_()
+ for name, attrs, content in parseXML(COLR_V1_XML):
+ colr.fromXML(name, attrs, content, font)
+
+ assert colr.compile(font) == COLR_V1_DATA
diff --git a/setup.cfg b/setup.cfg
index 000db1f..51f7fac 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.7.0
+current_version = 4.8.0
commit = True
tag = False
tag_name = {new_version}
diff --git a/setup.py b/setup.py
index f9ad12a..20fee0b 100755
--- a/setup.py
+++ b/setup.py
@@ -437,7 +437,7 @@
setup_params = dict(
name="fonttools",
- version="4.7.0",
+ version="4.8.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",