From 76902b7129372e8f56c52438920ee968ec2cbd1c Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 19 May 2020 09:51:17 +0100 Subject: [PATCH] [docs] fontTools.misc.* part 1 (#1956) * Document misc.arrayTools * Document misc.bezierTools * Document cliTools * Document eexec --- Doc/source/misc/arrayTools.rst | 7 +- Doc/source/misc/bezierTools.rst | 6 +- Doc/source/misc/cliTools.rst | 6 +- Doc/source/misc/eexec.rst | 6 +- Doc/source/misc/index.rst | 9 +- Lib/fontTools/misc/arrayTools.py | 259 ++++++++++++++++++++++------ Lib/fontTools/misc/bezierTools.py | 277 +++++++++++++++++++++++------- Lib/fontTools/misc/cliTools.py | 22 +++ Lib/fontTools/misc/eexec.py | 62 ++++++- 9 files changed, 512 insertions(+), 142 deletions(-) diff --git a/Doc/source/misc/arrayTools.rst b/Doc/source/misc/arrayTools.rst index 7beaa7afb..d996cc203 100644 --- a/Doc/source/misc/arrayTools.rst +++ b/Doc/source/misc/arrayTools.rst @@ -1,8 +1,9 @@ -########## -arrayTools -########## +############################################# +arrayTools: Various array and rectangle tools +############################################# .. automodule:: fontTools.misc.arrayTools + :member-order: bysource :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/bezierTools.rst b/Doc/source/misc/bezierTools.rst index a8bafe53a..10ddc3aa5 100644 --- a/Doc/source/misc/bezierTools.rst +++ b/Doc/source/misc/bezierTools.rst @@ -1,6 +1,6 @@ -########### -bezierTools -########### +#################################################### +bezierTools: Routines for working with Bezier curves +#################################################### .. automodule:: fontTools.misc.bezierTools :inherited-members: diff --git a/Doc/source/misc/cliTools.rst b/Doc/source/misc/cliTools.rst index 6e1b90e03..36b2aeb9c 100644 --- a/Doc/source/misc/cliTools.rst +++ b/Doc/source/misc/cliTools.rst @@ -1,6 +1,6 @@ -######## -cliTools -######## +################################################################### +cliTools: Utilities for command-line interfaces and console scripts +################################################################### .. automodule:: fontTools.misc.cliTools :inherited-members: diff --git a/Doc/source/misc/eexec.rst b/Doc/source/misc/eexec.rst index 53a9aa063..b229d5854 100644 --- a/Doc/source/misc/eexec.rst +++ b/Doc/source/misc/eexec.rst @@ -1,6 +1,6 @@ -##### -eexec -##### +############################################################### +eexec: PostScript charstring encryption and decryption routines +############################################################### .. automodule:: fontTools.misc.eexec :inherited-members: diff --git a/Doc/source/misc/index.rst b/Doc/source/misc/index.rst index 63f431013..92423299f 100644 --- a/Doc/source/misc/index.rst +++ b/Doc/source/misc/index.rst @@ -1,6 +1,9 @@ -#### -misc -#### +########################################################## +misc: Miscellaneous libraries helpful for font engineering +########################################################## + +This is a collection of packages, most of which are used as internal support +utilities by fontTools, but some of which may be more generally useful. .. toctree:: :maxdepth: 2 diff --git a/Lib/fontTools/misc/arrayTools.py b/Lib/fontTools/misc/arrayTools.py index f929676c6..81b2418dc 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. + """Iterate over current and next items in iterable. - >>> 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')) + 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 bd436fe23..bc3bea2e0 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! + """Solve a cubic equation. - >>> 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 + 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 48913c14f..4e5353b93 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 19ec6b08e..36719a1cb 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"