From 48ad6aad4d82d71b250cdcbfd0849852df53df41 Mon Sep 17 00:00:00 2001 From: Haibo Huang Date: Wed, 20 May 2020 04:45:51 -0700 Subject: Upgrade fonttools to 4.10.2 Test: None Change-Id: Id3822c9b749cbea541cf013f9404d8548292e3f4 --- Lib/fontTools/__init__.py | 2 +- Lib/fontTools/feaLib/ast.py | 40 +++-- Lib/fontTools/misc/arrayTools.py | 261 +++++++++++++++++++++++------- Lib/fontTools/misc/bezierTools.py | 279 +++++++++++++++++++++++++-------- Lib/fontTools/misc/cliTools.py | 22 +++ Lib/fontTools/misc/eexec.py | 62 +++++++- Lib/fontTools/ttLib/sfnt.py | 50 +++--- Lib/fontTools/ttLib/tables/_n_a_m_e.py | 7 +- Lib/fonttools.egg-info/PKG-INFO | 49 ++++-- 9 files changed, 590 insertions(+), 182 deletions(-) (limited to 'Lib') diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py index 11cc487d..32cd174b 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.10.0" +version = __version__ = "4.10.2" __all__ = ["version", "log", "configLogger"] diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index a305e67f..8d5f97a8 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -617,16 +617,24 @@ class ChainContextPosStatement(Statement): ``prefix``, ``glyphs``, and ``suffix`` should be lists of `glyph-containing objects`_ . - ``lookups`` should be a list of lists containing :class:`LookupBlock` - statements. The length of the outer list should equal to the length of - ``glyphs``; the inner lists can be of variable length. Where there is no - chaining lookup at the given glyph position, the entry in ``lookups`` - should be ``None``.""" + ``lookups`` should be a list of elements representing what lookups + to apply at each glyph position. Each element should be a + :class:`LookupBlock` to apply a single chaining lookup at the given + position, a list of :class:`LookupBlock`\ s to apply multiple + lookups, or ``None`` to apply no lookup. The length of the outer + list should equal the length of ``glyphs``; the inner lists can be + of variable length.""" def __init__(self, prefix, glyphs, suffix, lookups, location=None): Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix - self.lookups = lookups + self.lookups = list(lookups) + for i, lookup in enumerate(lookups): + if lookup: + try: + (_ for _ in lookup) + except TypeError: + self.lookups[i] = [lookup] def build(self, builder): """Calls the builder's ``add_chain_context_pos`` callback.""" @@ -662,14 +670,24 @@ class ChainContextSubstStatement(Statement): ``prefix``, ``glyphs``, and ``suffix`` should be lists of `glyph-containing objects`_ . - ``lookups`` should be a list of :class:`LookupBlock` statements, with - length equal to the length of ``glyphs``. Where there is no chaining - lookup at the given glyph position, the entry in ``lookups`` should be - ``None``.""" + ``lookups`` should be a list of elements representing what lookups + to apply at each glyph position. Each element should be a + :class:`LookupBlock` to apply a single chaining lookup at the given + position, a list of :class:`LookupBlock`\ s to apply multiple + lookups, or ``None`` to apply no lookup. The length of the outer + list should equal the length of ``glyphs``; the inner lists can be + of variable length.""" + def __init__(self, prefix, glyphs, suffix, lookups, location=None): Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix - self.lookups = lookups + self.lookups = list(lookups) + for i, lookup in enumerate(lookups): + if lookup: + try: + (_ for _ in lookup) + except TypeError: + self.lookups[i] = [lookup] def build(self, builder): """Calls the builder's ``add_chain_context_subst`` callback.""" diff --git a/Lib/fontTools/misc/arrayTools.py b/Lib/fontTools/misc/arrayTools.py index f929676c..81b2418d 100644 --- a/Lib/fontTools/misc/arrayTools.py +++ b/Lib/fontTools/misc/arrayTools.py @@ -1,8 +1,6 @@ -# -# Various array and rectangle tools, but mostly rectangles, hence the -# name of this module (not). -# - +"""Routines for calculating bounding boxes, point in rectangle calculations and +so on. +""" from fontTools.misc.py23 import * from fontTools.misc.fixedTools import otRound @@ -11,8 +9,13 @@ import math import operator def calcBounds(array): - """Return the bounding rectangle of a 2D points array as a tuple: - (xMin, yMin, xMax, yMax) + """Calculate the bounding rectangle of a 2D points array. + + Args: + array: A sequence of 2D tuples. + + Returns: + A four-item tuple representing the bounding rectangle ``(xMin, yMin, xMax, yMax)``. """ if len(array) == 0: return 0, 0, 0, 0 @@ -21,29 +24,64 @@ def calcBounds(array): return min(xs), min(ys), max(xs), max(ys) def calcIntBounds(array, round=otRound): - """Return the integer bounding rectangle of a 2D points array as a - tuple: (xMin, yMin, xMax, yMax) - Values are rounded to closest integer towards +Infinity using otRound - function by default, unless an optional 'round' function is passed. + """Calculate the integer bounding rectangle of a 2D points array. + + Values are rounded to closest integer towards ``+Infinity`` using the + :func:`fontTools.misc.fixedTools.otRound` function by default, unless + an optional ``round`` function is passed. + + Args: + array: A sequence of 2D tuples. + round: A rounding function of type ``f(x: float) -> int``. + + Returns: + A four-item tuple of integers representing the bounding rectangle: + ``(xMin, yMin, xMax, yMax)``. """ return tuple(round(v) for v in calcBounds(array)) def updateBounds(bounds, p, min=min, max=max): - """Return the bounding recangle of rectangle bounds and point (x, y).""" + """Add a point to a bounding rectangle. + + Args: + bounds: A bounding rectangle expressed as a tuple + ``(xMin, yMin, xMax, yMax)``. + p: A 2D tuple representing a point. + min,max: functions to compute the minimum and maximum. + + Returns: + The updated bounding rectangle ``(xMin, yMin, xMax, yMax)``. + """ (x, y) = p xMin, yMin, xMax, yMax = bounds return min(xMin, x), min(yMin, y), max(xMax, x), max(yMax, y) def pointInRect(p, rect): - """Return True when point (x, y) is inside rect.""" + """Test if a point is inside a bounding rectangle. + + Args: + p: A 2D tuple representing a point. + rect: A bounding rectangle expressed as a tuple + ``(xMin, yMin, xMax, yMax)``. + + Returns: + ``True`` if the point is inside the rectangle, ``False`` otherwise. + """ (x, y) = p xMin, yMin, xMax, yMax = rect return (xMin <= x <= xMax) and (yMin <= y <= yMax) def pointsInRect(array, rect): - """Find out which points or array are inside rect. - Returns an array with a boolean for each point. + """Determine which points are inside a bounding rectangle. + + Args: + array: A sequence of 2D tuples. + rect: A bounding rectangle expressed as a tuple + ``(xMin, yMin, xMax, yMax)``. + + Returns: + A list containing the points inside the rectangle. """ if len(array) < 1: return [] @@ -51,41 +89,105 @@ def pointsInRect(array, rect): return [(xMin <= x <= xMax) and (yMin <= y <= yMax) for x, y in array] def vectorLength(vector): - """Return the length of the given vector.""" + """Calculate the length of the given vector. + + Args: + vector: A 2D tuple. + + Returns: + The Euclidean length of the vector. + """ x, y = vector return math.sqrt(x**2 + y**2) def asInt16(array): - """Round and cast to 16 bit integer.""" + """Round a list of floats to 16-bit signed integers. + + Args: + array: List of float values. + + Returns: + A list of rounded integers. + """ return [int(math.floor(i+0.5)) for i in array] def normRect(rect): - """Normalize the rectangle so that the following holds: + """Normalize a bounding box rectangle. + + This function "turns the rectangle the right way up", so that the following + holds:: + xMin <= xMax and yMin <= yMax + + Args: + rect: A bounding rectangle expressed as a tuple + ``(xMin, yMin, xMax, yMax)``. + + Returns: + A normalized bounding rectangle. """ (xMin, yMin, xMax, yMax) = rect return min(xMin, xMax), min(yMin, yMax), max(xMin, xMax), max(yMin, yMax) def scaleRect(rect, x, y): - """Scale the rectangle by x, y.""" + """Scale a bounding box rectangle. + + Args: + rect: A bounding rectangle expressed as a tuple + ``(xMin, yMin, xMax, yMax)``. + x: Factor to scale the rectangle along the X axis. + Y: Factor to scale the rectangle along the Y axis. + + Returns: + A scaled bounding rectangle. + """ (xMin, yMin, xMax, yMax) = rect return xMin * x, yMin * y, xMax * x, yMax * y def offsetRect(rect, dx, dy): - """Offset the rectangle by dx, dy.""" + """Offset a bounding box rectangle. + + Args: + rect: A bounding rectangle expressed as a tuple + ``(xMin, yMin, xMax, yMax)``. + dx: Amount to offset the rectangle along the X axis. + dY: Amount to offset the rectangle along the Y axis. + + Returns: + An offset bounding rectangle. + """ (xMin, yMin, xMax, yMax) = rect return xMin+dx, yMin+dy, xMax+dx, yMax+dy def insetRect(rect, dx, dy): - """Inset the rectangle by dx, dy on all sides.""" + """Inset a bounding box rectangle on all sides. + + Args: + rect: A bounding rectangle expressed as a tuple + ``(xMin, yMin, xMax, yMax)``. + dx: Amount to inset the rectangle along the X axis. + dY: Amount to inset the rectangle along the Y axis. + + Returns: + An inset bounding rectangle. + """ (xMin, yMin, xMax, yMax) = rect return xMin+dx, yMin+dy, xMax-dx, yMax-dy def sectRect(rect1, rect2): - """Return a boolean and a rectangle. If the input rectangles intersect, return - True and the intersecting rectangle. Return False and (0, 0, 0, 0) if the input - rectangles don't intersect. + """Test for rectangle-rectangle intersection. + + Args: + rect1: First bounding rectangle, expressed as tuples + ``(xMin, yMin, xMax, yMax)``. + rect2: Second bounding rectangle. + + Returns: + A boolean and a rectangle. + If the input rectangles intersect, returns ``True`` and the intersecting + rectangle. Returns ``False`` and ``(0, 0, 0, 0)`` if the input + rectangles don't intersect. """ (xMin1, yMin1, xMax1, yMax1) = rect1 (xMin2, yMin2, xMax2, yMax2) = rect2 @@ -96,9 +198,16 @@ def sectRect(rect1, rect2): return True, (xMin, yMin, xMax, yMax) def unionRect(rect1, rect2): - """Return the smallest rectangle in which both input rectangles are fully - enclosed. In other words, return the total bounding rectangle of both input - rectangles. + """Determine union of bounding rectangles. + + Args: + rect1: First bounding rectangle, expressed as tuples + ``(xMin, yMin, xMax, yMax)``. + rect2: Second bounding rectangle. + + Returns: + The smallest rectangle in which both input rectangles are fully + enclosed. """ (xMin1, yMin1, xMax1, yMax1) = rect1 (xMin2, yMin2, xMax2, yMax2) = rect2 @@ -106,16 +215,32 @@ def unionRect(rect1, rect2): max(xMax1, xMax2), max(yMax1, yMax2)) return (xMin, yMin, xMax, yMax) -def rectCenter(rect0): - """Return the center of the rectangle as an (x, y) coordinate.""" - (xMin, yMin, xMax, yMax) = rect0 +def rectCenter(rect): + """Determine rectangle center. + + Args: + rect: Bounding rectangle, expressed as tuples + ``(xMin, yMin, xMax, yMax)``. + + Returns: + A 2D tuple representing the point at the center of the rectangle. + """ + (xMin, yMin, xMax, yMax) = rect return (xMin+xMax)/2, (yMin+yMax)/2 -def intRect(rect1): - """Return the rectangle, rounded off to integer values, but guaranteeing that - the resulting rectangle is NOT smaller than the original. +def intRect(rect): + """Round a rectangle to integer values. + + Guarantees that the resulting rectangle is NOT smaller than the original. + + Args: + rect: Bounding rectangle, expressed as tuples + ``(xMin, yMin, xMax, yMax)``. + + Returns: + A rounded bounding rectangle. """ - (xMin, yMin, xMax, yMax) = rect1 + (xMin, yMin, xMax, yMax) = rect xMin = int(math.floor(xMin)) yMin = int(math.floor(yMin)) xMax = int(math.ceil(xMax)) @@ -124,9 +249,18 @@ def intRect(rect1): class Vector(object): - """A math-like vector.""" + """A math-like vector. + + Represents an n-dimensional numeric vector. ``Vector`` objects support + vector addition and subtraction, scalar multiplication and division, + negation, rounding, and comparison tests. + + Attributes: + values: Sequence of values stored in the vector. + """ def __init__(self, values, keep=False): + """Initialize a vector. If ``keep`` is true, values will be copied.""" self.values = values if keep else list(values) def __getitem__(self, index): @@ -191,6 +325,7 @@ class Vector(object): def __round__(self): return Vector(self._unaryOp(round), keep=True) def toInt(self): + """Synonym for ``round``.""" return self.__round__() def __eq__(self, other): @@ -208,6 +343,8 @@ class Vector(object): def __abs__(self): return math.sqrt(sum([x*x for x in self.values])) def dot(self, other): + """Performs vector dot product, returning sum of + ``a[0] * b[0], a[1] * b[1], ...``""" a = self.values b = other.values if type(other) == Vector else b assert len(a) == len(b) @@ -215,29 +352,37 @@ class Vector(object): def pairwise(iterable, reverse=False): - """Iterate over current and next items in iterable, optionally in - reverse order. - - >>> tuple(pairwise([])) - () - >>> tuple(pairwise([], reverse=True)) - () - >>> tuple(pairwise([0])) - ((0, 0),) - >>> tuple(pairwise([0], reverse=True)) - ((0, 0),) - >>> tuple(pairwise([0, 1])) - ((0, 1), (1, 0)) - >>> tuple(pairwise([0, 1], reverse=True)) - ((1, 0), (0, 1)) - >>> tuple(pairwise([0, 1, 2])) - ((0, 1), (1, 2), (2, 0)) - >>> tuple(pairwise([0, 1, 2], reverse=True)) - ((2, 1), (1, 0), (0, 2)) - >>> tuple(pairwise(['a', 'b', 'c', 'd'])) - (('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'a')) - >>> tuple(pairwise(['a', 'b', 'c', 'd'], reverse=True)) - (('d', 'c'), ('c', 'b'), ('b', 'a'), ('a', 'd')) + """Iterate over current and next items in iterable. + + Args: + iterable: An iterable + reverse: If true, iterate in reverse order. + + Returns: + A iterable yielding two elements per iteration. + + Example: + + >>> tuple(pairwise([])) + () + >>> tuple(pairwise([], reverse=True)) + () + >>> tuple(pairwise([0])) + ((0, 0),) + >>> tuple(pairwise([0], reverse=True)) + ((0, 0),) + >>> tuple(pairwise([0, 1])) + ((0, 1), (1, 0)) + >>> tuple(pairwise([0, 1], reverse=True)) + ((1, 0), (0, 1)) + >>> tuple(pairwise([0, 1, 2])) + ((0, 1), (1, 2), (2, 0)) + >>> tuple(pairwise([0, 1, 2], reverse=True)) + ((2, 1), (1, 0), (0, 2)) + >>> tuple(pairwise(['a', 'b', 'c', 'd'])) + (('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'a')) + >>> tuple(pairwise(['a', 'b', 'c', 'd'], reverse=True)) + (('d', 'c'), ('c', 'b'), ('b', 'a'), ('a', 'd')) """ if not iterable: return diff --git a/Lib/fontTools/misc/bezierTools.py b/Lib/fontTools/misc/bezierTools.py index bd436fe2..bc3bea2e 100644 --- a/Lib/fontTools/misc/bezierTools.py +++ b/Lib/fontTools/misc/bezierTools.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""fontTools.misc.bezierTools.py -- tools for working with bezier path segments. +"""fontTools.misc.bezierTools.py -- tools for working with Bezier path segments. """ from fontTools.misc.arrayTools import calcBounds @@ -29,7 +29,19 @@ __all__ = [ def calcCubicArcLength(pt1, pt2, pt3, pt4, tolerance=0.005): - """Return the arc length for a cubic bezier segment.""" + """Calculates the arc length for a cubic Bezier segment. + + Whereas :func:`approximateCubicArcLength` approximates the length, this + function calculates it by "measuring", recursively dividing the curve + until the divided segments are shorter than ``tolerance``. + + Args: + pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. + tolerance: Controls the precision of the calcuation. + + Returns: + Arc length value. + """ return calcCubicArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4), tolerance) @@ -49,7 +61,15 @@ def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): return _calcCubicArcLengthCRecurse(mult, *one) + _calcCubicArcLengthCRecurse(mult, *two) def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005): - """Return the arc length for a cubic bezier segment using complex points.""" + """Calculates the arc length for a cubic Bezier segment. + + Args: + pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers. + tolerance: Controls the precision of the calcuation. + + Returns: + Arc length value. + """ mult = 1. + 1.5 * tolerance # The 1.5 is a empirical hack; no math return _calcCubicArcLengthCRecurse(mult, pt1, pt2, pt3, pt4) @@ -69,8 +89,17 @@ def _intSecAtan(x): def calcQuadraticArcLength(pt1, pt2, pt3): - """Return the arc length for a qudratic bezier segment. - pt1 and pt3 are the "anchor" points, pt2 is the "handle". + """Calculates the arc length for a quadratic Bezier segment. + + Args: + pt1: Start point of the Bezier as 2D tuple. + pt2: Handle point of the Bezier as 2D tuple. + pt3: End point of the Bezier as 2D tuple. + + Returns: + Arc length value. + + Example:: >>> calcQuadraticArcLength((0, 0), (0, 0), (0, 0)) # empty segment 0.0 @@ -95,9 +124,16 @@ def calcQuadraticArcLength(pt1, pt2, pt3): def calcQuadraticArcLengthC(pt1, pt2, pt3): - """Return the arc length for a qudratic bezier segment using complex points. - pt1 and pt3 are the "anchor" points, pt2 is the "handle".""" + """Calculates the arc length for a quadratic Bezier segment. + Args: + pt1: Start point of the Bezier as a complex number. + pt2: Handle point of the Bezier as a complex number. + pt3: End point of the Bezier as a complex number. + + Returns: + Arc length value. + """ # Analytical solution to the length of a quadratic bezier. # I'll explain how I arrived at this later. d0 = pt2 - pt1 @@ -120,15 +156,36 @@ def calcQuadraticArcLengthC(pt1, pt2, pt3): def approximateQuadraticArcLength(pt1, pt2, pt3): - # Approximate length of quadratic Bezier curve using Gauss-Legendre quadrature - # with n=3 points. + """Calculates the arc length for a quadratic Bezier segment. + + Uses Gauss-Legendre quadrature for a branch-free approximation. + See :func:`calcQuadraticArcLength` for a slower but more accurate result. + + Args: + pt1: Start point of the Bezier as 2D tuple. + pt2: Handle point of the Bezier as 2D tuple. + pt3: End point of the Bezier as 2D tuple. + + Returns: + Approximate arc length value. + """ return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) def approximateQuadraticArcLengthC(pt1, pt2, pt3): - # Approximate length of quadratic Bezier curve using Gauss-Legendre quadrature - # with n=3 points for complex points. - # + """Calculates the arc length for a quadratic Bezier segment. + + Uses Gauss-Legendre quadrature for a branch-free approximation. + See :func:`calcQuadraticArcLength` for a slower but more accurate result. + + Args: + pt1: Start point of the Bezier as a complex number. + pt2: Handle point of the Bezier as a complex number. + pt3: End point of the Bezier as a complex number. + + Returns: + Approximate arc length value. + """ # This, essentially, approximates the length-of-derivative function # to be integrated with the best-matching fifth-degree polynomial # approximation of it. @@ -145,8 +202,17 @@ def approximateQuadraticArcLengthC(pt1, pt2, pt3): def calcQuadraticBounds(pt1, pt2, pt3): - """Return the bounding rectangle for a qudratic bezier segment. - pt1 and pt3 are the "anchor" points, pt2 is the "handle". + """Calculates the bounding rectangle for a quadratic Bezier segment. + + Args: + pt1: Start point of the Bezier as a 2D tuple. + pt2: Handle point of the Bezier as a 2D tuple. + pt3: End point of the Bezier as a 2D tuple. + + Returns: + A four-item tuple representing the bounding rectangle ``(xMin, yMin, xMax, yMax)``. + + Example:: >>> calcQuadraticBounds((0, 0), (50, 100), (100, 0)) (0, 0, 100, 50.0) @@ -166,8 +232,18 @@ def calcQuadraticBounds(pt1, pt2, pt3): def approximateCubicArcLength(pt1, pt2, pt3, pt4): - """Return the approximate arc length for a cubic bezier segment. - pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles". + """Approximates the arc length for a cubic Bezier segment. + + Uses Gauss-Lobatto quadrature with n=5 points to approximate arc length. + See :func:`calcCubicArcLength` for a slower but more accurate result. + + Args: + pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. + + Returns: + Arc length value. + + Example:: >>> approximateCubicArcLength((0, 0), (25, 100), (75, 100), (100, 0)) 190.04332968932817 @@ -180,18 +256,18 @@ def approximateCubicArcLength(pt1, pt2, pt3, pt4): >>> approximateCubicArcLength((0, 0), (50, 0), (100, -50), (-50, 0)) # cusp 154.80848416537057 """ - # Approximate length of cubic Bezier curve using Gauss-Lobatto quadrature - # with n=5 points. return approximateCubicArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4)) def approximateCubicArcLengthC(pt1, pt2, pt3, pt4): - """Return the approximate arc length for a cubic bezier segment of complex points. - pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles".""" + """Approximates the arc length for a cubic Bezier segment. - # Approximate length of cubic Bezier curve using Gauss-Lobatto quadrature - # with n=5 points for complex points. - # + Args: + pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers. + + Returns: + Arc length value. + """ # This, essentially, approximates the length-of-derivative function # to be integrated with the best-matching seventh-degree polynomial # approximation of it. @@ -210,8 +286,15 @@ def approximateCubicArcLengthC(pt1, pt2, pt3, pt4): def calcCubicBounds(pt1, pt2, pt3, pt4): - """Return the bounding rectangle for a cubic bezier segment. - pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles". + """Calculates the bounding rectangle for a quadratic Bezier segment. + + Args: + pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. + + Returns: + A four-item tuple representing the bounding rectangle ``(xMin, yMin, xMax, yMax)``. + + Example:: >>> calcCubicBounds((0, 0), (25, 100), (75, 100), (100, 0)) (0, 0, 100, 75.0) @@ -235,11 +318,22 @@ def calcCubicBounds(pt1, pt2, pt3, pt4): def splitLine(pt1, pt2, where, isHorizontal): - """Split the line between pt1 and pt2 at position 'where', which - is an x coordinate if isHorizontal is False, a y coordinate if - isHorizontal is True. Return a list of two line segments if the - line was successfully split, or a list containing the original - line. + """Split a line at a given coordinate. + + Args: + pt1: Start point of line as 2D tuple. + pt2: End point of line as 2D tuple. + where: Position at which to split the line. + isHorizontal: Direction of the ray splitting the line. If true, + ``where`` is interpreted as a Y coordinate; if false, then + ``where`` is interpreted as an X coordinate. + + Returns: + A list of two line segments (each line segment being two 2D tuples) + if the line was successfully split, or a list containing the original + line. + + Example:: >>> printSegments(splitLine((0, 0), (100, 100), 50, True)) ((0, 0), (50, 50)) @@ -281,9 +375,21 @@ def splitLine(pt1, pt2, where, isHorizontal): def splitQuadratic(pt1, pt2, pt3, where, isHorizontal): - """Split the quadratic curve between pt1, pt2 and pt3 at position 'where', - which is an x coordinate if isHorizontal is False, a y coordinate if - isHorizontal is True. Return a list of curve segments. + """Split a quadratic Bezier curve at a given coordinate. + + Args: + pt1,pt2,pt3: Control points of the Bezier as 2D tuples. + where: Position at which to split the curve. + isHorizontal: Direction of the ray splitting the curve. If true, + ``where`` is interpreted as a Y coordinate; if false, then + ``where`` is interpreted as an X coordinate. + + Returns: + A list of two curve segments (each curve segment being three 2D tuples) + if the curve was successfully split, or a list containing the original + curve. + + Example:: >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 150, False)) ((0, 0), (50, 100), (100, 0)) @@ -313,9 +419,21 @@ def splitQuadratic(pt1, pt2, pt3, where, isHorizontal): def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal): - """Split the cubic curve between pt1, pt2, pt3 and pt4 at position 'where', - which is an x coordinate if isHorizontal is False, a y coordinate if - isHorizontal is True. Return a list of curve segments. + """Split a cubic Bezier curve at a given coordinate. + + Args: + pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. + where: Position at which to split the curve. + isHorizontal: Direction of the ray splitting the curve. If true, + ``where`` is interpreted as a Y coordinate; if false, then + ``where`` is interpreted as an X coordinate. + + Returns: + A list of two curve segments (each curve segment being four 2D tuples) + if the curve was successfully split, or a list containing the original + curve. + + Example:: >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 150, False)) ((0, 0), (25, 100), (75, 100), (100, 0)) @@ -337,8 +455,16 @@ def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal): def splitQuadraticAtT(pt1, pt2, pt3, *ts): - """Split the quadratic curve between pt1, pt2 and pt3 at one or more - values of t. Return a list of curve segments. + """Split a quadratic Bezier curve at one or more values of t. + + Args: + pt1,pt2,pt3: Control points of the Bezier as 2D tuples. + *ts: Positions at which to split the curve. + + Returns: + A list of curve segments (each curve segment being three 2D tuples). + + Examples:: >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5)) ((0, 0), (25, 50), (50, 50)) @@ -353,8 +479,16 @@ def splitQuadraticAtT(pt1, pt2, pt3, *ts): def splitCubicAtT(pt1, pt2, pt3, pt4, *ts): - """Split the cubic curve between pt1, pt2, pt3 and pt4 at one or more - values of t. Return a list of curve segments. + """Split a cubic Bezier curve at one or more values of t. + + Args: + pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. + *ts: Positions at which to split the curve. + + Returns: + A list of curve segments (each curve segment being four 2D tuples). + + Examples:: >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5)) ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) @@ -437,10 +571,18 @@ from math import sqrt, acos, cos, pi def solveQuadratic(a, b, c, sqrt=sqrt): - """Solve a quadratic equation where a, b and c are real. - a*x*x + b*x + c = 0 - This function returns a list of roots. Note that the returned list - is neither guaranteed to be sorted nor to contain unique values! + """Solve a quadratic equation. + + Solves *a*x*x + b*x + c = 0* where a, b and c are real. + + Args: + a: coefficient of *x²* + b: coefficient of *x* + c: constant term + + Returns: + A list of roots. Note that the returned list is neither guaranteed to + be sorted nor to contain unique values! """ if abs(a) < epsilon: if abs(b) < epsilon: @@ -462,25 +604,36 @@ def solveQuadratic(a, b, c, def solveCubic(a, b, c, d): - """Solve a cubic equation where a, b, c and d are real. - a*x*x*x + b*x*x + c*x + d = 0 - This function returns a list of roots. Note that the returned list - is neither guaranteed to be sorted nor to contain unique values! - - >>> solveCubic(1, 1, -6, 0) - [-3.0, -0.0, 2.0] - >>> solveCubic(-10.0, -9.0, 48.0, -29.0) - [-2.9, 1.0, 1.0] - >>> solveCubic(-9.875, -9.0, 47.625, -28.75) - [-2.911392, 1.0, 1.0] - >>> solveCubic(1.0, -4.5, 6.75, -3.375) - [1.5, 1.5, 1.5] - >>> solveCubic(-12.0, 18.0, -9.0, 1.50023651123) - [0.5, 0.5, 0.5] - >>> solveCubic( - ... 9.0, 0.0, 0.0, -7.62939453125e-05 - ... ) == [-0.0, -0.0, -0.0] - True + """Solve a cubic equation. + + Solves *a*x*x*x + b*x*x + c*x + d = 0* where a, b, c and d are real. + + Args: + a: coefficient of *x³* + b: coefficient of *x²* + c: coefficient of *x* + d: constant term + + Returns: + A list of roots. Note that the returned list is neither guaranteed to + be sorted nor to contain unique values! + + Examples:: + + >>> solveCubic(1, 1, -6, 0) + [-3.0, -0.0, 2.0] + >>> solveCubic(-10.0, -9.0, 48.0, -29.0) + [-2.9, 1.0, 1.0] + >>> solveCubic(-9.875, -9.0, 47.625, -28.75) + [-2.911392, 1.0, 1.0] + >>> solveCubic(1.0, -4.5, 6.75, -3.375) + [1.5, 1.5, 1.5] + >>> solveCubic(-12.0, 18.0, -9.0, 1.50023651123) + [0.5, 0.5, 0.5] + >>> solveCubic( + ... 9.0, 0.0, 0.0, -7.62939453125e-05 + ... ) == [-0.0, -0.0, -0.0] + True """ # # adapted from: diff --git a/Lib/fontTools/misc/cliTools.py b/Lib/fontTools/misc/cliTools.py index 48913c14..4e5353b9 100644 --- a/Lib/fontTools/misc/cliTools.py +++ b/Lib/fontTools/misc/cliTools.py @@ -8,6 +8,28 @@ numberAddedRE = re.compile(r"#\d+$") def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False): + """Generates a suitable file name for writing output. + + Often tools will want to take a file, do some kind of transformation to it, + and write it out again. This function determines an appropriate name for the + output file, through one or more of the following steps: + + - changing the output directory + - replacing the file extension + - suffixing the filename with a number (``#1``, ``#2``, etc.) to avoid + overwriting an existing file. + + Args: + input: Name of input file. + outputDir: Optionally, a new directory to write the file into. + extension: Optionally, a replacement for the current file extension. + overWrite: Overwriting an existing file is permitted if true; if false + and the proposed filename exists, a new name will be generated by + adding an appropriate number suffix. + + Returns: + str: Suitable output filename + """ dirName, fileName = os.path.split(input) fileName, ext = os.path.splitext(fileName) if outputDir: diff --git a/Lib/fontTools/misc/eexec.py b/Lib/fontTools/misc/eexec.py index 19ec6b08..36719a1c 100644 --- a/Lib/fontTools/misc/eexec.py +++ b/Lib/fontTools/misc/eexec.py @@ -1,5 +1,15 @@ -"""fontTools.misc.eexec.py -- Module implementing the eexec and -charstring encryption algorithm as used by PostScript Type 1 fonts. +""" +PostScript Type 1 fonts make use of two types of encryption: charstring +encryption and ``eexec`` encryption. Charstring encryption is used for +the charstrings themselves, while ``eexec`` is used to encrypt larger +sections of the font program, such as the ``Private`` and ``CharStrings`` +dictionaries. Despite the different names, the algorithm is the same, +although ``eexec`` encryption uses a fixed initial key R=55665. + +The algorithm uses cipher feedback, meaning that the ciphertext is used +to modify the key. Because of this, the routines in this module return +the new key at the end of the operation. + """ from fontTools.misc.py23 import * @@ -19,12 +29,24 @@ def _encryptChar(plain, R): def decrypt(cipherstring, R): r""" - >>> testStr = b"\0\0asdadads asds\265" - >>> decryptedStr, R = decrypt(testStr, 12321) - >>> decryptedStr == b'0d\nh\x15\xe8\xc4\xb2\x15\x1d\x108\x1a<6\xa1' - True - >>> R == 36142 - True + Decrypts a string using the Type 1 encryption algorithm. + + Args: + cipherstring: String of ciphertext. + R: Initial key. + + Returns: + decryptedStr: Plaintext string. + R: Output key for subsequent decryptions. + + Examples:: + + >>> testStr = b"\0\0asdadads asds\265" + >>> decryptedStr, R = decrypt(testStr, 12321) + >>> decryptedStr == b'0d\nh\x15\xe8\xc4\xb2\x15\x1d\x108\x1a<6\xa1' + True + >>> R == 36142 + True """ plainList = [] for cipher in cipherstring: @@ -35,6 +57,30 @@ def decrypt(cipherstring, R): def encrypt(plainstring, R): r""" + Encrypts a string using the Type 1 encryption algorithm. + + Note that the algorithm as described in the Type 1 specification requires the + plaintext to be prefixed with a number of random bytes. (For ``eexec`` the + number of random bytes is set to 4.) This routine does *not* add the random + prefix to its input. + + Args: + plainstring: String of plaintext. + R: Initial key. + + Returns: + cipherstring: Ciphertext string. + R: Output key for subsequent encryptions. + + Examples:: + + >>> testStr = b"\0\0asdadads asds\265" + >>> decryptedStr, R = decrypt(testStr, 12321) + >>> decryptedStr == b'0d\nh\x15\xe8\xc4\xb2\x15\x1d\x108\x1a<6\xa1' + True + >>> R == 36142 + True + >>> testStr = b'0d\nh\x15\xe8\xc4\xb2\x15\x1d\x108\x1a<6\xa1' >>> encryptedStr, R = encrypt(testStr, 12321) >>> encryptedStr == b"\0\0asdadads asds\265" diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py index 9c45305d..2f3b6698 100644 --- a/Lib/fontTools/ttLib/sfnt.py +++ b/Lib/fontTools/ttLib/sfnt.py @@ -12,7 +12,9 @@ classes, since whenever to number of tables changes or whenever a table's length chages you need to rewrite the whole file anyway. """ -from fontTools.misc.py23 import * +from io import BytesIO +from types import SimpleNamespace +from fontTools.misc.py23 import Tag from fontTools.misc import sstruct from fontTools.ttLib import TTLibError import struct @@ -122,29 +124,29 @@ class SFNTReader(object): def close(self): self.file.close() - def __deepcopy__(self, memo): - """Overrides the default deepcopy of SFNTReader object, to make it work - in the case when TTFont is loaded with lazy=True, and thus reader holds a - reference to a file object which is not pickleable. - We work around it by manually copying the data into a in-memory stream. - """ - from copy import deepcopy - - cls = self.__class__ - obj = cls.__new__(cls) - for k, v in self.__dict__.items(): - if k == "file": - pos = v.tell() - v.seek(0) - buf = BytesIO(v.read()) - v.seek(pos) - buf.seek(pos) - if hasattr(v, "name"): - buf.name = v.name - obj.file = buf - else: - obj.__dict__[k] = deepcopy(v, memo) - return obj + # We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able + # and deepcopy-able. When a TTFont is loaded as lazy=True, SFNTReader holds a + # reference to an external file object which is not pickleable. So in __getstate__ + # we store the file name and current position, and in __setstate__ we reopen the + # same named file after unpickling. + + def __getstate__(self): + if isinstance(self.file, BytesIO): + # BytesIO is already pickleable, return the state unmodified + return self.__dict__ + + # remove unpickleable file attribute, and only store its name and pos + state = self.__dict__.copy() + del state["file"] + state["_filename"] = self.file.name + state["_filepos"] = self.file.tell() + return state + + def __setstate__(self, state): + if "file" not in state: + self.file = open(state.pop("_filename"), "rb") + self.file.seek(state.pop("_filepos")) + self.__dict__.update(state) # default compression level for WOFF 1.0 tables and metadata diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py index ec5d07ee..3f756c8f 100644 --- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py +++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py @@ -222,8 +222,11 @@ class table__n_a_m_e(DefaultTable.DefaultTable): # Collect matching name IDs matchingNames = dict() for name in self.names: - key = (name.string, name.platformID, - name.platEncID, name.langID) + try: + key = (name.toUnicode(), name.platformID, + name.platEncID, name.langID) + except UnicodeDecodeError: + continue if key in reqNameSet: nameSet = matchingNames.setdefault(name.nameID, set()) nameSet.add(key) diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO index 3ca17207..96babb71 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.10.0 +Version: 4.10.2 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -211,15 +211,15 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland, Jelle Bosma, Sascha Brawer, Tom Byrer, Frédéric Coiffier, Vincent - Connare, Dave Crossland, Simon Daniels, Peter Dekkers, Behdad Esfahbod, - Behnam Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Yannis - Haralambous, Greg Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson, - Denis Moyogo Jacquerye, Jack Jansen, Tom Kacvinsky, Jens Kutilek, - Antoine Leca, Werner Lemberg, Tal Leming, Peter Lofting, Cosimo Lupo, - Masaya Nakamura, Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret - Rieger, Read Roberts, Guido van Rossum, Just van Rossum, Andreas Seidel, - Georg Seifert, Chris Simpkins, Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov, - Paul Wise. + Connare, David Corbett, Dave Crossland, Simon Daniels, Peter Dekkers, + Behdad Esfahbod, Behnam Esfahbod, Hannes Famira, Sam Fishman, Matt + Fontaine, Yannis Haralambous, Greg Hitchcock, Jeremie Hornus, Khaled + Hosny, John Hudson, Denis Moyogo Jacquerye, Jack Jansen, Tom Kacvinsky, + Jens Kutilek, Antoine Leca, Werner Lemberg, Tal Leming, Peter Lofting, + Cosimo Lupo, Masaya Nakamura, Dave Opstad, Laurence Penney, Roozbeh + Pournader, Garret Rieger, Read Roberts, Guido van Rossum, Just van + Rossum, Andreas Seidel, Georg Seifert, Chris Simpkins, Miguel Sousa, + Adam Twardoch, Adrien Tétar, Vitaly Volkov, Paul Wise. Copyrights ~~~~~~~~~~ @@ -253,6 +253,25 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py Changelog ~~~~~~~~~ + 4.10.2 (released 2020-05-20) + ---------------------------- + + - [sfnt] Fixed ``NameError: SimpleNamespace`` while reading TTC header. The regression + was introduced with 4.10.1 after removing ``py23`` star import. + + 4.10.1 (released 2020-05-19) + ---------------------------- + + - [sfnt] Make ``SFNTReader`` pickleable even when TTFont is loaded with lazy=True + option and thus keeps a reference to an external file (#1962, #1967). + - [feaLib.ast] Restore backward compatibility (broken in 4.10 with #1905) for + ``ChainContextPosStatement`` and ``ChainContextSubstStatement`` classes. + Make them accept either list of lookups or list of lists of lookups (#1961). + - [docs] Document some modules in ``fontTools.misc`` package: ``arrayTools``, + ``bezierTools`` ``cliTools`` and ``eexec`` (#1956). + - [ttLib._n_a_m_e] Fixed ``findMultilingualName()`` when name record's ``string`` is + encoded as bytes sequence (#1963). + 4.10.0 (released 2020-05-15) ---------------------------- @@ -1945,13 +1964,13 @@ Classifier: Topic :: Text Processing :: Fonts Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion Requires-Python: >=3.6 -Provides-Extra: interpolatable +Provides-Extra: unicode +Provides-Extra: all Provides-Extra: ufo +Provides-Extra: interpolatable Provides-Extra: graphite -Provides-Extra: lxml -Provides-Extra: unicode Provides-Extra: type1 +Provides-Extra: symfont +Provides-Extra: lxml Provides-Extra: plot Provides-Extra: woff -Provides-Extra: symfont -Provides-Extra: all -- cgit v1.2.3