aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2021-01-25 19:23:39 -0800
committerHaibo Huang <hhb@google.com>2021-01-26 12:43:36 -0800
commitf8d82f7a26154921102824802b7c16b9ac722f85 (patch)
tree7e436bb43355218e783d501f0bd394bb9a19bc24
parente7b287f76cb1898590bcccd533663f6e5830b24b (diff)
downloadfonttools-f8d82f7a26154921102824802b7c16b9ac722f85.tar.gz
Upgrade fonttools to 4.19.0
Test: make Change-Id: I94aa7732ede5deb42a281dd99302c2e3140af4a4
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/colorLib/builder.py75
-rw-r--r--Lib/fontTools/colorLib/geometry.py145
-rw-r--r--Lib/fontTools/encodings/codecs.py42
-rw-r--r--Lib/fontTools/otlLib/builder.py3
-rw-r--r--Lib/fontTools/pens/basePen.py12
-rw-r--r--Lib/fontTools/pens/hashPointPen.py6
-rw-r--r--Lib/fontTools/subset/__init__.py36
-rw-r--r--Lib/fontTools/varLib/featureVars.py7
-rw-r--r--METADATA10
-rw-r--r--NEWS.rst16
-rw-r--r--Tests/colorLib/builder_test.py167
-rw-r--r--Tests/otlLib/builder_test.py8
-rw-r--r--Tests/subset/data/CmapSubsetTest.subset.ttx14
-rw-r--r--Tests/subset/data/CmapSubsetTest.ttx225
-rw-r--r--Tests/subset/data/TestContextSubstFormat3.ttx8
-rw-r--r--Tests/subset/subset_test.py7
-rw-r--r--Tests/ttLib/tables/_n_a_m_e_test.py12
-rw-r--r--Tests/ttLib/woff2_test.py2
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py2
21 files changed, 740 insertions, 61 deletions
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 09dce766..cc7d69f9 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -4,6 +4,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "4.18.2"
+version = __version__ = "4.19.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py
index 724136ab..998ab60d 100644
--- a/Lib/fontTools/colorLib/builder.py
+++ b/Lib/fontTools/colorLib/builder.py
@@ -6,6 +6,7 @@ import collections
import copy
import enum
from functools import partial
+from math import ceil, log
from typing import (
Any,
Dict,
@@ -34,6 +35,7 @@ from fontTools.ttLib.tables.otTables import (
VariableInt,
)
from .errors import ColorLibError
+from .geometry import round_start_circle_stable_containment
# TODO move type aliases to colorLib.types?
@@ -328,9 +330,9 @@ def _split_color_glyphs_by_version(
def _to_variable_value(
value: _ScalarInput,
- minValue: _Number,
- maxValue: _Number,
- cls: Type[VariableValue],
+ cls: Type[VariableValue] = VariableFloat,
+ minValue: Optional[_Number] = None,
+ maxValue: Optional[_Number] = None,
) -> VariableValue:
if not isinstance(value, cls):
try:
@@ -339,9 +341,9 @@ def _to_variable_value(
value = cls(value)
else:
value = cls._make(it)
- if value.value < minValue:
+ if minValue is not None and value.value < minValue:
raise OverflowError(f"{cls.__name__}: {value.value} < {minValue}")
- if value.value > maxValue:
+ if maxValue is not None and value.value > maxValue:
raise OverflowError(f"{cls.__name__}: {value.value} < {maxValue}")
return value
@@ -526,7 +528,21 @@ class LayerV1ListBuilder:
ot_paint.Format = int(ot.Paint.Format.PaintRadialGradient)
ot_paint.ColorLine = _to_color_line(colorLine)
- for i, (x, y), r in [(0, c0, r0), (1, c1, r1)]:
+ # normalize input types (which may or may not specify a varIdx)
+ x0, y0 = _to_variable_value(c0[0]), _to_variable_value(c0[1])
+ r0 = _to_variable_value(r0)
+ x1, y1 = _to_variable_value(c1[0]), _to_variable_value(c1[1])
+ r1 = _to_variable_value(r1)
+
+ # avoid abrupt change after rounding when c0 is near c1's perimeter
+ c = round_start_circle_stable_containment(
+ (x0.value, y0.value), r0.value, (x1.value, y1.value), r1.value
+ )
+ x0, y0 = x0._replace(value=c.centre[0]), y0._replace(value=c.centre[1])
+ r0 = r0._replace(value=c.radius)
+
+ for i, (x, y, r) in enumerate(((x0, y0, r0), (x1, y1, r1))):
+ # rounding happens here as floats are converted to integers
setattr(ot_paint, f"x{i}", _to_variable_int16(x))
setattr(ot_paint, f"y{i}", _to_variable_int16(y))
setattr(ot_paint, f"r{i}", _to_variable_uint16(r))
@@ -617,7 +633,10 @@ class LayerV1ListBuilder:
ot_paint.Format = int(ot.Paint.Format.PaintColrLayers)
self.slices.append(ot_paint)
- paints = [self.buildPaint(p) for p in paints]
+ paints = [
+ self.buildPaint(p)
+ for p in _build_n_ary_tree(paints, n=MAX_PAINT_COLR_LAYER_COUNT)
+ ]
# Look for reuse, with preference to longer sequences
found_reuse = True
@@ -761,3 +780,45 @@ def buildColrV1(
glyphs.BaseGlyphCount = len(baseGlyphs)
glyphs.BaseGlyphV1Record = baseGlyphs
return (layers, glyphs)
+
+
+def _build_n_ary_tree(leaves, n):
+ """Build N-ary tree from sequence of leaf nodes.
+
+ Return a list of lists where each non-leaf node is a list containing
+ max n nodes.
+ """
+ if not leaves:
+ return []
+
+ assert n > 1
+
+ depth = ceil(log(len(leaves), n))
+
+ if depth <= 1:
+ return list(leaves)
+
+ # Fully populate complete subtrees of root until we have enough leaves left
+ root = []
+ unassigned = None
+ full_step = n ** (depth - 1)
+ for i in range(0, len(leaves), full_step):
+ subtree = leaves[i : i + full_step]
+ if len(subtree) < full_step:
+ unassigned = subtree
+ break
+ while len(subtree) > n:
+ subtree = [subtree[k : k + n] for k in range(0, len(subtree), n)]
+ root.append(subtree)
+
+ if unassigned:
+ # Recurse to fill the last subtree, which is the only partially populated one
+ subtree = _build_n_ary_tree(unassigned, n)
+ if len(subtree) <= n - len(root):
+ # replace last subtree with its children if they can still fit
+ root.extend(subtree)
+ else:
+ root.append(subtree)
+ assert len(root) <= n
+
+ return root
diff --git a/Lib/fontTools/colorLib/geometry.py b/Lib/fontTools/colorLib/geometry.py
new file mode 100644
index 00000000..ec647535
--- /dev/null
+++ b/Lib/fontTools/colorLib/geometry.py
@@ -0,0 +1,145 @@
+"""Helpers for manipulating 2D points and vectors in COLR table."""
+
+from math import copysign, cos, hypot, pi
+from fontTools.misc.fixedTools import otRound
+
+
+def _vector_between(origin, target):
+ return (target[0] - origin[0], target[1] - origin[1])
+
+
+def _round_point(pt):
+ return (otRound(pt[0]), otRound(pt[1]))
+
+
+def _unit_vector(vec):
+ length = hypot(*vec)
+ if length == 0:
+ return None
+ return (vec[0] / length, vec[1] / length)
+
+
+# This is the same tolerance used by Skia's SkTwoPointConicalGradient.cpp to detect
+# when a radial gradient's focal point lies on the end circle.
+_NEARLY_ZERO = 1 / (1 << 12) # 0.000244140625
+
+
+# The unit vector's X and Y components are respectively
+# U = (cos(α), sin(α))
+# where α is the angle between the unit vector and the positive x axis.
+_UNIT_VECTOR_THRESHOLD = cos(3 / 8 * pi) # == sin(1/8 * pi) == 0.38268343236508984
+
+
+def _rounding_offset(direction):
+ # Return 2-tuple of -/+ 1.0 or 0.0 approximately based on the direction vector.
+ # We divide the unit circle in 8 equal slices oriented towards the cardinal
+ # (N, E, S, W) and intermediate (NE, SE, SW, NW) directions. To each slice we
+ # map one of the possible cases: -1, 0, +1 for either X and Y coordinate.
+ # E.g. Return (+1.0, -1.0) if unit vector is oriented towards SE, or
+ # (-1.0, 0.0) if it's pointing West, etc.
+ uv = _unit_vector(direction)
+ if not uv:
+ return (0, 0)
+
+ result = []
+ for uv_component in uv:
+ if -_UNIT_VECTOR_THRESHOLD <= uv_component < _UNIT_VECTOR_THRESHOLD:
+ # unit vector component near 0: direction almost orthogonal to the
+ # direction of the current axis, thus keep coordinate unchanged
+ result.append(0)
+ else:
+ # nudge coord by +/- 1.0 in direction of unit vector
+ result.append(copysign(1.0, uv_component))
+ return tuple(result)
+
+
+class Circle:
+ def __init__(self, centre, radius):
+ self.centre = centre
+ self.radius = radius
+
+ def __repr__(self):
+ return f"Circle(centre={self.centre}, radius={self.radius})"
+
+ def round(self):
+ return Circle(_round_point(self.centre), otRound(self.radius))
+
+ def inside(self, outer_circle):
+ dist = self.radius + hypot(*_vector_between(self.centre, outer_circle.centre))
+ return (
+ abs(outer_circle.radius - dist) <= _NEARLY_ZERO
+ or outer_circle.radius > dist
+ )
+
+ def concentric(self, other):
+ return self.centre == other.centre
+
+ def move(self, dx, dy):
+ self.centre = (self.centre[0] + dx, self.centre[1] + dy)
+
+
+def round_start_circle_stable_containment(c0, r0, c1, r1):
+ """Round start circle so that it stays inside/outside end circle after rounding.
+
+ The rounding of circle coordinates to integers may cause an abrupt change
+ if the start circle c0 is so close to the end circle c1's perimiter that
+ it ends up falling outside (or inside) as a result of the rounding.
+ To keep the gradient unchanged, we nudge it in the right direction.
+
+ See:
+ https://github.com/googlefonts/colr-gradients-spec/issues/204
+ https://github.com/googlefonts/picosvg/issues/158
+ """
+ start, end = Circle(c0, r0), Circle(c1, r1)
+
+ inside_before_round = start.inside(end)
+
+ round_start = start.round()
+ round_end = end.round()
+ inside_after_round = round_start.inside(round_end)
+
+ if inside_before_round == inside_after_round:
+ return round_start
+ elif inside_after_round:
+ # start was outside before rounding: we need to push start away from end
+ direction = _vector_between(round_end.centre, round_start.centre)
+ radius_delta = +1.0
+ else:
+ # start was inside before rounding: we need to push start towards end
+ direction = _vector_between(round_start.centre, round_end.centre)
+ radius_delta = -1.0
+ dx, dy = _rounding_offset(direction)
+
+ # At most 2 iterations ought to be enough to converge. Before the loop, we
+ # know the start circle didn't keep containment after normal rounding; thus
+ # we continue adjusting by -/+ 1.0 until containment is restored.
+ # Normal rounding can at most move each coordinates -/+0.5; in the worst case
+ # both the start and end circle's centres and radii will be rounded in opposite
+ # directions, e.g. when they move along a 45 degree diagonal:
+ # c0 = (1.5, 1.5) ===> (2.0, 2.0)
+ # r0 = 0.5 ===> 1.0
+ # c1 = (0.499, 0.499) ===> (0.0, 0.0)
+ # r1 = 2.499 ===> 2.0
+ # In this example, the relative distance between the circles, calculated
+ # as r1 - (r0 + distance(c0, c1)) is initially 0.57437 (c0 is inside c1), and
+ # -1.82842 after rounding (c0 is now outside c1). Nudging c0 by -1.0 on both
+ # x and y axes moves it towards c1 by hypot(-1.0, -1.0) = 1.41421. Two of these
+ # moves cover twice that distance, which is enough to restore containment.
+ max_attempts = 2
+ for _ in range(max_attempts):
+ if round_start.concentric(round_end):
+ # can't move c0 towards c1 (they are the same), so we change the radius
+ round_start.radius += radius_delta
+ assert round_start.radius >= 0
+ else:
+ round_start.move(dx, dy)
+ if inside_before_round == round_start.inside(round_end):
+ break
+ else: # likely a bug
+ raise AssertionError(
+ f"Rounding circle {start} "
+ f"{'inside' if inside_before_round else 'outside'} "
+ f"{end} failed after {max_attempts} attempts!"
+ )
+
+ return round_start
diff --git a/Lib/fontTools/encodings/codecs.py b/Lib/fontTools/encodings/codecs.py
index ac2b9909..c2288a77 100644
--- a/Lib/fontTools/encodings/codecs.py
+++ b/Lib/fontTools/encodings/codecs.py
@@ -16,43 +16,29 @@ class ExtendCodec(codecs.Codec):
self.info = codecs.CodecInfo(name=self.name, encode=self.encode, decode=self.decode)
codecs.register_error(name, self.error)
- def encode(self, input, errors='strict'):
- assert errors == 'strict'
- #return codecs.encode(input, self.base_encoding, self.name), len(input)
-
- # The above line could totally be all we needed, relying on the error
- # handling to replace the unencodable Unicode characters with our extended
- # byte sequences.
- #
- # However, there seems to be a design bug in Python (probably intentional):
- # the error handler for encoding is supposed to return a **Unicode** character,
- # that then needs to be encodable itself... Ugh.
- #
- # So we implement what codecs.encode() should have been doing: which is expect
- # error handler to return bytes() to be added to the output.
- #
- # This seems to have been fixed in Python 3.3. We should try using that and
- # use fallback only if that failed.
- # https://docs.python.org/3.3/library/codecs.html#codecs.register_error
-
+ def _map(self, mapper, output_type, exc_type, input, errors):
+ base_error_handler = codecs.lookup_error(errors)
length = len(input)
- out = b''
+ out = output_type()
while input:
+ # first try to use self.error as the error handler
try:
- part = codecs.encode(input, self.base_encoding)
+ part = mapper(input, self.base_encoding, errors=self.name)
out += part
- input = '' # All converted
- except UnicodeEncodeError as e:
- # Convert the correct part
- out += codecs.encode(input[:e.start], self.base_encoding)
- replacement, pos = self.error(e)
+ break # All converted
+ except exc_type as e:
+ # else convert the correct part, handle error as requested and continue
+ out += mapper(input[:e.start], self.base_encoding, self.name)
+ replacement, pos = base_error_handler(e)
out += replacement
input = input[pos:]
return out, length
+ def encode(self, input, errors='strict'):
+ return self._map(codecs.encode, bytes, UnicodeEncodeError, input, errors)
+
def decode(self, input, errors='strict'):
- assert errors == 'strict'
- return codecs.decode(input, self.base_encoding, self.name), len(input)
+ return self._map(codecs.decode, str, UnicodeDecodeError, input, errors)
def error(self, e):
if isinstance(e, UnicodeDecodeError):
diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py
index 7e144451..029aa3fc 100644
--- a/Lib/fontTools/otlLib/builder.py
+++ b/Lib/fontTools/otlLib/builder.py
@@ -2574,7 +2574,8 @@ class ClassDefBuilder(object):
return
self.classes_.add(glyphs)
for glyph in glyphs:
- assert glyph not in self.glyphs_
+ if glyph in self.glyphs_:
+ raise OpenTypeLibError(f"Glyph {glyph} is already present in class.", None)
self.glyphs_[glyph] = glyphs
def classes(self):
diff --git a/Lib/fontTools/pens/basePen.py b/Lib/fontTools/pens/basePen.py
index 1593024f..c8c4c551 100644
--- a/Lib/fontTools/pens/basePen.py
+++ b/Lib/fontTools/pens/basePen.py
@@ -147,6 +147,10 @@ class LoggingPen(LogMixin, AbstractPen):
pass
+class MissingComponentError(KeyError):
+ """Indicates a component pointing to a non-existent glyph in the glyphset."""
+
+
class DecomposingPen(LoggingPen):
""" Implements a 'addComponent' method that decomposes components
@@ -155,10 +159,12 @@ class DecomposingPen(LoggingPen):
You must override moveTo, lineTo, curveTo and qCurveTo. You may
additionally override closePath, endPath and addComponent.
+
+ By default a warning message is logged when a base glyph is missing;
+ set the class variable ``skipMissingComponents`` to False if you want
+ to raise a :class:`MissingComponentError` exception.
"""
- # By default a warning message is logged when a base glyph is missing;
- # set this to False if you want to raise a 'KeyError' exception
skipMissingComponents = True
def __init__(self, glyphSet):
@@ -176,7 +182,7 @@ class DecomposingPen(LoggingPen):
glyph = self.glyphSet[glyphName]
except KeyError:
if not self.skipMissingComponents:
- raise
+ raise MissingComponentError(glyphName)
self.log.warning(
"glyph '%s' is missing from glyphSet; skipped" % glyphName)
else:
diff --git a/Lib/fontTools/pens/hashPointPen.py b/Lib/fontTools/pens/hashPointPen.py
index f3276f70..9aef5d87 100644
--- a/Lib/fontTools/pens/hashPointPen.py
+++ b/Lib/fontTools/pens/hashPointPen.py
@@ -1,6 +1,7 @@
# Modified from https://github.com/adobe-type-tools/psautohint/blob/08b346865710ed3c172f1eb581d6ef243b203f99/python/psautohint/ufoFont.py#L800-L838
import hashlib
+from fontTools.pens.basePen import MissingComponentError
from fontTools.pens.pointPen import AbstractPointPen
@@ -69,5 +70,8 @@ class HashPointPen(AbstractPointPen):
):
tr = "".join([f"{t:+}" for t in transformation])
self.data.append("[")
- self.glyphset[baseGlyphName].drawPoints(self)
+ try:
+ self.glyphset[baseGlyphName].drawPoints(self)
+ except KeyError:
+ raise MissingComponentError(baseGlyphName)
self.data.append(f"({tr})]")
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index d78aa8a8..82605d51 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -2207,7 +2207,17 @@ def prune_pre_subset(self, font, options):
@_add_method(ttLib.getTableClass('cmap'))
def subset_glyphs(self, s):
s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only
+
+ tables_format12_bmp = []
+ table_plat0_enc3 = {} # Unicode platform, Unicode BMP only, keyed by language
+ table_plat3_enc1 = {} # Windows platform, Unicode BMP, keyed by language
+
for t in self.tables:
+ if t.platformID == 0 and t.platEncID == 3:
+ table_plat0_enc3[t.language] = t
+ if t.platformID == 3 and t.platEncID == 1:
+ table_plat3_enc1[t.language] = t
+
if t.format == 14:
# TODO(behdad) We drop all the default-UVS mappings
# for glyphs_requested. So it's the caller's responsibility to make
@@ -2219,16 +2229,38 @@ def subset_glyphs(self, s):
elif t.isUnicode():
t.cmap = {u:g for u,g in t.cmap.items()
if g in s.glyphs_requested or u in s.unicodes_requested}
+ # Collect format 12 tables that hold only basic multilingual plane
+ # codepoints.
+ if t.format == 12 and t.cmap and max(t.cmap.keys()) < 0x10000:
+ tables_format12_bmp.append(t)
else:
t.cmap = {u:g for u,g in t.cmap.items()
if g in s.glyphs_requested}
+
+ # Fomat 12 tables are redundant if they contain just the same BMP codepoints
+ # their little BMP-only encoding siblings contain.
+ for t in tables_format12_bmp:
+ if (
+ t.platformID == 0 # Unicode platform
+ and t.platEncID == 4 # Unicode full repertoire
+ and t.language in table_plat0_enc3 # Have a BMP-only sibling?
+ and table_plat0_enc3[t.language].cmap == t.cmap
+ ):
+ t.cmap.clear()
+ elif (
+ t.platformID == 3 # Windows platform
+ and t.platEncID == 10 # Unicode full repertoire
+ and t.language in table_plat3_enc1 # Have a BMP-only sibling?
+ and table_plat3_enc1[t.language].cmap == t.cmap
+ ):
+ t.cmap.clear()
+
self.tables = [t for t in self.tables
if (t.cmap if t.format != 14 else t.uvsDict)]
self.numSubTables = len(self.tables)
# TODO(behdad) Convert formats when needed.
# In particular, if we have a format=12 without non-BMP
- # characters, either drop format=12 one or convert it
- # to format=4 if there's not one.
+ # characters, convert it to format=4 if there's not one.
return True # Required table
@_add_method(ttLib.getTableClass('DSIG'))
diff --git a/Lib/fontTools/varLib/featureVars.py b/Lib/fontTools/varLib/featureVars.py
index 76e8cc4a..45f3d839 100644
--- a/Lib/fontTools/varLib/featureVars.py
+++ b/Lib/fontTools/varLib/featureVars.py
@@ -10,7 +10,7 @@ from fontTools.ttLib.tables import otTables as ot
from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable
from collections import OrderedDict
-from .errors import VarLibValidationError
+from .errors import VarLibError, VarLibValidationError
def addFeatureVariations(font, conditionalSubstitutions, featureTag='rvrn'):
@@ -298,6 +298,11 @@ def addFeatureVariationsRaw(font, conditionalSubstitutions, featureTag='rvrn'):
varFeatureIndex = gsub.FeatureList.FeatureRecord.index(varFeature)
for scriptRecord in gsub.ScriptList.ScriptRecord:
+ if scriptRecord.Script.DefaultLangSys is None:
+ raise VarLibError(
+ "Feature variations require that the script "
+ f"'{scriptRecord.ScriptTag}' defines a default language system."
+ )
langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord]
for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems:
langSys.FeatureIndex.append(varFeatureIndex)
diff --git a/METADATA b/METADATA
index 389a623d..5606bbf5 100644
--- a/METADATA
+++ b/METADATA
@@ -10,16 +10,16 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/archive/4.18.2.zip"
+ value: "https://github.com/fonttools/fonttools/archive/4.19.0.zip"
}
- version: "4.18.2"
+ version: "4.19.0"
# would be NOTICE except for:
# Doc/README.md
# or RESTRICTED except for the SIL OFL 1.1 license
license_type: BY_EXCEPTION_ONLY
last_upgrade_date {
- year: 2020
- month: 12
- day: 16
+ year: 2021
+ month: 1
+ day: 25
}
}
diff --git a/NEWS.rst b/NEWS.rst
index 55542d06..e22a0f62 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,19 @@
+4.19.0 (released 2021-01-25)
+----------------------------
+
+- [codecs] Handle ``errors`` parameter different from 'strict' for the custom
+ extended mac encodings (#2137, #2132).
+- [featureVars] Raise better error message when a script is missing the required
+ default language system (#2154).
+- [COLRv1] Avoid abrupt change caused by rounding ``PaintRadialGradient.c0`` when
+ the start circle almost touches the end circle's perimeter (#2148).
+- [COLRv1] Support building unlimited lists of paints as 255-ary trees of
+ ``PaintColrLayers`` tables (#2153).
+- [subset] Prune redundant format-12 cmap subtables when all non-BMP characters
+ are dropped (#2146).
+- [basePen] Raise ``MissingComponentError`` instead of bare ``KeyError`` when a
+ referenced component is missing (#2145).
+
4.18.2 (released 2020-12-16)
----------------------------
diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py
index d1e94df9..43ec96a4 100644
--- a/Tests/colorLib/builder_test.py
+++ b/Tests/colorLib/builder_test.py
@@ -1,7 +1,8 @@
from fontTools.ttLib import newTable
from fontTools.ttLib.tables import otTables as ot
from fontTools.colorLib import builder
-from fontTools.colorLib.builder import LayerV1ListBuilder
+from fontTools.colorLib.geometry import round_start_circle_stable_containment, Circle
+from fontTools.colorLib.builder import LayerV1ListBuilder, _build_n_ary_tree
from fontTools.colorLib.errors import ColorLibError
import pytest
from typing import List
@@ -673,6 +674,43 @@ def test_buildColrV1():
assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g"
+def test_buildColrV1_more_than_255_paints():
+ num_paints = 364
+ colorGlyphs = {
+ "a": [
+ {
+ "format": 5, # PaintGlyph
+ "paint": 0,
+ "glyph": name,
+ }
+ for name in (f"glyph{i}" for i in range(num_paints))
+ ],
+ }
+ layers, baseGlyphs = builder.buildColrV1(colorGlyphs)
+ paints = layers.Paint
+
+ assert len(paints) == num_paints + 1
+
+ assert all(paints[i].Format == ot.Paint.Format.PaintGlyph for i in range(255))
+
+ assert paints[255].Format == ot.Paint.Format.PaintColrLayers
+ assert paints[255].FirstLayerIndex == 0
+ assert paints[255].NumLayers == 255
+
+ assert all(
+ paints[i].Format == ot.Paint.Format.PaintGlyph
+ for i in range(256, num_paints + 1)
+ )
+
+ assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
+ assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a"
+ assert (
+ baseGlyphs.BaseGlyphV1Record[0].Paint.Format == ot.Paint.Format.PaintColrLayers
+ )
+ assert baseGlyphs.BaseGlyphV1Record[0].Paint.FirstLayerIndex == 255
+ assert baseGlyphs.BaseGlyphV1Record[0].Paint.NumLayers == num_paints + 1 - 255
+
+
def test_split_color_glyphs_by_version():
layerBuilder = LayerV1ListBuilder()
colorGlyphs = {
@@ -1055,3 +1093,130 @@ class BuildCOLRTest(object):
assert hasattr(colr, "table")
assert isinstance(colr.table, ot.COLR)
assert colr.table.VarStore is None
+
+
+class TrickyRadialGradientTest:
+ @staticmethod
+ def circle_inside_circle(c0, r0, c1, r1, rounded=False):
+ if rounded:
+ return Circle(c0, r0).round().inside(Circle(c1, r1).round())
+ else:
+ return Circle(c0, r0).inside(Circle(c1, r1))
+
+ def round_start_circle(self, c0, r0, c1, r1, inside=True):
+ assert self.circle_inside_circle(c0, r0, c1, r1) is inside
+ assert self.circle_inside_circle(c0, r0, c1, r1, rounded=True) is not inside
+ r = round_start_circle_stable_containment(c0, r0, c1, r1)
+ assert (
+ self.circle_inside_circle(r.centre, r.radius, c1, r1, rounded=True)
+ is inside
+ )
+ return r.centre, r.radius
+
+ def test_noto_emoji_mosquito_u1f99f(self):
+ # https://github.com/googlefonts/picosvg/issues/158
+ c0 = (385.23508, 70.56727999999998)
+ r0 = 0
+ c1 = (642.99108, 104.70327999999995)
+ r1 = 260.0072
+ assert self.round_start_circle(c0, r0, c1, r1, inside=True) == ((386, 71), 0)
+
+ @pytest.mark.parametrize(
+ "c0, r0, c1, r1, inside, expected",
+ [
+ # inside before round, outside after round
+ ((1.4, 0), 0, (2.6, 0), 1.3, True, ((2, 0), 0)),
+ ((1, 0), 0.6, (2.8, 0), 2.45, True, ((2, 0), 1)),
+ ((6.49, 6.49), 0, (0.49, 0.49), 8.49, True, ((5, 5), 0)),
+ # outside before round, inside after round
+ ((0, 0), 0, (2, 0), 1.5, False, ((-1, 0), 0)),
+ ((0, -0.5), 0, (0, -2.5), 1.5, False, ((0, 1), 0)),
+ # the following ones require two nudges to round correctly
+ ((0.5, 0), 0, (9.4, 0), 8.8, False, ((-1, 0), 0)),
+ ((1.5, 1.5), 0, (0.49, 0.49), 1.49, True, ((0, 0), 0)),
+ # limit case when circle almost exactly overlap
+ ((0.5000001, 0), 0.5000001, (0.499999, 0), 0.4999999, True, ((0, 0), 0)),
+ # concentrical circles, r0 > r1
+ ((0, 0), 1.49, (0, 0), 1, False, ((0, 0), 2)),
+ ],
+ )
+ def test_nudge_start_circle_position(self, c0, r0, c1, r1, inside, expected):
+ assert self.round_start_circle(c0, r0, c1, r1, inside) == expected
+
+
+@pytest.mark.parametrize(
+ "lst, n, expected",
+ [
+ ([0], 2, [0]),
+ ([0, 1], 2, [0, 1]),
+ ([0, 1, 2], 2, [[0, 1], 2]),
+ ([0, 1, 2], 3, [0, 1, 2]),
+ ([0, 1, 2, 3], 2, [[0, 1], [2, 3]]),
+ ([0, 1, 2, 3], 3, [[0, 1, 2], 3]),
+ ([0, 1, 2, 3, 4], 3, [[0, 1, 2], 3, 4]),
+ ([0, 1, 2, 3, 4, 5], 3, [[0, 1, 2], [3, 4, 5]]),
+ (list(range(7)), 3, [[0, 1, 2], [3, 4, 5], 6]),
+ (list(range(8)), 3, [[0, 1, 2], [3, 4, 5], [6, 7]]),
+ (list(range(9)), 3, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]),
+ (list(range(10)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9]),
+ (list(range(11)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9, 10]),
+ (list(range(12)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11]]),
+ (list(range(13)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], 12]),
+ (
+ list(range(14)),
+ 3,
+ [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], 12, 13]],
+ ),
+ (
+ list(range(15)),
+ 3,
+ [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], [12, 13, 14]],
+ ),
+ (
+ list(range(16)),
+ 3,
+ [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], [12, 13, 14], 15]],
+ ),
+ (
+ list(range(23)),
+ 3,
+ [
+ [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
+ [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
+ [[18, 19, 20], 21, 22],
+ ],
+ ),
+ (
+ list(range(27)),
+ 3,
+ [
+ [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
+ [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
+ [[18, 19, 20], [21, 22, 23], [24, 25, 26]],
+ ],
+ ),
+ (
+ list(range(28)),
+ 3,
+ [
+ [
+ [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
+ [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
+ [[18, 19, 20], [21, 22, 23], [24, 25, 26]],
+ ],
+ 27,
+ ],
+ ),
+ (list(range(257)), 256, [list(range(256)), 256]),
+ (list(range(258)), 256, [list(range(256)), 256, 257]),
+ (list(range(512)), 256, [list(range(256)), list(range(256, 512))]),
+ (list(range(512 + 1)), 256, [list(range(256)), list(range(256, 512)), 512]),
+ (
+ list(range(256 ** 2)),
+ 256,
+ [list(range(k * 256, k * 256 + 256)) for k in range(256)],
+ ),
+ ],
+)
+def test_build_n_ary_tree(lst, n, expected):
+ assert _build_n_ary_tree(lst, n) == expected
diff --git a/Tests/otlLib/builder_test.py b/Tests/otlLib/builder_test.py
index bdfc6450..3ea5a745 100644
--- a/Tests/otlLib/builder_test.py
+++ b/Tests/otlLib/builder_test.py
@@ -2,7 +2,7 @@ import io
import struct
from fontTools.misc.fixedTools import floatToFixed
from fontTools.misc.testTools import getXML
-from fontTools.otlLib import builder
+from fontTools.otlLib import builder, error
from fontTools import ttLib
from fontTools.ttLib.tables import otTables
import pytest
@@ -1101,6 +1101,12 @@ class ClassDefBuilderTest(object):
assert not b.canAdd({"d", "e", "f"})
assert not b.canAdd({"f"})
+ def test_add_exception(self):
+ b = builder.ClassDefBuilder(useClass0=True)
+ b.add({"a", "b", "c"})
+ with pytest.raises(error.OpenTypeLibError):
+ b.add({"a", "d"})
+
buildStatTable_test_data = [
([
diff --git a/Tests/subset/data/CmapSubsetTest.subset.ttx b/Tests/subset/data/CmapSubsetTest.subset.ttx
new file mode 100644
index 00000000..10b94a34
--- /dev/null
+++ b/Tests/subset/data/CmapSubsetTest.subset.ttx
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.18">
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ </cmap>
+
+</ttFont>
diff --git a/Tests/subset/data/CmapSubsetTest.ttx b/Tests/subset/data/CmapSubsetTest.ttx
new file mode 100644
index 00000000..ffbfae7f
--- /dev/null
+++ b/Tests/subset/data/CmapSubsetTest.ttx
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.18">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="a"/>
+ <GlyphID id="2" name="basket"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="0.0"/>
+ <checkSumAdjustment value="0xc643119c"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Tue Jan 12 16:39:39 2021"/>
+ <modified value="Tue Jan 12 16:39:39 2021"/>
+ <xMin value="50"/>
+ <yMin value="-200"/>
+ <xMax value="450"/>
+ <yMax value="800"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="1000"/>
+ <descent value="-200"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="942"/>
+ <minLeftSideBearing value="50"/>
+ <minRightSideBearing value="50"/>
+ <xMaxExtent value="450"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="3"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="3"/>
+ <maxPoints value="8"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="660"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="650"/>
+ <ySubscriptYSize value="600"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="75"/>
+ <ySuperscriptXSize value="650"/>
+ <ySuperscriptYSize value="600"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="350"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="300"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulUnicodeRange2 value="00000010 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="NONE"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="97"/>
+ <usLastCharIndex value="65535"/>
+ <sTypoAscender value="800"/>
+ <sTypoDescender value="-200"/>
+ <sTypoLineGap value="200"/>
+ <usWinAscent value="1000"/>
+ <usWinDescent value="200"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="500"/>
+ <sCapHeight value="700"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="500" lsb="50"/>
+ <mtx name="a" width="538" lsb="0"/>
+ <mtx name="basket" width="942" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ <cmap_format_12 platformID="0" platEncID="4" format="12" reserved="0" length="40" language="0" nGroups="2">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x1f9fa" name="basket"/><!-- BASKET -->
+ </cmap_format_12>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ <cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="40" language="0" nGroups="2">
+ <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+ <map code="0x1f9fa" name="basket"/><!-- BASKET -->
+ </cmap_format_12>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="50" yMin="-200" xMax="450" yMax="800">
+ <contour>
+ <pt x="50" y="-200" on="1"/>
+ <pt x="50" y="800" on="1"/>
+ <pt x="450" y="800" on="1"/>
+ <pt x="450" y="-200" on="1"/>
+ </contour>
+ <contour>
+ <pt x="100" y="-150" on="1"/>
+ <pt x="400" y="-150" on="1"/>
+ <pt x="400" y="750" on="1"/>
+ <pt x="100" y="750" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="a"/><!-- contains no outline data -->
+
+ <TTGlyph name="basket"/><!-- contains no outline data -->
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ New Font
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 0.000;NONE;NewFont-Regular
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ New Font Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 0.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Regular
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-75"/>
+ <underlineThickness value="50"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ <psName name="basket"/>
+ </extraNames>
+ </post>
+
+</ttFont>
diff --git a/Tests/subset/data/TestContextSubstFormat3.ttx b/Tests/subset/data/TestContextSubstFormat3.ttx
index 899b037e..3e9bfcd2 100644
--- a/Tests/subset/data/TestContextSubstFormat3.ttx
+++ b/Tests/subset/data/TestContextSubstFormat3.ttx
@@ -17,7 +17,7 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="1.0"/>
- <checkSumAdjustment value="0xa69ed898"/>
+ <checkSumAdjustment value="0xa6bcdc24"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00001111"/>
<unitsPerEm value="1000"/>
@@ -142,15 +142,9 @@
<cmap_format_4 platformID="0" platEncID="3" language="0">
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
</cmap_format_4>
- <cmap_format_12 platformID="0" platEncID="4" format="12" reserved="0" length="28" language="0" nGroups="1">
- <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
- </cmap_format_12>
<cmap_format_4 platformID="3" platEncID="1" language="0">
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
</cmap_format_4>
- <cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="28" language="0" nGroups="1">
- <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
- </cmap_format_12>
</cmap>
<loca>
diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py
index 0d2f9fe2..d37634f1 100644
--- a/Tests/subset/subset_test.py
+++ b/Tests/subset/subset_test.py
@@ -753,6 +753,13 @@ class SubsetTest(unittest.TestCase):
# check all glyphs are kept via GSUB closure, no changes expected
self.expect_ttx(subsetfont, ttx)
+ def test_cmap_prune_format12(self):
+ _, fontpath = self.compile_font(self.getpath("CmapSubsetTest.ttx"), ".ttf")
+ subsetpath = self.temp_path(".ttf")
+ subset.main([fontpath, "--glyphs=a", "--output-file=%s" % subsetpath])
+ subsetfont = TTFont(subsetpath)
+ self.expect_ttx(subsetfont, self.getpath("CmapSubsetTest.subset.ttx"), ["cmap"])
+
@pytest.fixture
def featureVarsTestFont():
diff --git a/Tests/ttLib/tables/_n_a_m_e_test.py b/Tests/ttLib/tables/_n_a_m_e_test.py
index bc4aab2f..11aeebae 100644
--- a/Tests/ttLib/tables/_n_a_m_e_test.py
+++ b/Tests/ttLib/tables/_n_a_m_e_test.py
@@ -432,6 +432,18 @@ class NameRecordTest(unittest.TestCase):
name = makeName(b'\xfe', 123, 1, 1, 0) # Mac Japanese
self.assertEqual(name.toUnicode(), unichr(0x2122))
+ def test_extended_mac_encodings_errors(self):
+ s = "汉仪彩云体简"
+ name = makeName(s.encode("x_mac_simp_chinese_ttx"), 123, 1, 25, 0)
+ # first check we round-trip with 'strict'
+ self.assertEqual(name.toUnicode(errors="strict"), s)
+
+ # append an incomplete invalid sequence and check that we handle
+ # errors with the requested error handler
+ name.string += b"\xba"
+ self.assertEqual(name.toUnicode(errors="backslashreplace"), s + "\\xba")
+ self.assertEqual(name.toUnicode(errors="replace"), s + "�")
+
def test_extended_unknown(self):
name = makeName(b'\xfe', 123, 10, 11, 12)
self.assertEqual(name.getEncoding(), "ascii")
diff --git a/Tests/ttLib/woff2_test.py b/Tests/ttLib/woff2_test.py
index 661fd448..5923b7f2 100644
--- a/Tests/ttLib/woff2_test.py
+++ b/Tests/ttLib/woff2_test.py
@@ -203,7 +203,7 @@ def normalise_font(font, padding=4):
# drop DSIG but keep a copy
DSIG_copy = copy.deepcopy(font['DSIG'])
del font['DSIG']
- # ovverride TTFont attributes
+ # override TTFont attributes
origFlavor = font.flavor
origRecalcBBoxes = font.recalcBBoxes
origRecalcTimestamp = font.recalcTimestamp
diff --git a/setup.cfg b/setup.cfg
index 285f8ef5..8f8c5308 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.18.2
+current_version = 4.19.0
commit = True
tag = False
tag_name = {new_version}
diff --git a/setup.py b/setup.py
index 5f24baff..5e3226c4 100755
--- a/setup.py
+++ b/setup.py
@@ -441,7 +441,7 @@ if ext_modules:
setup_params = dict(
name="fonttools",
- version="4.18.2",
+ version="4.19.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",