diff options
Diffstat (limited to 'Lib/fontTools/misc')
30 files changed, 5240 insertions, 4507 deletions
diff --git a/Lib/fontTools/misc/arrayTools.py b/Lib/fontTools/misc/arrayTools.py index 01ccbe82..ced8d87a 100644 --- a/Lib/fontTools/misc/arrayTools.py +++ b/Lib/fontTools/misc/arrayTools.py @@ -23,6 +23,7 @@ def calcBounds(array): ys = [y for x, y in array] return min(xs), min(ys), max(xs), max(ys) + def calcIntBounds(array, round=otRound): """Calculate the integer bounding rectangle of a 2D points array. @@ -46,7 +47,7 @@ def updateBounds(bounds, p, min=min, max=max): Args: bounds: A bounding rectangle expressed as a tuple - ``(xMin, yMin, xMax, yMax)``. + ``(xMin, yMin, xMax, yMax), or None``. p: A 2D tuple representing a point. min,max: functions to compute the minimum and maximum. @@ -54,9 +55,12 @@ def updateBounds(bounds, p, min=min, max=max): The updated bounding rectangle ``(xMin, yMin, xMax, yMax)``. """ (x, y) = p + if bounds is None: + return x, y, x, y xMin, yMin, xMax, yMax = bounds return min(xMin, x), min(yMin, y), max(xMax, x), max(yMax, y) + def pointInRect(p, rect): """Test if a point is inside a bounding rectangle. @@ -72,6 +76,7 @@ def pointInRect(p, rect): xMin, yMin, xMax, yMax = rect return (xMin <= x <= xMax) and (yMin <= y <= yMax) + def pointsInRect(array, rect): """Determine which points are inside a bounding rectangle. @@ -88,6 +93,7 @@ def pointsInRect(array, rect): xMin, yMin, xMax, yMax = rect return [(xMin <= x <= xMax) and (yMin <= y <= yMax) for x, y in array] + def vectorLength(vector): """Calculate the length of the given vector. @@ -100,6 +106,7 @@ def vectorLength(vector): x, y = vector return math.sqrt(x**2 + y**2) + def asInt16(array): """Round a list of floats to 16-bit signed integers. @@ -109,7 +116,7 @@ def asInt16(array): Returns: A list of rounded integers. """ - return [int(math.floor(i+0.5)) for i in array] + return [int(math.floor(i + 0.5)) for i in array] def normRect(rect): @@ -130,6 +137,7 @@ def normRect(rect): (xMin, yMin, xMax, yMax) = rect return min(xMin, xMax), min(yMin, yMax), max(xMin, xMax), max(yMin, yMax) + def scaleRect(rect, x, y): """Scale a bounding box rectangle. @@ -145,6 +153,7 @@ def scaleRect(rect, x, y): (xMin, yMin, xMax, yMax) = rect return xMin * x, yMin * y, xMax * x, yMax * y + def offsetRect(rect, dx, dy): """Offset a bounding box rectangle. @@ -158,7 +167,8 @@ def offsetRect(rect, dx, dy): An offset bounding rectangle. """ (xMin, yMin, xMax, yMax) = rect - return xMin+dx, yMin+dy, xMax+dx, yMax+dy + return xMin + dx, yMin + dy, xMax + dx, yMax + dy + def insetRect(rect, dx, dy): """Inset a bounding box rectangle on all sides. @@ -173,7 +183,8 @@ def insetRect(rect, dx, dy): An inset bounding rectangle. """ (xMin, yMin, xMax, yMax) = rect - return xMin+dx, yMin+dy, xMax-dx, yMax-dy + return xMin + dx, yMin + dy, xMax - dx, yMax - dy + def sectRect(rect1, rect2): """Test for rectangle-rectangle intersection. @@ -191,12 +202,17 @@ def sectRect(rect1, rect2): """ (xMin1, yMin1, xMax1, yMax1) = rect1 (xMin2, yMin2, xMax2, yMax2) = rect2 - xMin, yMin, xMax, yMax = (max(xMin1, xMin2), max(yMin1, yMin2), - min(xMax1, xMax2), min(yMax1, yMax2)) + xMin, yMin, xMax, yMax = ( + max(xMin1, xMin2), + max(yMin1, yMin2), + min(xMax1, xMax2), + min(yMax1, yMax2), + ) if xMin >= xMax or yMin >= yMax: return False, (0, 0, 0, 0) return True, (xMin, yMin, xMax, yMax) + def unionRect(rect1, rect2): """Determine union of bounding rectangles. @@ -211,10 +227,15 @@ def unionRect(rect1, rect2): """ (xMin1, yMin1, xMax1, yMax1) = rect1 (xMin2, yMin2, xMax2, yMax2) = rect2 - xMin, yMin, xMax, yMax = (min(xMin1, xMin2), min(yMin1, yMin2), - max(xMax1, xMax2), max(yMax1, yMax2)) + xMin, yMin, xMax, yMax = ( + min(xMin1, xMin2), + min(yMin1, yMin2), + max(xMax1, xMax2), + max(yMax1, yMax2), + ) return (xMin, yMin, xMax, yMax) + def rectCenter(rect): """Determine rectangle center. @@ -226,7 +247,8 @@ def rectCenter(rect): A 2D tuple representing the point at the center of the rectangle. """ (xMin, yMin, xMax, yMax) = rect - return (xMin+xMax)/2, (yMin+yMax)/2 + return (xMin + xMax) / 2, (yMin + yMax) / 2 + def rectArea(rect): """Determine rectangle area. @@ -241,6 +263,7 @@ def rectArea(rect): (xMin, yMin, xMax, yMax) = rect return (yMax - yMin) * (xMax - xMin) + def intRect(rect): """Round a rectangle to integer values. @@ -261,8 +284,28 @@ def intRect(rect): return (xMin, yMin, xMax, yMax) -class Vector(_Vector): +def quantizeRect(rect, factor=1): + """ + >>> bounds = (72.3, -218.4, 1201.3, 919.1) + >>> quantizeRect(bounds) + (72, -219, 1202, 920) + >>> quantizeRect(bounds, factor=10) + (70, -220, 1210, 920) + >>> quantizeRect(bounds, factor=100) + (0, -300, 1300, 1000) + """ + if factor < 1: + raise ValueError(f"Expected quantization factor >= 1, found: {factor!r}") + xMin, yMin, xMax, yMax = normRect(rect) + return ( + int(math.floor(xMin / factor) * factor), + int(math.floor(yMin / factor) * factor), + int(math.ceil(xMax / factor) * factor), + int(math.ceil(yMax / factor) * factor), + ) + +class Vector(_Vector): def __init__(self, *args, **kwargs): warnings.warn( "fontTools.misc.arrayTools.Vector has been deprecated, please use " @@ -373,7 +416,9 @@ def _test(): (0, 2, 4, 5) """ + if __name__ == "__main__": import sys import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/bezierTools.py b/Lib/fontTools/misc/bezierTools.py index 25e5c548..21ab0a5d 100644 --- a/Lib/fontTools/misc/bezierTools.py +++ b/Lib/fontTools/misc/bezierTools.py @@ -7,6 +7,17 @@ from fontTools.misc.transform import Identity import math from collections import namedtuple +try: + import cython + + COMPILED = cython.compiled +except (AttributeError, ImportError): + # if cython not installed, use mock module with no-op decorators and types + from fontTools.misc import cython + + COMPILED = False + + Intersection = namedtuple("Intersection", ["pt", "t1", "t2"]) @@ -26,10 +37,13 @@ __all__ = [ "splitCubic", "splitQuadraticAtT", "splitCubicAtT", + "splitCubicAtTC", + "splitCubicIntoTwoAtTC", "solveQuadratic", "solveCubic", "quadraticPointAtT", "cubicPointAtT", + "cubicPointAtTC", "linePointAtT", "segmentPointAtT", "lineLineIntersections", @@ -67,6 +81,14 @@ def _split_cubic_into_two(p0, p1, p2, p3): ) +@cython.returns(cython.double) +@cython.locals( + p0=cython.complex, + p1=cython.complex, + p2=cython.complex, + p3=cython.complex, +) +@cython.locals(mult=cython.double, arch=cython.double, box=cython.double) def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): arch = abs(p0 - p3) box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3) @@ -79,6 +101,17 @@ def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): ) +@cython.returns(cython.double) +@cython.locals( + pt1=cython.complex, + pt2=cython.complex, + pt3=cython.complex, + pt4=cython.complex, +) +@cython.locals( + tolerance=cython.double, + mult=cython.double, +) def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005): """Calculates the arc length for a cubic Bezier segment. @@ -97,14 +130,22 @@ epsilonDigits = 6 epsilon = 1e-10 +@cython.cfunc +@cython.inline +@cython.returns(cython.double) +@cython.locals(v1=cython.complex, v2=cython.complex) def _dot(v1, v2): return (v1 * v2.conjugate()).real +@cython.cfunc +@cython.inline +@cython.returns(cython.double) +@cython.locals(x=cython.double) def _intSecAtan(x): # In : sympy.integrate(sp.sec(sp.atan(x))) # Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2 - return x * math.sqrt(x ** 2 + 1) / 2 + math.asinh(x) / 2 + return x * math.sqrt(x**2 + 1) / 2 + math.asinh(x) / 2 def calcQuadraticArcLength(pt1, pt2, pt3): @@ -142,6 +183,25 @@ def calcQuadraticArcLength(pt1, pt2, pt3): return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) +@cython.returns(cython.double) +@cython.locals( + pt1=cython.complex, + pt2=cython.complex, + pt3=cython.complex, + d0=cython.complex, + d1=cython.complex, + d=cython.complex, + n=cython.complex, +) +@cython.locals( + scale=cython.double, + origDist=cython.double, + a=cython.double, + b=cython.double, + x0=cython.double, + x1=cython.double, + Len=cython.double, +) def calcQuadraticArcLengthC(pt1, pt2, pt3): """Calculates the arc length for a quadratic Bezier segment. @@ -154,7 +214,7 @@ def calcQuadraticArcLengthC(pt1, pt2, pt3): Arc length value. """ # Analytical solution to the length of a quadratic bezier. - # I'll explain how I arrived at this later. + # Documentation: https://github.com/fonttools/fonttools/issues/3055 d0 = pt2 - pt1 d1 = pt3 - pt2 d = d1 - d0 @@ -191,6 +251,17 @@ def approximateQuadraticArcLength(pt1, pt2, pt3): return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) +@cython.returns(cython.double) +@cython.locals( + pt1=cython.complex, + pt2=cython.complex, + pt3=cython.complex, +) +@cython.locals( + v0=cython.double, + v1=cython.double, + v2=cython.double, +) def approximateQuadraticArcLengthC(pt1, pt2, pt3): """Calculates the arc length for a quadratic Bezier segment. @@ -288,6 +359,20 @@ def approximateCubicArcLength(pt1, pt2, pt3, pt4): ) +@cython.returns(cython.double) +@cython.locals( + pt1=cython.complex, + pt2=cython.complex, + pt3=cython.complex, + pt4=cython.complex, +) +@cython.locals( + v0=cython.double, + v1=cython.double, + v2=cython.double, + v3=cython.double, + v4=cython.double, +) def approximateCubicArcLengthC(pt1, pt2, pt3, pt4): """Approximates the arc length for a cubic Bezier segment. @@ -549,6 +634,70 @@ def splitCubicAtT(pt1, pt2, pt3, pt4, *ts): return _splitCubicAtT(a, b, c, d, *ts) +@cython.locals( + pt1=cython.complex, + pt2=cython.complex, + pt3=cython.complex, + pt4=cython.complex, + a=cython.complex, + b=cython.complex, + c=cython.complex, + d=cython.complex, +) +def splitCubicAtTC(pt1, pt2, pt3, pt4, *ts): + """Split a cubic Bezier curve at one or more values of t. + + Args: + pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers.. + *ts: Positions at which to split the curve. + + Yields: + Curve segments (each curve segment being four complex numbers). + """ + a, b, c, d = calcCubicParametersC(pt1, pt2, pt3, pt4) + yield from _splitCubicAtTC(a, b, c, d, *ts) + + +@cython.returns(cython.complex) +@cython.locals( + t=cython.double, + pt1=cython.complex, + pt2=cython.complex, + pt3=cython.complex, + pt4=cython.complex, + pointAtT=cython.complex, + off1=cython.complex, + off2=cython.complex, +) +@cython.locals( + t2=cython.double, _1_t=cython.double, _1_t_2=cython.double, _2_t_1_t=cython.double +) +def splitCubicIntoTwoAtTC(pt1, pt2, pt3, pt4, t): + """Split a cubic Bezier curve at t. + + Args: + pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers. + t: Position at which to split the curve. + + Returns: + A tuple of two curve segments (each curve segment being four complex numbers). + """ + t2 = t * t + _1_t = 1 - t + _1_t_2 = _1_t * _1_t + _2_t_1_t = 2 * t * _1_t + pointAtT = ( + _1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4 + ) + off1 = _1_t_2 * pt1 + _2_t_1_t * pt2 + t2 * pt3 + off2 = _1_t_2 * pt2 + _2_t_1_t * pt3 + t2 * pt4 + + pt2 = pt1 + (pt2 - pt1) * t + pt3 = pt4 + (pt3 - pt4) * _1_t + + return ((pt1, pt2, off1, pointAtT), (pointAtT, off2, pt3, pt4)) + + def _splitQuadraticAtT(a, b, c, *ts): ts = list(ts) segments = [] @@ -611,6 +760,44 @@ def _splitCubicAtT(a, b, c, d, *ts): return segments +@cython.locals( + a=cython.complex, + b=cython.complex, + c=cython.complex, + d=cython.complex, + t1=cython.double, + t2=cython.double, + delta=cython.double, + delta_2=cython.double, + delta_3=cython.double, + a1=cython.complex, + b1=cython.complex, + c1=cython.complex, + d1=cython.complex, +) +def _splitCubicAtTC(a, b, c, d, *ts): + ts = list(ts) + ts.insert(0, 0.0) + ts.append(1.0) + for i in range(len(ts) - 1): + t1 = ts[i] + t2 = ts[i + 1] + delta = t2 - t1 + + delta_2 = delta * delta + delta_3 = delta * delta_2 + t1_2 = t1 * t1 + t1_3 = t1 * t1_2 + + # calc new a, b, c and d + a1 = a * delta_3 + b1 = (3 * a * t1 + b) * delta_2 + c1 = (2 * b * t1 + c + 3 * a * t1_2) * delta + d1 = a * t1_3 + b * t1_2 + c * t1 + d + pt1, pt2, pt3, pt4 = calcCubicPointsC(a1, b1, c1, d1) + yield (pt1, pt2, pt3, pt4) + + # # Equation solvers. # @@ -773,6 +960,24 @@ def calcCubicParameters(pt1, pt2, pt3, pt4): return (ax, ay), (bx, by), (cx, cy), (dx, dy) +@cython.cfunc +@cython.inline +@cython.locals( + pt1=cython.complex, + pt2=cython.complex, + pt3=cython.complex, + pt4=cython.complex, + a=cython.complex, + b=cython.complex, + c=cython.complex, +) +def calcCubicParametersC(pt1, pt2, pt3, pt4): + c = (pt2 - pt1) * 3.0 + b = (pt3 - pt2) * 3.0 - c + a = pt4 - pt1 - c - b + return (a, b, c, pt1) + + def calcQuadraticPoints(a, b, c): ax, ay = a bx, by = b @@ -802,6 +1007,24 @@ def calcCubicPoints(a, b, c, d): return (x1, y1), (x2, y2), (x3, y3), (x4, y4) +@cython.cfunc +@cython.inline +@cython.locals( + a=cython.complex, + b=cython.complex, + c=cython.complex, + d=cython.complex, + p2=cython.complex, + p3=cython.complex, + p4=cython.complex, +) +def calcCubicPointsC(a, b, c, d): + p2 = c * (1 / 3) + d + p3 = (b + c) * (1 / 3) + p2 + p4 = a + b + c + d + return (d, p2, p3, p4) + + # # Point at time # @@ -845,21 +1068,47 @@ def cubicPointAtT(pt1, pt2, pt3, pt4, t): Returns: A 2D tuple with the coordinates of the point. """ + t2 = t * t + _1_t = 1 - t + _1_t_2 = _1_t * _1_t x = ( - (1 - t) * (1 - t) * (1 - t) * pt1[0] - + 3 * (1 - t) * (1 - t) * t * pt2[0] - + 3 * (1 - t) * t * t * pt3[0] - + t * t * t * pt4[0] + _1_t_2 * _1_t * pt1[0] + + 3 * (_1_t_2 * t * pt2[0] + _1_t * t2 * pt3[0]) + + t2 * t * pt4[0] ) y = ( - (1 - t) * (1 - t) * (1 - t) * pt1[1] - + 3 * (1 - t) * (1 - t) * t * pt2[1] - + 3 * (1 - t) * t * t * pt3[1] - + t * t * t * pt4[1] + _1_t_2 * _1_t * pt1[1] + + 3 * (_1_t_2 * t * pt2[1] + _1_t * t2 * pt3[1]) + + t2 * t * pt4[1] ) return (x, y) +@cython.returns(cython.complex) +@cython.locals( + t=cython.double, + pt1=cython.complex, + pt2=cython.complex, + pt3=cython.complex, + pt4=cython.complex, +) +@cython.locals(t2=cython.double, _1_t=cython.double, _1_t_2=cython.double) +def cubicPointAtTC(pt1, pt2, pt3, pt4, t): + """Finds the point at time `t` on a cubic curve. + + Args: + pt1, pt2, pt3, pt4: Coordinates of the curve as complex numbers. + t: The time along the curve. + + Returns: + A complex number with the coordinates of the point. + """ + t2 = t * t + _1_t = 1 - t + _1_t_2 = _1_t * _1_t + return _1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4 + + def segmentPointAtT(seg, t): if len(seg) == 2: return linePointAtT(*seg, t) diff --git a/Lib/fontTools/misc/classifyTools.py b/Lib/fontTools/misc/classifyTools.py index ae88a8f7..2235bbd7 100644 --- a/Lib/fontTools/misc/classifyTools.py +++ b/Lib/fontTools/misc/classifyTools.py @@ -4,168 +4,168 @@ class Classifier(object): - """ - Main Classifier object, used to classify things into similar sets. - """ - - def __init__(self, sort=True): - - self._things = set() # set of all things known so far - self._sets = [] # list of class sets produced so far - self._mapping = {} # map from things to their class set - self._dirty = False - self._sort = sort - - def add(self, set_of_things): - """ - Add a set to the classifier. Any iterable is accepted. - """ - if not set_of_things: - return - - self._dirty = True - - things, sets, mapping = self._things, self._sets, self._mapping - - s = set(set_of_things) - intersection = s.intersection(things) # existing things - s.difference_update(intersection) # new things - difference = s - del s - - # Add new class for new things - if difference: - things.update(difference) - sets.append(difference) - for thing in difference: - mapping[thing] = difference - del difference - - while intersection: - # Take one item and process the old class it belongs to - old_class = mapping[next(iter(intersection))] - old_class_intersection = old_class.intersection(intersection) - - # Update old class to remove items from new set - old_class.difference_update(old_class_intersection) - - # Remove processed items from todo list - intersection.difference_update(old_class_intersection) - - # Add new class for the intersection with old class - sets.append(old_class_intersection) - for thing in old_class_intersection: - mapping[thing] = old_class_intersection - del old_class_intersection - - def update(self, list_of_sets): - """ - Add a a list of sets to the classifier. Any iterable of iterables is accepted. - """ - for s in list_of_sets: - self.add(s) - - def _process(self): - if not self._dirty: - return - - # Do any deferred processing - sets = self._sets - self._sets = [s for s in sets if s] - - if self._sort: - self._sets = sorted(self._sets, key=lambda s: (-len(s), sorted(s))) - - self._dirty = False - - # Output methods - - def getThings(self): - """Returns the set of all things known so far. - - The return value belongs to the Classifier object and should NOT - be modified while the classifier is still in use. - """ - self._process() - return self._things - - def getMapping(self): - """Returns the mapping from things to their class set. - - The return value belongs to the Classifier object and should NOT - be modified while the classifier is still in use. - """ - self._process() - return self._mapping - - def getClasses(self): - """Returns the list of class sets. - - The return value belongs to the Classifier object and should NOT - be modified while the classifier is still in use. - """ - self._process() - return self._sets + """ + Main Classifier object, used to classify things into similar sets. + """ + + def __init__(self, sort=True): + self._things = set() # set of all things known so far + self._sets = [] # list of class sets produced so far + self._mapping = {} # map from things to their class set + self._dirty = False + self._sort = sort + + def add(self, set_of_things): + """ + Add a set to the classifier. Any iterable is accepted. + """ + if not set_of_things: + return + + self._dirty = True + + things, sets, mapping = self._things, self._sets, self._mapping + + s = set(set_of_things) + intersection = s.intersection(things) # existing things + s.difference_update(intersection) # new things + difference = s + del s + + # Add new class for new things + if difference: + things.update(difference) + sets.append(difference) + for thing in difference: + mapping[thing] = difference + del difference + + while intersection: + # Take one item and process the old class it belongs to + old_class = mapping[next(iter(intersection))] + old_class_intersection = old_class.intersection(intersection) + + # Update old class to remove items from new set + old_class.difference_update(old_class_intersection) + + # Remove processed items from todo list + intersection.difference_update(old_class_intersection) + + # Add new class for the intersection with old class + sets.append(old_class_intersection) + for thing in old_class_intersection: + mapping[thing] = old_class_intersection + del old_class_intersection + + def update(self, list_of_sets): + """ + Add a a list of sets to the classifier. Any iterable of iterables is accepted. + """ + for s in list_of_sets: + self.add(s) + + def _process(self): + if not self._dirty: + return + + # Do any deferred processing + sets = self._sets + self._sets = [s for s in sets if s] + + if self._sort: + self._sets = sorted(self._sets, key=lambda s: (-len(s), sorted(s))) + + self._dirty = False + + # Output methods + + def getThings(self): + """Returns the set of all things known so far. + + The return value belongs to the Classifier object and should NOT + be modified while the classifier is still in use. + """ + self._process() + return self._things + + def getMapping(self): + """Returns the mapping from things to their class set. + + The return value belongs to the Classifier object and should NOT + be modified while the classifier is still in use. + """ + self._process() + return self._mapping + + def getClasses(self): + """Returns the list of class sets. + + The return value belongs to the Classifier object and should NOT + be modified while the classifier is still in use. + """ + self._process() + return self._sets def classify(list_of_sets, sort=True): - """ - Takes a iterable of iterables (list of sets from here on; but any - iterable works.), and returns the smallest list of sets such that - each set, is either a subset, or is disjoint from, each of the input - sets. - - In other words, this function classifies all the things present in - any of the input sets, into similar classes, based on which sets - things are a member of. - - If sort=True, return class sets are sorted by decreasing size and - their natural sort order within each class size. Otherwise, class - sets are returned in the order that they were identified, which is - generally not significant. - - >>> classify([]) == ([], {}) - True - >>> classify([[]]) == ([], {}) - True - >>> classify([[], []]) == ([], {}) - True - >>> classify([[1]]) == ([{1}], {1: {1}}) - True - >>> classify([[1,2]]) == ([{1, 2}], {1: {1, 2}, 2: {1, 2}}) - True - >>> classify([[1],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) - True - >>> classify([[1,2],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) - True - >>> classify([[1,2],[2,4]]) == ([{1}, {2}, {4}], {1: {1}, 2: {2}, 4: {4}}) - True - >>> classify([[1,2],[2,4,5]]) == ( - ... [{4, 5}, {1}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) - True - >>> classify([[1,2],[2,4,5]], sort=False) == ( - ... [{1}, {4, 5}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) - True - >>> classify([[1,2,9],[2,4,5]], sort=False) == ( - ... [{1, 9}, {4, 5}, {2}], {1: {1, 9}, 2: {2}, 4: {4, 5}, 5: {4, 5}, - ... 9: {1, 9}}) - True - >>> classify([[1,2,9,15],[2,4,5]], sort=False) == ( - ... [{1, 9, 15}, {4, 5}, {2}], {1: {1, 9, 15}, 2: {2}, 4: {4, 5}, - ... 5: {4, 5}, 9: {1, 9, 15}, 15: {1, 9, 15}}) - True - >>> classes, mapping = classify([[1,2,9,15],[2,4,5],[15,5]], sort=False) - >>> set([frozenset(c) for c in classes]) == set( - ... [frozenset(s) for s in ({1, 9}, {4}, {2}, {5}, {15})]) - True - >>> mapping == {1: {1, 9}, 2: {2}, 4: {4}, 5: {5}, 9: {1, 9}, 15: {15}} - True - """ - classifier = Classifier(sort=sort) - classifier.update(list_of_sets) - return classifier.getClasses(), classifier.getMapping() + """ + Takes a iterable of iterables (list of sets from here on; but any + iterable works.), and returns the smallest list of sets such that + each set, is either a subset, or is disjoint from, each of the input + sets. + + In other words, this function classifies all the things present in + any of the input sets, into similar classes, based on which sets + things are a member of. + + If sort=True, return class sets are sorted by decreasing size and + their natural sort order within each class size. Otherwise, class + sets are returned in the order that they were identified, which is + generally not significant. + + >>> classify([]) == ([], {}) + True + >>> classify([[]]) == ([], {}) + True + >>> classify([[], []]) == ([], {}) + True + >>> classify([[1]]) == ([{1}], {1: {1}}) + True + >>> classify([[1,2]]) == ([{1, 2}], {1: {1, 2}, 2: {1, 2}}) + True + >>> classify([[1],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) + True + >>> classify([[1,2],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) + True + >>> classify([[1,2],[2,4]]) == ([{1}, {2}, {4}], {1: {1}, 2: {2}, 4: {4}}) + True + >>> classify([[1,2],[2,4,5]]) == ( + ... [{4, 5}, {1}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) + True + >>> classify([[1,2],[2,4,5]], sort=False) == ( + ... [{1}, {4, 5}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) + True + >>> classify([[1,2,9],[2,4,5]], sort=False) == ( + ... [{1, 9}, {4, 5}, {2}], {1: {1, 9}, 2: {2}, 4: {4, 5}, 5: {4, 5}, + ... 9: {1, 9}}) + True + >>> classify([[1,2,9,15],[2,4,5]], sort=False) == ( + ... [{1, 9, 15}, {4, 5}, {2}], {1: {1, 9, 15}, 2: {2}, 4: {4, 5}, + ... 5: {4, 5}, 9: {1, 9, 15}, 15: {1, 9, 15}}) + True + >>> classes, mapping = classify([[1,2,9,15],[2,4,5],[15,5]], sort=False) + >>> set([frozenset(c) for c in classes]) == set( + ... [frozenset(s) for s in ({1, 9}, {4}, {2}, {5}, {15})]) + True + >>> mapping == {1: {1, 9}, 2: {2}, 4: {4}, 5: {5}, 9: {1, 9}, 15: {15}} + True + """ + classifier = Classifier(sort=sort) + classifier.update(list_of_sets) + return classifier.getClasses(), classifier.getMapping() if __name__ == "__main__": - import sys, doctest - sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) + import sys, doctest + + sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) diff --git a/Lib/fontTools/misc/cliTools.py b/Lib/fontTools/misc/cliTools.py index e7dadf98..8322ea9e 100644 --- a/Lib/fontTools/misc/cliTools.py +++ b/Lib/fontTools/misc/cliTools.py @@ -6,7 +6,9 @@ import re numberAddedRE = re.compile(r"#\d+$") -def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False, suffix=""): +def makeOutputFileName( + input, outputDir=None, extension=None, overWrite=False, suffix="" +): """Generates a suitable file name for writing output. Often tools will want to take a file, do some kind of transformation to it, @@ -44,6 +46,7 @@ def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False, s if not overWrite: while os.path.exists(output): output = os.path.join( - dirName, fileName + suffix + "#" + repr(n) + extension) + dirName, fileName + suffix + "#" + repr(n) + extension + ) n += 1 return output diff --git a/Lib/fontTools/misc/cython.py b/Lib/fontTools/misc/cython.py index 0ba659f6..2a42d94a 100644 --- a/Lib/fontTools/misc/cython.py +++ b/Lib/fontTools/misc/cython.py @@ -10,9 +10,11 @@ We only define the symbols that we use. E.g. see fontTools.cu2qu from types import SimpleNamespace + def _empty_decorator(x): return x + compiled = False for name in ("double", "complex", "int"): diff --git a/Lib/fontTools/misc/dictTools.py b/Lib/fontTools/misc/dictTools.py index ae7932c9..e3c0df73 100644 --- a/Lib/fontTools/misc/dictTools.py +++ b/Lib/fontTools/misc/dictTools.py @@ -1,7 +1,8 @@ """Misc dict tools.""" -__all__ = ['hashdict'] +__all__ = ["hashdict"] + # https://stackoverflow.com/questions/1151658/python-hashable-dicts class hashdict(dict): @@ -26,36 +27,54 @@ class hashdict(dict): http://stackoverflow.com/questions/1151658/python-hashable-dicts """ + def __key(self): return tuple(sorted(self.items())) + def __repr__(self): - return "{0}({1})".format(self.__class__.__name__, - ", ".join("{0}={1}".format( - str(i[0]),repr(i[1])) for i in self.__key())) + return "{0}({1})".format( + self.__class__.__name__, + ", ".join("{0}={1}".format(str(i[0]), repr(i[1])) for i in self.__key()), + ) def __hash__(self): return hash(self.__key()) + def __setitem__(self, key, value): - raise TypeError("{0} does not support item assignment" - .format(self.__class__.__name__)) + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + def __delitem__(self, key): - raise TypeError("{0} does not support item assignment" - .format(self.__class__.__name__)) + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + def clear(self): - raise TypeError("{0} does not support item assignment" - .format(self.__class__.__name__)) + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + def pop(self, *args, **kwargs): - raise TypeError("{0} does not support item assignment" - .format(self.__class__.__name__)) + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + def popitem(self, *args, **kwargs): - raise TypeError("{0} does not support item assignment" - .format(self.__class__.__name__)) + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + def setdefault(self, *args, **kwargs): - raise TypeError("{0} does not support item assignment" - .format(self.__class__.__name__)) + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + def update(self, *args, **kwargs): - raise TypeError("{0} does not support item assignment" - .format(self.__class__.__name__)) + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + # update is not ok because it mutates the object # __add__ is ok because it creates a new object # while the new object is under construction, it's ok to mutate it @@ -63,4 +82,3 @@ class hashdict(dict): result = hashdict(self) dict.update(result, right) return result - diff --git a/Lib/fontTools/misc/eexec.py b/Lib/fontTools/misc/eexec.py index d1d4bb6a..cafa312c 100644 --- a/Lib/fontTools/misc/eexec.py +++ b/Lib/fontTools/misc/eexec.py @@ -16,98 +16,104 @@ from fontTools.misc.textTools import bytechr, bytesjoin, byteord def _decryptChar(cipher, R): - cipher = byteord(cipher) - plain = ( (cipher ^ (R>>8)) ) & 0xFF - R = ( (cipher + R) * 52845 + 22719 ) & 0xFFFF - return bytechr(plain), R + cipher = byteord(cipher) + plain = ((cipher ^ (R >> 8))) & 0xFF + R = ((cipher + R) * 52845 + 22719) & 0xFFFF + return bytechr(plain), R + def _encryptChar(plain, R): - plain = byteord(plain) - cipher = ( (plain ^ (R>>8)) ) & 0xFF - R = ( (cipher + R) * 52845 + 22719 ) & 0xFFFF - return bytechr(cipher), R + plain = byteord(plain) + cipher = ((plain ^ (R >> 8))) & 0xFF + R = ((cipher + R) * 52845 + 22719) & 0xFFFF + return bytechr(cipher), R def decrypt(cipherstring, R): - r""" - 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: - plain, R = _decryptChar(cipher, R) - plainList.append(plain) - plainstring = bytesjoin(plainList) - return plainstring, int(R) + r""" + 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: + plain, R = _decryptChar(cipher, R) + plainList.append(plain) + plainstring = bytesjoin(plainList) + return plainstring, int(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" - True - >>> R == 36142 - True - """ - cipherList = [] - for plain in plainstring: - cipher, R = _encryptChar(plain, R) - cipherList.append(cipher) - cipherstring = bytesjoin(cipherList) - return cipherstring, int(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" + True + >>> R == 36142 + True + """ + cipherList = [] + for plain in plainstring: + cipher, R = _encryptChar(plain, R) + cipherList.append(cipher) + cipherstring = bytesjoin(cipherList) + return cipherstring, int(R) def hexString(s): - import binascii - return binascii.hexlify(s) + import binascii + + return binascii.hexlify(s) + def deHexString(h): - import binascii - h = bytesjoin(h.split()) - return binascii.unhexlify(h) + import binascii + + h = bytesjoin(h.split()) + return binascii.unhexlify(h) if __name__ == "__main__": - import sys - import doctest - sys.exit(doctest.testmod().failed) + import sys + import doctest + + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/encodingTools.py b/Lib/fontTools/misc/encodingTools.py index eccf951d..3b2651d3 100644 --- a/Lib/fontTools/misc/encodingTools.py +++ b/Lib/fontTools/misc/encodingTools.py @@ -5,67 +5,68 @@ import fontTools.encodings.codecs # Map keyed by platformID, then platEncID, then possibly langID _encodingMap = { - 0: { # Unicode - 0: 'utf_16_be', - 1: 'utf_16_be', - 2: 'utf_16_be', - 3: 'utf_16_be', - 4: 'utf_16_be', - 5: 'utf_16_be', - 6: 'utf_16_be', - }, - 1: { # Macintosh - # See - # https://github.com/fonttools/fonttools/issues/236 - 0: { # Macintosh, platEncID==0, keyed by langID - 15: "mac_iceland", - 17: "mac_turkish", - 18: "mac_croatian", - 24: "mac_latin2", - 25: "mac_latin2", - 26: "mac_latin2", - 27: "mac_latin2", - 28: "mac_latin2", - 36: "mac_latin2", - 37: "mac_romanian", - 38: "mac_latin2", - 39: "mac_latin2", - 40: "mac_latin2", - Ellipsis: 'mac_roman', # Other - }, - 1: 'x_mac_japanese_ttx', - 2: 'x_mac_trad_chinese_ttx', - 3: 'x_mac_korean_ttx', - 6: 'mac_greek', - 7: 'mac_cyrillic', - 25: 'x_mac_simp_chinese_ttx', - 29: 'mac_latin2', - 35: 'mac_turkish', - 37: 'mac_iceland', - }, - 2: { # ISO - 0: 'ascii', - 1: 'utf_16_be', - 2: 'latin1', - }, - 3: { # Microsoft - 0: 'utf_16_be', - 1: 'utf_16_be', - 2: 'shift_jis', - 3: 'gb2312', - 4: 'big5', - 5: 'euc_kr', - 6: 'johab', - 10: 'utf_16_be', - }, + 0: { # Unicode + 0: "utf_16_be", + 1: "utf_16_be", + 2: "utf_16_be", + 3: "utf_16_be", + 4: "utf_16_be", + 5: "utf_16_be", + 6: "utf_16_be", + }, + 1: { # Macintosh + # See + # https://github.com/fonttools/fonttools/issues/236 + 0: { # Macintosh, platEncID==0, keyed by langID + 15: "mac_iceland", + 17: "mac_turkish", + 18: "mac_croatian", + 24: "mac_latin2", + 25: "mac_latin2", + 26: "mac_latin2", + 27: "mac_latin2", + 28: "mac_latin2", + 36: "mac_latin2", + 37: "mac_romanian", + 38: "mac_latin2", + 39: "mac_latin2", + 40: "mac_latin2", + Ellipsis: "mac_roman", # Other + }, + 1: "x_mac_japanese_ttx", + 2: "x_mac_trad_chinese_ttx", + 3: "x_mac_korean_ttx", + 6: "mac_greek", + 7: "mac_cyrillic", + 25: "x_mac_simp_chinese_ttx", + 29: "mac_latin2", + 35: "mac_turkish", + 37: "mac_iceland", + }, + 2: { # ISO + 0: "ascii", + 1: "utf_16_be", + 2: "latin1", + }, + 3: { # Microsoft + 0: "utf_16_be", + 1: "utf_16_be", + 2: "shift_jis", + 3: "gb2312", + 4: "big5", + 5: "euc_kr", + 6: "johab", + 10: "utf_16_be", + }, } + def getEncoding(platformID, platEncID, langID, default=None): - """Returns the Python encoding name for OpenType platformID/encodingID/langID - triplet. If encoding for these values is not known, by default None is - returned. That can be overriden by passing a value to the default argument. - """ - encoding = _encodingMap.get(platformID, {}).get(platEncID, default) - if isinstance(encoding, dict): - encoding = encoding.get(langID, encoding[Ellipsis]) - return encoding + """Returns the Python encoding name for OpenType platformID/encodingID/langID + triplet. If encoding for these values is not known, by default None is + returned. That can be overriden by passing a value to the default argument. + """ + encoding = _encodingMap.get(platformID, {}).get(platEncID, default) + if isinstance(encoding, dict): + encoding = encoding.get(langID, encoding[Ellipsis]) + return encoding diff --git a/Lib/fontTools/misc/etree.py b/Lib/fontTools/misc/etree.py index cd4df365..9d4a65c3 100644 --- a/Lib/fontTools/misc/etree.py +++ b/Lib/fontTools/misc/etree.py @@ -244,7 +244,8 @@ except ImportError: except UnicodeDecodeError: raise ValueError( "Bytes strings can only contain ASCII characters. " - "Use unicode strings for non-ASCII characters.") + "Use unicode strings for non-ASCII characters." + ) except AttributeError: _raise_serialization_error(s) if s and _invalid_xml_string.search(s): @@ -425,9 +426,7 @@ except ImportError: write(_escape_cdata(elem.tail)) def _raise_serialization_error(text): - raise TypeError( - "cannot serialize %r (type %s)" % (text, type(text).__name__) - ) + raise TypeError("cannot serialize %r (type %s)" % (text, type(text).__name__)) def _escape_cdata(text): # escape character data diff --git a/Lib/fontTools/misc/filenames.py b/Lib/fontTools/misc/filenames.py index 0f010008..d279f89c 100644 --- a/Lib/fontTools/misc/filenames.py +++ b/Lib/fontTools/misc/filenames.py @@ -27,216 +27,220 @@ maxFileNameLength = 255 class NameTranslationError(Exception): - pass + pass def userNameToFileName(userName, existing=[], prefix="", suffix=""): - """Converts from a user name to a file name. - - Takes care to avoid illegal characters, reserved file names, ambiguity between - upper- and lower-case characters, and clashes with existing files. - - Args: - userName (str): The input file name. - existing: A case-insensitive list of all existing file names. - prefix: Prefix to be prepended to the file name. - suffix: Suffix to be appended to the file name. - - Returns: - A suitable filename. - - Raises: - NameTranslationError: If no suitable name could be generated. - - Examples:: - - >>> userNameToFileName("a") == "a" - True - >>> userNameToFileName("A") == "A_" - True - >>> userNameToFileName("AE") == "A_E_" - True - >>> userNameToFileName("Ae") == "A_e" - True - >>> userNameToFileName("ae") == "ae" - True - >>> userNameToFileName("aE") == "aE_" - True - >>> userNameToFileName("a.alt") == "a.alt" - True - >>> userNameToFileName("A.alt") == "A_.alt" - True - >>> userNameToFileName("A.Alt") == "A_.A_lt" - True - >>> userNameToFileName("A.aLt") == "A_.aL_t" - True - >>> userNameToFileName(u"A.alT") == "A_.alT_" - True - >>> userNameToFileName("T_H") == "T__H_" - True - >>> userNameToFileName("T_h") == "T__h" - True - >>> userNameToFileName("t_h") == "t_h" - True - >>> userNameToFileName("F_F_I") == "F__F__I_" - True - >>> userNameToFileName("f_f_i") == "f_f_i" - True - >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" - True - >>> userNameToFileName(".notdef") == "_notdef" - True - >>> userNameToFileName("con") == "_con" - True - >>> userNameToFileName("CON") == "C_O_N_" - True - >>> userNameToFileName("con.alt") == "_con.alt" - True - >>> userNameToFileName("alt.con") == "alt._con" - True - """ - # the incoming name must be a str - if not isinstance(userName, str): - raise ValueError("The value for userName must be a string.") - # establish the prefix and suffix lengths - prefixLength = len(prefix) - suffixLength = len(suffix) - # replace an initial period with an _ - # if no prefix is to be added - if not prefix and userName[0] == ".": - userName = "_" + userName[1:] - # filter the user name - filteredUserName = [] - for character in userName: - # replace illegal characters with _ - if character in illegalCharacters: - character = "_" - # add _ to all non-lower characters - elif character != character.lower(): - character += "_" - filteredUserName.append(character) - userName = "".join(filteredUserName) - # clip to 255 - sliceLength = maxFileNameLength - prefixLength - suffixLength - userName = userName[:sliceLength] - # test for illegal files names - parts = [] - for part in userName.split("."): - if part.lower() in reservedFileNames: - part = "_" + part - parts.append(part) - userName = ".".join(parts) - # test for clash - fullName = prefix + userName + suffix - if fullName.lower() in existing: - fullName = handleClash1(userName, existing, prefix, suffix) - # finished - return fullName + """Converts from a user name to a file name. + + Takes care to avoid illegal characters, reserved file names, ambiguity between + upper- and lower-case characters, and clashes with existing files. + + Args: + userName (str): The input file name. + existing: A case-insensitive list of all existing file names. + prefix: Prefix to be prepended to the file name. + suffix: Suffix to be appended to the file name. + + Returns: + A suitable filename. + + Raises: + NameTranslationError: If no suitable name could be generated. + + Examples:: + + >>> userNameToFileName("a") == "a" + True + >>> userNameToFileName("A") == "A_" + True + >>> userNameToFileName("AE") == "A_E_" + True + >>> userNameToFileName("Ae") == "A_e" + True + >>> userNameToFileName("ae") == "ae" + True + >>> userNameToFileName("aE") == "aE_" + True + >>> userNameToFileName("a.alt") == "a.alt" + True + >>> userNameToFileName("A.alt") == "A_.alt" + True + >>> userNameToFileName("A.Alt") == "A_.A_lt" + True + >>> userNameToFileName("A.aLt") == "A_.aL_t" + True + >>> userNameToFileName(u"A.alT") == "A_.alT_" + True + >>> userNameToFileName("T_H") == "T__H_" + True + >>> userNameToFileName("T_h") == "T__h" + True + >>> userNameToFileName("t_h") == "t_h" + True + >>> userNameToFileName("F_F_I") == "F__F__I_" + True + >>> userNameToFileName("f_f_i") == "f_f_i" + True + >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" + True + >>> userNameToFileName(".notdef") == "_notdef" + True + >>> userNameToFileName("con") == "_con" + True + >>> userNameToFileName("CON") == "C_O_N_" + True + >>> userNameToFileName("con.alt") == "_con.alt" + True + >>> userNameToFileName("alt.con") == "alt._con" + True + """ + # the incoming name must be a str + if not isinstance(userName, str): + raise ValueError("The value for userName must be a string.") + # establish the prefix and suffix lengths + prefixLength = len(prefix) + suffixLength = len(suffix) + # replace an initial period with an _ + # if no prefix is to be added + if not prefix and userName[0] == ".": + userName = "_" + userName[1:] + # filter the user name + filteredUserName = [] + for character in userName: + # replace illegal characters with _ + if character in illegalCharacters: + character = "_" + # add _ to all non-lower characters + elif character != character.lower(): + character += "_" + filteredUserName.append(character) + userName = "".join(filteredUserName) + # clip to 255 + sliceLength = maxFileNameLength - prefixLength - suffixLength + userName = userName[:sliceLength] + # test for illegal files names + parts = [] + for part in userName.split("."): + if part.lower() in reservedFileNames: + part = "_" + part + parts.append(part) + userName = ".".join(parts) + # test for clash + fullName = prefix + userName + suffix + if fullName.lower() in existing: + fullName = handleClash1(userName, existing, prefix, suffix) + # finished + return fullName + def handleClash1(userName, existing=[], prefix="", suffix=""): - """ - existing should be a case-insensitive list - of all existing file names. - - >>> prefix = ("0" * 5) + "." - >>> suffix = "." + ("0" * 10) - >>> existing = ["a" * 5] - - >>> e = list(existing) - >>> handleClash1(userName="A" * 5, existing=e, - ... prefix=prefix, suffix=suffix) == ( - ... '00000.AAAAA000000000000001.0000000000') - True - - >>> e = list(existing) - >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) - >>> handleClash1(userName="A" * 5, existing=e, - ... prefix=prefix, suffix=suffix) == ( - ... '00000.AAAAA000000000000002.0000000000') - True - - >>> e = list(existing) - >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) - >>> handleClash1(userName="A" * 5, existing=e, - ... prefix=prefix, suffix=suffix) == ( - ... '00000.AAAAA000000000000001.0000000000') - True - """ - # if the prefix length + user name length + suffix length + 15 is at - # or past the maximum length, silce 15 characters off of the user name - prefixLength = len(prefix) - suffixLength = len(suffix) - if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength: - l = (prefixLength + len(userName) + suffixLength + 15) - sliceLength = maxFileNameLength - l - userName = userName[:sliceLength] - finalName = None - # try to add numbers to create a unique name - counter = 1 - while finalName is None: - name = userName + str(counter).zfill(15) - fullName = prefix + name + suffix - if fullName.lower() not in existing: - finalName = fullName - break - else: - counter += 1 - if counter >= 999999999999999: - break - # if there is a clash, go to the next fallback - if finalName is None: - finalName = handleClash2(existing, prefix, suffix) - # finished - return finalName + """ + existing should be a case-insensitive list + of all existing file names. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = ["a" * 5] + + >>> e = list(existing) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000002.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + """ + # if the prefix length + user name length + suffix length + 15 is at + # or past the maximum length, silce 15 characters off of the user name + prefixLength = len(prefix) + suffixLength = len(suffix) + if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength: + l = prefixLength + len(userName) + suffixLength + 15 + sliceLength = maxFileNameLength - l + userName = userName[:sliceLength] + finalName = None + # try to add numbers to create a unique name + counter = 1 + while finalName is None: + name = userName + str(counter).zfill(15) + fullName = prefix + name + suffix + if fullName.lower() not in existing: + finalName = fullName + break + else: + counter += 1 + if counter >= 999999999999999: + break + # if there is a clash, go to the next fallback + if finalName is None: + finalName = handleClash2(existing, prefix, suffix) + # finished + return finalName + def handleClash2(existing=[], prefix="", suffix=""): - """ - existing should be a case-insensitive list - of all existing file names. - - >>> prefix = ("0" * 5) + "." - >>> suffix = "." + ("0" * 10) - >>> existing = [prefix + str(i) + suffix for i in range(100)] - - >>> e = list(existing) - >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( - ... '00000.100.0000000000') - True - - >>> e = list(existing) - >>> e.remove(prefix + "1" + suffix) - >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( - ... '00000.1.0000000000') - True - - >>> e = list(existing) - >>> e.remove(prefix + "2" + suffix) - >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( - ... '00000.2.0000000000') - True - """ - # calculate the longest possible string - maxLength = maxFileNameLength - len(prefix) - len(suffix) - maxValue = int("9" * maxLength) - # try to find a number - finalName = None - counter = 1 - while finalName is None: - fullName = prefix + str(counter) + suffix - if fullName.lower() not in existing: - finalName = fullName - break - else: - counter += 1 - if counter >= maxValue: - break - # raise an error if nothing has been found - if finalName is None: - raise NameTranslationError("No unique name could be found.") - # finished - return finalName + """ + existing should be a case-insensitive list + of all existing file names. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = [prefix + str(i) + suffix for i in range(100)] + + >>> e = list(existing) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.100.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "1" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.1.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "2" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.2.0000000000') + True + """ + # calculate the longest possible string + maxLength = maxFileNameLength - len(prefix) - len(suffix) + maxValue = int("9" * maxLength) + # try to find a number + finalName = None + counter = 1 + while finalName is None: + fullName = prefix + str(counter) + suffix + if fullName.lower() not in existing: + finalName = fullName + break + else: + counter += 1 + if counter >= maxValue: + break + # raise an error if nothing has been found + if finalName is None: + raise NameTranslationError("No unique name could be found.") + # finished + return finalName + if __name__ == "__main__": - import doctest - import sys - sys.exit(doctest.testmod().failed) + import doctest + import sys + + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/fixedTools.py b/Lib/fontTools/misc/fixedTools.py index 6ec7d06e..33004287 100644 --- a/Lib/fontTools/misc/fixedTools.py +++ b/Lib/fontTools/misc/fixedTools.py @@ -23,16 +23,16 @@ import logging log = logging.getLogger(__name__) __all__ = [ - "MAX_F2DOT14", - "fixedToFloat", - "floatToFixed", - "floatToFixedToFloat", - "floatToFixedToStr", - "fixedToStr", - "strToFixed", - "strToFixedToFloat", - "ensureVersionIsLong", - "versionToFixed", + "MAX_F2DOT14", + "fixedToFloat", + "floatToFixed", + "floatToFixedToFloat", + "floatToFixedToStr", + "fixedToStr", + "strToFixed", + "strToFixedToFloat", + "ensureVersionIsLong", + "versionToFixed", ] @@ -40,212 +40,214 @@ MAX_F2DOT14 = 0x7FFF / (1 << 14) def fixedToFloat(value, precisionBits): - """Converts a fixed-point number to a float given the number of - precision bits. + """Converts a fixed-point number to a float given the number of + precision bits. - Args: - value (int): Number in fixed-point format. - precisionBits (int): Number of precision bits. + Args: + value (int): Number in fixed-point format. + precisionBits (int): Number of precision bits. - Returns: - Floating point value. + Returns: + Floating point value. - Examples:: + Examples:: - >>> import math - >>> f = fixedToFloat(-10139, precisionBits=14) - >>> math.isclose(f, -0.61883544921875) - True - """ - return value / (1 << precisionBits) + >>> import math + >>> f = fixedToFloat(-10139, precisionBits=14) + >>> math.isclose(f, -0.61883544921875) + True + """ + return value / (1 << precisionBits) def floatToFixed(value, precisionBits): - """Converts a float to a fixed-point number given the number of - precision bits. + """Converts a float to a fixed-point number given the number of + precision bits. - Args: - value (float): Floating point value. - precisionBits (int): Number of precision bits. + Args: + value (float): Floating point value. + precisionBits (int): Number of precision bits. - Returns: - int: Fixed-point representation. + Returns: + int: Fixed-point representation. - Examples:: + Examples:: - >>> floatToFixed(-0.61883544921875, precisionBits=14) - -10139 - >>> floatToFixed(-0.61884, precisionBits=14) - -10139 - """ - return otRound(value * (1 << precisionBits)) + >>> floatToFixed(-0.61883544921875, precisionBits=14) + -10139 + >>> floatToFixed(-0.61884, precisionBits=14) + -10139 + """ + return otRound(value * (1 << precisionBits)) def floatToFixedToFloat(value, precisionBits): - """Converts a float to a fixed-point number and back again. + """Converts a float to a fixed-point number and back again. - By converting the float to fixed, rounding it, and converting it back - to float again, this returns a floating point values which is exactly - representable in fixed-point format. + By converting the float to fixed, rounding it, and converting it back + to float again, this returns a floating point values which is exactly + representable in fixed-point format. - Note: this **is** equivalent to ``fixedToFloat(floatToFixed(value))``. + Note: this **is** equivalent to ``fixedToFloat(floatToFixed(value))``. - Args: - value (float): The input floating point value. - precisionBits (int): Number of precision bits. + Args: + value (float): The input floating point value. + precisionBits (int): Number of precision bits. - Returns: - float: The transformed and rounded value. + Returns: + float: The transformed and rounded value. - Examples:: - >>> import math - >>> f1 = -0.61884 - >>> f2 = floatToFixedToFloat(-0.61884, precisionBits=14) - >>> f1 != f2 - True - >>> math.isclose(f2, -0.61883544921875) - True - """ - scale = 1 << precisionBits - return otRound(value * scale) / scale + Examples:: + >>> import math + >>> f1 = -0.61884 + >>> f2 = floatToFixedToFloat(-0.61884, precisionBits=14) + >>> f1 != f2 + True + >>> math.isclose(f2, -0.61883544921875) + True + """ + scale = 1 << precisionBits + return otRound(value * scale) / scale def fixedToStr(value, precisionBits): - """Converts a fixed-point number to a string representing a decimal float. + """Converts a fixed-point number to a string representing a decimal float. - This chooses the float that has the shortest decimal representation (the least - number of fractional decimal digits). + This chooses the float that has the shortest decimal representation (the least + number of fractional decimal digits). - For example, to convert a fixed-point number in a 2.14 format, use - ``precisionBits=14``:: + For example, to convert a fixed-point number in a 2.14 format, use + ``precisionBits=14``:: - >>> fixedToStr(-10139, precisionBits=14) - '-0.61884' + >>> fixedToStr(-10139, precisionBits=14) + '-0.61884' - This is pretty slow compared to the simple division used in ``fixedToFloat``. - Use sporadically when you need to serialize or print the fixed-point number in - a human-readable form. - It uses nearestMultipleShortestRepr under the hood. + This is pretty slow compared to the simple division used in ``fixedToFloat``. + Use sporadically when you need to serialize or print the fixed-point number in + a human-readable form. + It uses nearestMultipleShortestRepr under the hood. - Args: - value (int): The fixed-point value to convert. - precisionBits (int): Number of precision bits, *up to a maximum of 16*. + Args: + value (int): The fixed-point value to convert. + precisionBits (int): Number of precision bits, *up to a maximum of 16*. - Returns: - str: A string representation of the value. - """ - scale = 1 << precisionBits - return nearestMultipleShortestRepr(value/scale, factor=1.0/scale) + Returns: + str: A string representation of the value. + """ + scale = 1 << precisionBits + return nearestMultipleShortestRepr(value / scale, factor=1.0 / scale) def strToFixed(string, precisionBits): - """Converts a string representing a decimal float to a fixed-point number. + """Converts a string representing a decimal float to a fixed-point number. - Args: - string (str): A string representing a decimal float. - precisionBits (int): Number of precision bits, *up to a maximum of 16*. + Args: + string (str): A string representing a decimal float. + precisionBits (int): Number of precision bits, *up to a maximum of 16*. - Returns: - int: Fixed-point representation. + Returns: + int: Fixed-point representation. - Examples:: + Examples:: - >>> ## to convert a float string to a 2.14 fixed-point number: - >>> strToFixed('-0.61884', precisionBits=14) - -10139 - """ - value = float(string) - return otRound(value * (1 << precisionBits)) + >>> ## to convert a float string to a 2.14 fixed-point number: + >>> strToFixed('-0.61884', precisionBits=14) + -10139 + """ + value = float(string) + return otRound(value * (1 << precisionBits)) def strToFixedToFloat(string, precisionBits): - """Convert a string to a decimal float with fixed-point rounding. + """Convert a string to a decimal float with fixed-point rounding. - This first converts string to a float, then turns it into a fixed-point - number with ``precisionBits`` fractional binary digits, then back to a - float again. + This first converts string to a float, then turns it into a fixed-point + number with ``precisionBits`` fractional binary digits, then back to a + float again. - This is simply a shorthand for fixedToFloat(floatToFixed(float(s))). + This is simply a shorthand for fixedToFloat(floatToFixed(float(s))). - Args: - string (str): A string representing a decimal float. - precisionBits (int): Number of precision bits. + Args: + string (str): A string representing a decimal float. + precisionBits (int): Number of precision bits. - Returns: - float: The transformed and rounded value. + Returns: + float: The transformed and rounded value. - Examples:: + Examples:: - >>> import math - >>> s = '-0.61884' - >>> bits = 14 - >>> f = strToFixedToFloat(s, precisionBits=bits) - >>> math.isclose(f, -0.61883544921875) - True - >>> f == fixedToFloat(floatToFixed(float(s), precisionBits=bits), precisionBits=bits) - True - """ - value = float(string) - scale = 1 << precisionBits - return otRound(value * scale) / scale + >>> import math + >>> s = '-0.61884' + >>> bits = 14 + >>> f = strToFixedToFloat(s, precisionBits=bits) + >>> math.isclose(f, -0.61883544921875) + True + >>> f == fixedToFloat(floatToFixed(float(s), precisionBits=bits), precisionBits=bits) + True + """ + value = float(string) + scale = 1 << precisionBits + return otRound(value * scale) / scale def floatToFixedToStr(value, precisionBits): - """Convert float to string with fixed-point rounding. + """Convert float to string with fixed-point rounding. - This uses the shortest decimal representation (ie. the least - number of fractional decimal digits) to represent the equivalent - fixed-point number with ``precisionBits`` fractional binary digits. - It uses nearestMultipleShortestRepr under the hood. + This uses the shortest decimal representation (ie. the least + number of fractional decimal digits) to represent the equivalent + fixed-point number with ``precisionBits`` fractional binary digits. + It uses nearestMultipleShortestRepr under the hood. - >>> floatToFixedToStr(-0.61883544921875, precisionBits=14) - '-0.61884' + >>> floatToFixedToStr(-0.61883544921875, precisionBits=14) + '-0.61884' - Args: - value (float): The float value to convert. - precisionBits (int): Number of precision bits, *up to a maximum of 16*. + Args: + value (float): The float value to convert. + precisionBits (int): Number of precision bits, *up to a maximum of 16*. - Returns: - str: A string representation of the value. + Returns: + str: A string representation of the value. - """ - scale = 1 << precisionBits - return nearestMultipleShortestRepr(value, factor=1.0/scale) + """ + scale = 1 << precisionBits + return nearestMultipleShortestRepr(value, factor=1.0 / scale) def ensureVersionIsLong(value): - """Ensure a table version is an unsigned long. - - OpenType table version numbers are expressed as a single unsigned long - comprising of an unsigned short major version and unsigned short minor - version. This function detects if the value to be used as a version number - looks too small (i.e. is less than ``0x10000``), and converts it to - fixed-point using :func:`floatToFixed` if so. - - Args: - value (Number): a candidate table version number. - - Returns: - int: A table version number, possibly corrected to fixed-point. - """ - if value < 0x10000: - newValue = floatToFixed(value, 16) - log.warning( - "Table version value is a float: %.4f; " - "fix to use hex instead: 0x%08x", value, newValue) - value = newValue - return value + """Ensure a table version is an unsigned long. + + OpenType table version numbers are expressed as a single unsigned long + comprising of an unsigned short major version and unsigned short minor + version. This function detects if the value to be used as a version number + looks too small (i.e. is less than ``0x10000``), and converts it to + fixed-point using :func:`floatToFixed` if so. + + Args: + value (Number): a candidate table version number. + + Returns: + int: A table version number, possibly corrected to fixed-point. + """ + if value < 0x10000: + newValue = floatToFixed(value, 16) + log.warning( + "Table version value is a float: %.4f; " "fix to use hex instead: 0x%08x", + value, + newValue, + ) + value = newValue + return value def versionToFixed(value): - """Ensure a table version number is fixed-point. + """Ensure a table version number is fixed-point. - Args: - value (str): a candidate table version number. + Args: + value (str): a candidate table version number. - Returns: - int: A table version number, possibly corrected to fixed-point. - """ - value = int(value, 0) if value.startswith("0") else float(value) - value = ensureVersionIsLong(value) - return value + Returns: + int: A table version number, possibly corrected to fixed-point. + """ + value = int(value, 0) if value.startswith("0") else float(value) + value = ensureVersionIsLong(value) + return value diff --git a/Lib/fontTools/misc/intTools.py b/Lib/fontTools/misc/intTools.py index 6ba03e16..0ca29854 100644 --- a/Lib/fontTools/misc/intTools.py +++ b/Lib/fontTools/misc/intTools.py @@ -1,4 +1,4 @@ -__all__ = ["popCount"] +__all__ = ["popCount", "bit_count", "bit_indices"] try: @@ -13,7 +13,7 @@ except AttributeError: See https://docs.python.org/3.10/library/stdtypes.html#int.bit_count """ -popCount = bit_count +popCount = bit_count # alias def bit_indices(v): diff --git a/Lib/fontTools/misc/loggingTools.py b/Lib/fontTools/misc/loggingTools.py index d1baa839..78704f5a 100644 --- a/Lib/fontTools/misc/loggingTools.py +++ b/Lib/fontTools/misc/loggingTools.py @@ -13,524 +13,531 @@ TIME_LEVEL = logging.DEBUG # per-level format strings used by the default formatter # (the level name is not printed for INFO and DEBUG messages) DEFAULT_FORMATS = { - "*": "%(levelname)s: %(message)s", - "INFO": "%(message)s", - "DEBUG": "%(message)s", - } + "*": "%(levelname)s: %(message)s", + "INFO": "%(message)s", + "DEBUG": "%(message)s", +} class LevelFormatter(logging.Formatter): - """Log formatter with level-specific formatting. - - Formatter class which optionally takes a dict of logging levels to - format strings, allowing to customise the log records appearance for - specific levels. - - - Attributes: - fmt: A dictionary mapping logging levels to format strings. - The ``*`` key identifies the default format string. - datefmt: As per py:class:`logging.Formatter` - style: As per py:class:`logging.Formatter` - - >>> import sys - >>> handler = logging.StreamHandler(sys.stdout) - >>> formatter = LevelFormatter( - ... fmt={ - ... '*': '[%(levelname)s] %(message)s', - ... 'DEBUG': '%(name)s [%(levelname)s] %(message)s', - ... 'INFO': '%(message)s', - ... }) - >>> handler.setFormatter(formatter) - >>> log = logging.getLogger('test') - >>> log.setLevel(logging.DEBUG) - >>> log.addHandler(handler) - >>> log.debug('this uses a custom format string') - test [DEBUG] this uses a custom format string - >>> log.info('this also uses a custom format string') - this also uses a custom format string - >>> log.warning("this one uses the default format string") - [WARNING] this one uses the default format string - """ - - def __init__(self, fmt=None, datefmt=None, style="%"): - if style != '%': - raise ValueError( - "only '%' percent style is supported in both python 2 and 3") - if fmt is None: - fmt = DEFAULT_FORMATS - if isinstance(fmt, str): - default_format = fmt - custom_formats = {} - elif isinstance(fmt, Mapping): - custom_formats = dict(fmt) - default_format = custom_formats.pop("*", None) - else: - raise TypeError('fmt must be a str or a dict of str: %r' % fmt) - super(LevelFormatter, self).__init__(default_format, datefmt) - self.default_format = self._fmt - self.custom_formats = {} - for level, fmt in custom_formats.items(): - level = logging._checkLevel(level) - self.custom_formats[level] = fmt - - def format(self, record): - if self.custom_formats: - fmt = self.custom_formats.get(record.levelno, self.default_format) - if self._fmt != fmt: - self._fmt = fmt - # for python >= 3.2, _style needs to be set if _fmt changes - if PercentStyle: - self._style = PercentStyle(fmt) - return super(LevelFormatter, self).format(record) + """Log formatter with level-specific formatting. + + Formatter class which optionally takes a dict of logging levels to + format strings, allowing to customise the log records appearance for + specific levels. + + + Attributes: + fmt: A dictionary mapping logging levels to format strings. + The ``*`` key identifies the default format string. + datefmt: As per py:class:`logging.Formatter` + style: As per py:class:`logging.Formatter` + + >>> import sys + >>> handler = logging.StreamHandler(sys.stdout) + >>> formatter = LevelFormatter( + ... fmt={ + ... '*': '[%(levelname)s] %(message)s', + ... 'DEBUG': '%(name)s [%(levelname)s] %(message)s', + ... 'INFO': '%(message)s', + ... }) + >>> handler.setFormatter(formatter) + >>> log = logging.getLogger('test') + >>> log.setLevel(logging.DEBUG) + >>> log.addHandler(handler) + >>> log.debug('this uses a custom format string') + test [DEBUG] this uses a custom format string + >>> log.info('this also uses a custom format string') + this also uses a custom format string + >>> log.warning("this one uses the default format string") + [WARNING] this one uses the default format string + """ + + def __init__(self, fmt=None, datefmt=None, style="%"): + if style != "%": + raise ValueError( + "only '%' percent style is supported in both python 2 and 3" + ) + if fmt is None: + fmt = DEFAULT_FORMATS + if isinstance(fmt, str): + default_format = fmt + custom_formats = {} + elif isinstance(fmt, Mapping): + custom_formats = dict(fmt) + default_format = custom_formats.pop("*", None) + else: + raise TypeError("fmt must be a str or a dict of str: %r" % fmt) + super(LevelFormatter, self).__init__(default_format, datefmt) + self.default_format = self._fmt + self.custom_formats = {} + for level, fmt in custom_formats.items(): + level = logging._checkLevel(level) + self.custom_formats[level] = fmt + + def format(self, record): + if self.custom_formats: + fmt = self.custom_formats.get(record.levelno, self.default_format) + if self._fmt != fmt: + self._fmt = fmt + # for python >= 3.2, _style needs to be set if _fmt changes + if PercentStyle: + self._style = PercentStyle(fmt) + return super(LevelFormatter, self).format(record) def configLogger(**kwargs): - """A more sophisticated logging system configuation manager. - - This is more or less the same as :py:func:`logging.basicConfig`, - with some additional options and defaults. - - The default behaviour is to create a ``StreamHandler`` which writes to - sys.stderr, set a formatter using the ``DEFAULT_FORMATS`` strings, and add - the handler to the top-level library logger ("fontTools"). - - A number of optional keyword arguments may be specified, which can alter - the default behaviour. - - Args: - - logger: Specifies the logger name or a Logger instance to be - configured. (Defaults to "fontTools" logger). Unlike ``basicConfig``, - this function can be called multiple times to reconfigure a logger. - If the logger or any of its children already exists before the call is - made, they will be reset before the new configuration is applied. - filename: Specifies that a ``FileHandler`` be created, using the - specified filename, rather than a ``StreamHandler``. - filemode: Specifies the mode to open the file, if filename is - specified. (If filemode is unspecified, it defaults to ``a``). - format: Use the specified format string for the handler. This - argument also accepts a dictionary of format strings keyed by - level name, to allow customising the records appearance for - specific levels. The special ``'*'`` key is for 'any other' level. - datefmt: Use the specified date/time format. - level: Set the logger level to the specified level. - stream: Use the specified stream to initialize the StreamHandler. Note - that this argument is incompatible with ``filename`` - if both - are present, ``stream`` is ignored. - handlers: If specified, this should be an iterable of already created - handlers, which will be added to the logger. Any handler in the - list which does not have a formatter assigned will be assigned the - formatter created in this function. - filters: If specified, this should be an iterable of already created - filters. If the ``handlers`` do not already have filters assigned, - these filters will be added to them. - propagate: All loggers have a ``propagate`` attribute which determines - whether to continue searching for handlers up the logging hierarchy. - If not provided, the "propagate" attribute will be set to ``False``. - """ - # using kwargs to enforce keyword-only arguments in py2. - handlers = kwargs.pop("handlers", None) - if handlers is None: - if "stream" in kwargs and "filename" in kwargs: - raise ValueError("'stream' and 'filename' should not be " - "specified together") - else: - if "stream" in kwargs or "filename" in kwargs: - raise ValueError("'stream' or 'filename' should not be " - "specified together with 'handlers'") - if handlers is None: - filename = kwargs.pop("filename", None) - mode = kwargs.pop("filemode", 'a') - if filename: - h = logging.FileHandler(filename, mode) - else: - stream = kwargs.pop("stream", None) - h = logging.StreamHandler(stream) - handlers = [h] - # By default, the top-level library logger is configured. - logger = kwargs.pop("logger", "fontTools") - if not logger or isinstance(logger, str): - # empty "" or None means the 'root' logger - logger = logging.getLogger(logger) - # before (re)configuring, reset named logger and its children (if exist) - _resetExistingLoggers(parent=logger.name) - # use DEFAULT_FORMATS if 'format' is None - fs = kwargs.pop("format", None) - dfs = kwargs.pop("datefmt", None) - # XXX: '%' is the only format style supported on both py2 and 3 - style = kwargs.pop("style", '%') - fmt = LevelFormatter(fs, dfs, style) - filters = kwargs.pop("filters", []) - for h in handlers: - if h.formatter is None: - h.setFormatter(fmt) - if not h.filters: - for f in filters: - h.addFilter(f) - logger.addHandler(h) - if logger.name != "root": - # stop searching up the hierarchy for handlers - logger.propagate = kwargs.pop("propagate", False) - # set a custom severity level - level = kwargs.pop("level", None) - if level is not None: - logger.setLevel(level) - if kwargs: - keys = ', '.join(kwargs.keys()) - raise ValueError('Unrecognised argument(s): %s' % keys) + """A more sophisticated logging system configuation manager. + + This is more or less the same as :py:func:`logging.basicConfig`, + with some additional options and defaults. + + The default behaviour is to create a ``StreamHandler`` which writes to + sys.stderr, set a formatter using the ``DEFAULT_FORMATS`` strings, and add + the handler to the top-level library logger ("fontTools"). + + A number of optional keyword arguments may be specified, which can alter + the default behaviour. + + Args: + + logger: Specifies the logger name or a Logger instance to be + configured. (Defaults to "fontTools" logger). Unlike ``basicConfig``, + this function can be called multiple times to reconfigure a logger. + If the logger or any of its children already exists before the call is + made, they will be reset before the new configuration is applied. + filename: Specifies that a ``FileHandler`` be created, using the + specified filename, rather than a ``StreamHandler``. + filemode: Specifies the mode to open the file, if filename is + specified. (If filemode is unspecified, it defaults to ``a``). + format: Use the specified format string for the handler. This + argument also accepts a dictionary of format strings keyed by + level name, to allow customising the records appearance for + specific levels. The special ``'*'`` key is for 'any other' level. + datefmt: Use the specified date/time format. + level: Set the logger level to the specified level. + stream: Use the specified stream to initialize the StreamHandler. Note + that this argument is incompatible with ``filename`` - if both + are present, ``stream`` is ignored. + handlers: If specified, this should be an iterable of already created + handlers, which will be added to the logger. Any handler in the + list which does not have a formatter assigned will be assigned the + formatter created in this function. + filters: If specified, this should be an iterable of already created + filters. If the ``handlers`` do not already have filters assigned, + these filters will be added to them. + propagate: All loggers have a ``propagate`` attribute which determines + whether to continue searching for handlers up the logging hierarchy. + If not provided, the "propagate" attribute will be set to ``False``. + """ + # using kwargs to enforce keyword-only arguments in py2. + handlers = kwargs.pop("handlers", None) + if handlers is None: + if "stream" in kwargs and "filename" in kwargs: + raise ValueError( + "'stream' and 'filename' should not be " "specified together" + ) + else: + if "stream" in kwargs or "filename" in kwargs: + raise ValueError( + "'stream' or 'filename' should not be " + "specified together with 'handlers'" + ) + if handlers is None: + filename = kwargs.pop("filename", None) + mode = kwargs.pop("filemode", "a") + if filename: + h = logging.FileHandler(filename, mode) + else: + stream = kwargs.pop("stream", None) + h = logging.StreamHandler(stream) + handlers = [h] + # By default, the top-level library logger is configured. + logger = kwargs.pop("logger", "fontTools") + if not logger or isinstance(logger, str): + # empty "" or None means the 'root' logger + logger = logging.getLogger(logger) + # before (re)configuring, reset named logger and its children (if exist) + _resetExistingLoggers(parent=logger.name) + # use DEFAULT_FORMATS if 'format' is None + fs = kwargs.pop("format", None) + dfs = kwargs.pop("datefmt", None) + # XXX: '%' is the only format style supported on both py2 and 3 + style = kwargs.pop("style", "%") + fmt = LevelFormatter(fs, dfs, style) + filters = kwargs.pop("filters", []) + for h in handlers: + if h.formatter is None: + h.setFormatter(fmt) + if not h.filters: + for f in filters: + h.addFilter(f) + logger.addHandler(h) + if logger.name != "root": + # stop searching up the hierarchy for handlers + logger.propagate = kwargs.pop("propagate", False) + # set a custom severity level + level = kwargs.pop("level", None) + if level is not None: + logger.setLevel(level) + if kwargs: + keys = ", ".join(kwargs.keys()) + raise ValueError("Unrecognised argument(s): %s" % keys) def _resetExistingLoggers(parent="root"): - """ Reset the logger named 'parent' and all its children to their initial - state, if they already exist in the current configuration. - """ - root = logging.root - # get sorted list of all existing loggers - existing = sorted(root.manager.loggerDict.keys()) - if parent == "root": - # all the existing loggers are children of 'root' - loggers_to_reset = [parent] + existing - elif parent not in existing: - # nothing to do - return - elif parent in existing: - loggers_to_reset = [parent] - # collect children, starting with the entry after parent name - i = existing.index(parent) + 1 - prefixed = parent + "." - pflen = len(prefixed) - num_existing = len(existing) - while i < num_existing: - if existing[i][:pflen] == prefixed: - loggers_to_reset.append(existing[i]) - i += 1 - for name in loggers_to_reset: - if name == "root": - root.setLevel(logging.WARNING) - for h in root.handlers[:]: - root.removeHandler(h) - for f in root.filters[:]: - root.removeFilters(f) - root.disabled = False - else: - logger = root.manager.loggerDict[name] - logger.level = logging.NOTSET - logger.handlers = [] - logger.filters = [] - logger.propagate = True - logger.disabled = False + """Reset the logger named 'parent' and all its children to their initial + state, if they already exist in the current configuration. + """ + root = logging.root + # get sorted list of all existing loggers + existing = sorted(root.manager.loggerDict.keys()) + if parent == "root": + # all the existing loggers are children of 'root' + loggers_to_reset = [parent] + existing + elif parent not in existing: + # nothing to do + return + elif parent in existing: + loggers_to_reset = [parent] + # collect children, starting with the entry after parent name + i = existing.index(parent) + 1 + prefixed = parent + "." + pflen = len(prefixed) + num_existing = len(existing) + while i < num_existing: + if existing[i][:pflen] == prefixed: + loggers_to_reset.append(existing[i]) + i += 1 + for name in loggers_to_reset: + if name == "root": + root.setLevel(logging.WARNING) + for h in root.handlers[:]: + root.removeHandler(h) + for f in root.filters[:]: + root.removeFilters(f) + root.disabled = False + else: + logger = root.manager.loggerDict[name] + logger.level = logging.NOTSET + logger.handlers = [] + logger.filters = [] + logger.propagate = True + logger.disabled = False class Timer(object): - """ Keeps track of overall time and split/lap times. - - >>> import time - >>> timer = Timer() - >>> time.sleep(0.01) - >>> print("First lap:", timer.split()) - First lap: ... - >>> time.sleep(0.02) - >>> print("Second lap:", timer.split()) - Second lap: ... - >>> print("Overall time:", timer.time()) - Overall time: ... - - Can be used as a context manager inside with-statements. - - >>> with Timer() as t: - ... time.sleep(0.01) - >>> print("%0.3f seconds" % t.elapsed) - 0... seconds - - If initialised with a logger, it can log the elapsed time automatically - upon exiting the with-statement. - - >>> import logging - >>> log = logging.getLogger("my-fancy-timer-logger") - >>> configLogger(logger=log, level="DEBUG", format="%(message)s", stream=sys.stdout) - >>> with Timer(log, 'do something'): - ... time.sleep(0.01) - Took ... to do something - - The same Timer instance, holding a reference to a logger, can be reused - in multiple with-statements, optionally with different messages or levels. - - >>> timer = Timer(log) - >>> with timer(): - ... time.sleep(0.01) - elapsed time: ...s - >>> with timer('redo it', level=logging.INFO): - ... time.sleep(0.02) - Took ... to redo it - - It can also be used as a function decorator to log the time elapsed to run - the decorated function. - - >>> @timer() - ... def test1(): - ... time.sleep(0.01) - >>> @timer('run test 2', level=logging.INFO) - ... def test2(): - ... time.sleep(0.02) - >>> test1() - Took ... to run 'test1' - >>> test2() - Took ... to run test 2 - """ - - # timeit.default_timer choses the most accurate clock for each platform - _time = timeit.default_timer - default_msg = "elapsed time: %(time).3fs" - default_format = "Took %(time).3fs to %(msg)s" - - def __init__(self, logger=None, msg=None, level=None, start=None): - self.reset(start) - if logger is None: - for arg in ('msg', 'level'): - if locals().get(arg) is not None: - raise ValueError( - "'%s' can't be specified without a 'logger'" % arg) - self.logger = logger - self.level = level if level is not None else TIME_LEVEL - self.msg = msg - - def reset(self, start=None): - """ Reset timer to 'start_time' or the current time. """ - if start is None: - self.start = self._time() - else: - self.start = start - self.last = self.start - self.elapsed = 0.0 - - def time(self): - """ Return the overall time (in seconds) since the timer started. """ - return self._time() - self.start - - def split(self): - """ Split and return the lap time (in seconds) in between splits. """ - current = self._time() - self.elapsed = current - self.last - self.last = current - return self.elapsed - - def formatTime(self, msg, time): - """ Format 'time' value in 'msg' and return formatted string. - If 'msg' contains a '%(time)' format string, try to use that. - Otherwise, use the predefined 'default_format'. - If 'msg' is empty or None, fall back to 'default_msg'. - """ - if not msg: - msg = self.default_msg - if msg.find("%(time)") < 0: - msg = self.default_format % {"msg": msg, "time": time} - else: - try: - msg = msg % {"time": time} - except (KeyError, ValueError): - pass # skip if the format string is malformed - return msg - - def __enter__(self): - """ Start a new lap """ - self.last = self._time() - self.elapsed = 0.0 - return self - - def __exit__(self, exc_type, exc_value, traceback): - """ End the current lap. If timer has a logger, log the time elapsed, - using the format string in self.msg (or the default one). - """ - time = self.split() - if self.logger is None or exc_type: - # if there's no logger attached, or if any exception occurred in - # the with-statement, exit without logging the time - return - message = self.formatTime(self.msg, time) - # Allow log handlers to see the individual parts to facilitate things - # like a server accumulating aggregate stats. - msg_parts = { 'msg': self.msg, 'time': time } - self.logger.log(self.level, message, msg_parts) - - def __call__(self, func_or_msg=None, **kwargs): - """ If the first argument is a function, return a decorator which runs - the wrapped function inside Timer's context manager. - Otherwise, treat the first argument as a 'msg' string and return an updated - Timer instance, referencing the same logger. - A 'level' keyword can also be passed to override self.level. - """ - if isinstance(func_or_msg, Callable): - func = func_or_msg - # use the function name when no explicit 'msg' is provided - if not self.msg: - self.msg = "run '%s'" % func.__name__ - - @wraps(func) - def wrapper(*args, **kwds): - with self: - return func(*args, **kwds) - return wrapper - else: - msg = func_or_msg or kwargs.get("msg") - level = kwargs.get("level", self.level) - return self.__class__(self.logger, msg, level) - - def __float__(self): - return self.elapsed - - def __int__(self): - return int(self.elapsed) - - def __str__(self): - return "%.3f" % self.elapsed + """Keeps track of overall time and split/lap times. + + >>> import time + >>> timer = Timer() + >>> time.sleep(0.01) + >>> print("First lap:", timer.split()) + First lap: ... + >>> time.sleep(0.02) + >>> print("Second lap:", timer.split()) + Second lap: ... + >>> print("Overall time:", timer.time()) + Overall time: ... + + Can be used as a context manager inside with-statements. + + >>> with Timer() as t: + ... time.sleep(0.01) + >>> print("%0.3f seconds" % t.elapsed) + 0... seconds + + If initialised with a logger, it can log the elapsed time automatically + upon exiting the with-statement. + + >>> import logging + >>> log = logging.getLogger("my-fancy-timer-logger") + >>> configLogger(logger=log, level="DEBUG", format="%(message)s", stream=sys.stdout) + >>> with Timer(log, 'do something'): + ... time.sleep(0.01) + Took ... to do something + + The same Timer instance, holding a reference to a logger, can be reused + in multiple with-statements, optionally with different messages or levels. + + >>> timer = Timer(log) + >>> with timer(): + ... time.sleep(0.01) + elapsed time: ...s + >>> with timer('redo it', level=logging.INFO): + ... time.sleep(0.02) + Took ... to redo it + + It can also be used as a function decorator to log the time elapsed to run + the decorated function. + + >>> @timer() + ... def test1(): + ... time.sleep(0.01) + >>> @timer('run test 2', level=logging.INFO) + ... def test2(): + ... time.sleep(0.02) + >>> test1() + Took ... to run 'test1' + >>> test2() + Took ... to run test 2 + """ + + # timeit.default_timer choses the most accurate clock for each platform + _time = timeit.default_timer + default_msg = "elapsed time: %(time).3fs" + default_format = "Took %(time).3fs to %(msg)s" + + def __init__(self, logger=None, msg=None, level=None, start=None): + self.reset(start) + if logger is None: + for arg in ("msg", "level"): + if locals().get(arg) is not None: + raise ValueError("'%s' can't be specified without a 'logger'" % arg) + self.logger = logger + self.level = level if level is not None else TIME_LEVEL + self.msg = msg + + def reset(self, start=None): + """Reset timer to 'start_time' or the current time.""" + if start is None: + self.start = self._time() + else: + self.start = start + self.last = self.start + self.elapsed = 0.0 + + def time(self): + """Return the overall time (in seconds) since the timer started.""" + return self._time() - self.start + + def split(self): + """Split and return the lap time (in seconds) in between splits.""" + current = self._time() + self.elapsed = current - self.last + self.last = current + return self.elapsed + + def formatTime(self, msg, time): + """Format 'time' value in 'msg' and return formatted string. + If 'msg' contains a '%(time)' format string, try to use that. + Otherwise, use the predefined 'default_format'. + If 'msg' is empty or None, fall back to 'default_msg'. + """ + if not msg: + msg = self.default_msg + if msg.find("%(time)") < 0: + msg = self.default_format % {"msg": msg, "time": time} + else: + try: + msg = msg % {"time": time} + except (KeyError, ValueError): + pass # skip if the format string is malformed + return msg + + def __enter__(self): + """Start a new lap""" + self.last = self._time() + self.elapsed = 0.0 + return self + + def __exit__(self, exc_type, exc_value, traceback): + """End the current lap. If timer has a logger, log the time elapsed, + using the format string in self.msg (or the default one). + """ + time = self.split() + if self.logger is None or exc_type: + # if there's no logger attached, or if any exception occurred in + # the with-statement, exit without logging the time + return + message = self.formatTime(self.msg, time) + # Allow log handlers to see the individual parts to facilitate things + # like a server accumulating aggregate stats. + msg_parts = {"msg": self.msg, "time": time} + self.logger.log(self.level, message, msg_parts) + + def __call__(self, func_or_msg=None, **kwargs): + """If the first argument is a function, return a decorator which runs + the wrapped function inside Timer's context manager. + Otherwise, treat the first argument as a 'msg' string and return an updated + Timer instance, referencing the same logger. + A 'level' keyword can also be passed to override self.level. + """ + if isinstance(func_or_msg, Callable): + func = func_or_msg + # use the function name when no explicit 'msg' is provided + if not self.msg: + self.msg = "run '%s'" % func.__name__ + + @wraps(func) + def wrapper(*args, **kwds): + with self: + return func(*args, **kwds) + + return wrapper + else: + msg = func_or_msg or kwargs.get("msg") + level = kwargs.get("level", self.level) + return self.__class__(self.logger, msg, level) + + def __float__(self): + return self.elapsed + + def __int__(self): + return int(self.elapsed) + + def __str__(self): + return "%.3f" % self.elapsed class ChannelsFilter(logging.Filter): - """Provides a hierarchical filter for log entries based on channel names. - - Filters out records emitted from a list of enabled channel names, - including their children. It works the same as the ``logging.Filter`` - class, but allows the user to specify multiple channel names. - - >>> import sys - >>> handler = logging.StreamHandler(sys.stdout) - >>> handler.setFormatter(logging.Formatter("%(message)s")) - >>> filter = ChannelsFilter("A.B", "C.D") - >>> handler.addFilter(filter) - >>> root = logging.getLogger() - >>> root.addHandler(handler) - >>> root.setLevel(level=logging.DEBUG) - >>> logging.getLogger('A.B').debug('this record passes through') - this record passes through - >>> logging.getLogger('A.B.C').debug('records from children also pass') - records from children also pass - >>> logging.getLogger('C.D').debug('this one as well') - this one as well - >>> logging.getLogger('A.B.').debug('also this one') - also this one - >>> logging.getLogger('A.F').debug('but this one does not!') - >>> logging.getLogger('C.DE').debug('neither this one!') - """ - - def __init__(self, *names): - self.names = names - self.num = len(names) - self.lengths = {n: len(n) for n in names} - - def filter(self, record): - if self.num == 0: - return True - for name in self.names: - nlen = self.lengths[name] - if name == record.name: - return True - elif (record.name.find(name, 0, nlen) == 0 - and record.name[nlen] == "."): - return True - return False + """Provides a hierarchical filter for log entries based on channel names. + + Filters out records emitted from a list of enabled channel names, + including their children. It works the same as the ``logging.Filter`` + class, but allows the user to specify multiple channel names. + + >>> import sys + >>> handler = logging.StreamHandler(sys.stdout) + >>> handler.setFormatter(logging.Formatter("%(message)s")) + >>> filter = ChannelsFilter("A.B", "C.D") + >>> handler.addFilter(filter) + >>> root = logging.getLogger() + >>> root.addHandler(handler) + >>> root.setLevel(level=logging.DEBUG) + >>> logging.getLogger('A.B').debug('this record passes through') + this record passes through + >>> logging.getLogger('A.B.C').debug('records from children also pass') + records from children also pass + >>> logging.getLogger('C.D').debug('this one as well') + this one as well + >>> logging.getLogger('A.B.').debug('also this one') + also this one + >>> logging.getLogger('A.F').debug('but this one does not!') + >>> logging.getLogger('C.DE').debug('neither this one!') + """ + + def __init__(self, *names): + self.names = names + self.num = len(names) + self.lengths = {n: len(n) for n in names} + + def filter(self, record): + if self.num == 0: + return True + for name in self.names: + nlen = self.lengths[name] + if name == record.name: + return True + elif record.name.find(name, 0, nlen) == 0 and record.name[nlen] == ".": + return True + return False class CapturingLogHandler(logging.Handler): - def __init__(self, logger, level): - super(CapturingLogHandler, self).__init__(level=level) - self.records = [] - if isinstance(logger, str): - self.logger = logging.getLogger(logger) - else: - self.logger = logger - - def __enter__(self): - self.original_disabled = self.logger.disabled - self.original_level = self.logger.level - self.original_propagate = self.logger.propagate - - self.logger.addHandler(self) - self.logger.setLevel(self.level) - self.logger.disabled = False - self.logger.propagate = False - - return self - - def __exit__(self, type, value, traceback): - self.logger.removeHandler(self) - self.logger.setLevel(self.original_level) - self.logger.disabled = self.original_disabled - self.logger.propagate = self.original_propagate - - return self - - def emit(self, record): - self.records.append(record) - - def assertRegex(self, regexp, msg=None): - import re - pattern = re.compile(regexp) - for r in self.records: - if pattern.search(r.getMessage()): - return True - if msg is None: - msg = "Pattern '%s' not found in logger records" % regexp - assert 0, msg + def __init__(self, logger, level): + super(CapturingLogHandler, self).__init__(level=level) + self.records = [] + if isinstance(logger, str): + self.logger = logging.getLogger(logger) + else: + self.logger = logger + + def __enter__(self): + self.original_disabled = self.logger.disabled + self.original_level = self.logger.level + self.original_propagate = self.logger.propagate + + self.logger.addHandler(self) + self.logger.setLevel(self.level) + self.logger.disabled = False + self.logger.propagate = False + + return self + + def __exit__(self, type, value, traceback): + self.logger.removeHandler(self) + self.logger.setLevel(self.original_level) + self.logger.disabled = self.original_disabled + self.logger.propagate = self.original_propagate + + return self + + def emit(self, record): + self.records.append(record) + + def assertRegex(self, regexp, msg=None): + import re + + pattern = re.compile(regexp) + for r in self.records: + if pattern.search(r.getMessage()): + return True + if msg is None: + msg = "Pattern '%s' not found in logger records" % regexp + assert 0, msg class LogMixin(object): - """ Mixin class that adds logging functionality to another class. - - You can define a new class that subclasses from ``LogMixin`` as well as - other base classes through multiple inheritance. - All instances of that class will have a ``log`` property that returns - a ``logging.Logger`` named after their respective ``<module>.<class>``. - - For example: - - >>> class BaseClass(object): - ... pass - >>> class MyClass(LogMixin, BaseClass): - ... pass - >>> a = MyClass() - >>> isinstance(a.log, logging.Logger) - True - >>> print(a.log.name) - fontTools.misc.loggingTools.MyClass - >>> class AnotherClass(MyClass): - ... pass - >>> b = AnotherClass() - >>> isinstance(b.log, logging.Logger) - True - >>> print(b.log.name) - fontTools.misc.loggingTools.AnotherClass - """ - - @property - def log(self): - if not hasattr(self, "_log"): - name = ".".join( - (self.__class__.__module__, self.__class__.__name__) - ) - self._log = logging.getLogger(name) - return self._log + """Mixin class that adds logging functionality to another class. + + You can define a new class that subclasses from ``LogMixin`` as well as + other base classes through multiple inheritance. + All instances of that class will have a ``log`` property that returns + a ``logging.Logger`` named after their respective ``<module>.<class>``. + + For example: + + >>> class BaseClass(object): + ... pass + >>> class MyClass(LogMixin, BaseClass): + ... pass + >>> a = MyClass() + >>> isinstance(a.log, logging.Logger) + True + >>> print(a.log.name) + fontTools.misc.loggingTools.MyClass + >>> class AnotherClass(MyClass): + ... pass + >>> b = AnotherClass() + >>> isinstance(b.log, logging.Logger) + True + >>> print(b.log.name) + fontTools.misc.loggingTools.AnotherClass + """ + + @property + def log(self): + if not hasattr(self, "_log"): + name = ".".join((self.__class__.__module__, self.__class__.__name__)) + self._log = logging.getLogger(name) + return self._log def deprecateArgument(name, msg, category=UserWarning): - """ Raise a warning about deprecated function argument 'name'. """ - warnings.warn( - "%r is deprecated; %s" % (name, msg), category=category, stacklevel=3) + """Raise a warning about deprecated function argument 'name'.""" + warnings.warn("%r is deprecated; %s" % (name, msg), category=category, stacklevel=3) def deprecateFunction(msg, category=UserWarning): - """ Decorator to raise a warning when a deprecated function is called. """ - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - warnings.warn( - "%r is deprecated; %s" % (func.__name__, msg), - category=category, stacklevel=2) - return func(*args, **kwargs) - return wrapper - return decorator + """Decorator to raise a warning when a deprecated function is called.""" + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + "%r is deprecated; %s" % (func.__name__, msg), + category=category, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator if __name__ == "__main__": - import doctest - sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) + import doctest + + sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) diff --git a/Lib/fontTools/misc/macCreatorType.py b/Lib/fontTools/misc/macCreatorType.py index 6b191054..36b15aca 100644 --- a/Lib/fontTools/misc/macCreatorType.py +++ b/Lib/fontTools/misc/macCreatorType.py @@ -1,54 +1,56 @@ from fontTools.misc.textTools import Tag, bytesjoin, strjoin + try: - import xattr + import xattr except ImportError: - xattr = None + xattr = None def _reverseString(s): - s = list(s) - s.reverse() - return strjoin(s) + s = list(s) + s.reverse() + return strjoin(s) def getMacCreatorAndType(path): - """Returns file creator and file type codes for a path. - - Args: - path (str): A file path. - - Returns: - A tuple of two :py:class:`fontTools.textTools.Tag` objects, the first - representing the file creator and the second representing the - file type. - """ - if xattr is not None: - try: - finderInfo = xattr.getxattr(path, 'com.apple.FinderInfo') - except (KeyError, IOError): - pass - else: - fileType = Tag(finderInfo[:4]) - fileCreator = Tag(finderInfo[4:8]) - return fileCreator, fileType - return None, None + """Returns file creator and file type codes for a path. + + Args: + path (str): A file path. + + Returns: + A tuple of two :py:class:`fontTools.textTools.Tag` objects, the first + representing the file creator and the second representing the + file type. + """ + if xattr is not None: + try: + finderInfo = xattr.getxattr(path, "com.apple.FinderInfo") + except (KeyError, IOError): + pass + else: + fileType = Tag(finderInfo[:4]) + fileCreator = Tag(finderInfo[4:8]) + return fileCreator, fileType + return None, None def setMacCreatorAndType(path, fileCreator, fileType): - """Set file creator and file type codes for a path. - - Note that if the ``xattr`` module is not installed, no action is - taken but no error is raised. - - Args: - path (str): A file path. - fileCreator: A four-character file creator tag. - fileType: A four-character file type tag. - - """ - if xattr is not None: - from fontTools.misc.textTools import pad - if not all(len(s) == 4 for s in (fileCreator, fileType)): - raise TypeError('arg must be string of 4 chars') - finderInfo = pad(bytesjoin([fileType, fileCreator]), 32) - xattr.setxattr(path, 'com.apple.FinderInfo', finderInfo) + """Set file creator and file type codes for a path. + + Note that if the ``xattr`` module is not installed, no action is + taken but no error is raised. + + Args: + path (str): A file path. + fileCreator: A four-character file creator tag. + fileType: A four-character file type tag. + + """ + if xattr is not None: + from fontTools.misc.textTools import pad + + if not all(len(s) == 4 for s in (fileCreator, fileType)): + raise TypeError("arg must be string of 4 chars") + finderInfo = pad(bytesjoin([fileType, fileCreator]), 32) + xattr.setxattr(path, "com.apple.FinderInfo", finderInfo) diff --git a/Lib/fontTools/misc/macRes.py b/Lib/fontTools/misc/macRes.py index 895ca1b8..f5a6cfe4 100644 --- a/Lib/fontTools/misc/macRes.py +++ b/Lib/fontTools/misc/macRes.py @@ -7,216 +7,218 @@ from collections.abc import MutableMapping class ResourceError(Exception): - pass + pass class ResourceReader(MutableMapping): - """Reader for Mac OS resource forks. - - Parses a resource fork and returns resources according to their type. - If run on OS X, this will open the resource fork in the filesystem. - Otherwise, it will open the file itself and attempt to read it as - though it were a resource fork. - - The returned object can be indexed by type and iterated over, - returning in each case a list of py:class:`Resource` objects - representing all the resources of a certain type. - - """ - def __init__(self, fileOrPath): - """Open a file - - Args: - fileOrPath: Either an object supporting a ``read`` method, an - ``os.PathLike`` object, or a string. - """ - self._resources = OrderedDict() - if hasattr(fileOrPath, 'read'): - self.file = fileOrPath - else: - try: - # try reading from the resource fork (only works on OS X) - self.file = self.openResourceFork(fileOrPath) - self._readFile() - return - except (ResourceError, IOError): - # if it fails, use the data fork - self.file = self.openDataFork(fileOrPath) - self._readFile() - - @staticmethod - def openResourceFork(path): - if hasattr(path, "__fspath__"): # support os.PathLike objects - path = path.__fspath__() - with open(path + '/..namedfork/rsrc', 'rb') as resfork: - data = resfork.read() - infile = BytesIO(data) - infile.name = path - return infile - - @staticmethod - def openDataFork(path): - with open(path, 'rb') as datafork: - data = datafork.read() - infile = BytesIO(data) - infile.name = path - return infile - - def _readFile(self): - self._readHeaderAndMap() - self._readTypeList() - - def _read(self, numBytes, offset=None): - if offset is not None: - try: - self.file.seek(offset) - except OverflowError: - raise ResourceError("Failed to seek offset ('offset' is too large)") - if self.file.tell() != offset: - raise ResourceError('Failed to seek offset (reached EOF)') - try: - data = self.file.read(numBytes) - except OverflowError: - raise ResourceError("Cannot read resource ('numBytes' is too large)") - if len(data) != numBytes: - raise ResourceError('Cannot read resource (not enough data)') - return data - - def _readHeaderAndMap(self): - self.file.seek(0) - headerData = self._read(ResourceForkHeaderSize) - sstruct.unpack(ResourceForkHeader, headerData, self) - # seek to resource map, skip reserved - mapOffset = self.mapOffset + 22 - resourceMapData = self._read(ResourceMapHeaderSize, mapOffset) - sstruct.unpack(ResourceMapHeader, resourceMapData, self) - self.absTypeListOffset = self.mapOffset + self.typeListOffset - self.absNameListOffset = self.mapOffset + self.nameListOffset - - def _readTypeList(self): - absTypeListOffset = self.absTypeListOffset - numTypesData = self._read(2, absTypeListOffset) - self.numTypes, = struct.unpack('>H', numTypesData) - absTypeListOffset2 = absTypeListOffset + 2 - for i in range(self.numTypes + 1): - resTypeItemOffset = absTypeListOffset2 + ResourceTypeItemSize * i - resTypeItemData = self._read(ResourceTypeItemSize, resTypeItemOffset) - item = sstruct.unpack(ResourceTypeItem, resTypeItemData) - resType = tostr(item['type'], encoding='mac-roman') - refListOffset = absTypeListOffset + item['refListOffset'] - numRes = item['numRes'] + 1 - resources = self._readReferenceList(resType, refListOffset, numRes) - self._resources[resType] = resources - - def _readReferenceList(self, resType, refListOffset, numRes): - resources = [] - for i in range(numRes): - refOffset = refListOffset + ResourceRefItemSize * i - refData = self._read(ResourceRefItemSize, refOffset) - res = Resource(resType) - res.decompile(refData, self) - resources.append(res) - return resources - - def __getitem__(self, resType): - return self._resources[resType] - - def __delitem__(self, resType): - del self._resources[resType] - - def __setitem__(self, resType, resources): - self._resources[resType] = resources - - def __len__(self): - return len(self._resources) - - def __iter__(self): - return iter(self._resources) - - def keys(self): - return self._resources.keys() - - @property - def types(self): - """A list of the types of resources in the resource fork.""" - return list(self._resources.keys()) - - def countResources(self, resType): - """Return the number of resources of a given type.""" - try: - return len(self[resType]) - except KeyError: - return 0 - - def getIndices(self, resType): - """Returns a list of indices of resources of a given type.""" - numRes = self.countResources(resType) - if numRes: - return list(range(1, numRes+1)) - else: - return [] - - def getNames(self, resType): - """Return list of names of all resources of a given type.""" - return [res.name for res in self.get(resType, []) if res.name is not None] - - def getIndResource(self, resType, index): - """Return resource of given type located at an index ranging from 1 - to the number of resources for that type, or None if not found. - """ - if index < 1: - return None - try: - res = self[resType][index-1] - except (KeyError, IndexError): - return None - return res - - def getNamedResource(self, resType, name): - """Return the named resource of given type, else return None.""" - name = tostr(name, encoding='mac-roman') - for res in self.get(resType, []): - if res.name == name: - return res - return None - - def close(self): - if not self.file.closed: - self.file.close() + """Reader for Mac OS resource forks. + + Parses a resource fork and returns resources according to their type. + If run on OS X, this will open the resource fork in the filesystem. + Otherwise, it will open the file itself and attempt to read it as + though it were a resource fork. + + The returned object can be indexed by type and iterated over, + returning in each case a list of py:class:`Resource` objects + representing all the resources of a certain type. + + """ + + def __init__(self, fileOrPath): + """Open a file + + Args: + fileOrPath: Either an object supporting a ``read`` method, an + ``os.PathLike`` object, or a string. + """ + self._resources = OrderedDict() + if hasattr(fileOrPath, "read"): + self.file = fileOrPath + else: + try: + # try reading from the resource fork (only works on OS X) + self.file = self.openResourceFork(fileOrPath) + self._readFile() + return + except (ResourceError, IOError): + # if it fails, use the data fork + self.file = self.openDataFork(fileOrPath) + self._readFile() + + @staticmethod + def openResourceFork(path): + if hasattr(path, "__fspath__"): # support os.PathLike objects + path = path.__fspath__() + with open(path + "/..namedfork/rsrc", "rb") as resfork: + data = resfork.read() + infile = BytesIO(data) + infile.name = path + return infile + + @staticmethod + def openDataFork(path): + with open(path, "rb") as datafork: + data = datafork.read() + infile = BytesIO(data) + infile.name = path + return infile + + def _readFile(self): + self._readHeaderAndMap() + self._readTypeList() + + def _read(self, numBytes, offset=None): + if offset is not None: + try: + self.file.seek(offset) + except OverflowError: + raise ResourceError("Failed to seek offset ('offset' is too large)") + if self.file.tell() != offset: + raise ResourceError("Failed to seek offset (reached EOF)") + try: + data = self.file.read(numBytes) + except OverflowError: + raise ResourceError("Cannot read resource ('numBytes' is too large)") + if len(data) != numBytes: + raise ResourceError("Cannot read resource (not enough data)") + return data + + def _readHeaderAndMap(self): + self.file.seek(0) + headerData = self._read(ResourceForkHeaderSize) + sstruct.unpack(ResourceForkHeader, headerData, self) + # seek to resource map, skip reserved + mapOffset = self.mapOffset + 22 + resourceMapData = self._read(ResourceMapHeaderSize, mapOffset) + sstruct.unpack(ResourceMapHeader, resourceMapData, self) + self.absTypeListOffset = self.mapOffset + self.typeListOffset + self.absNameListOffset = self.mapOffset + self.nameListOffset + + def _readTypeList(self): + absTypeListOffset = self.absTypeListOffset + numTypesData = self._read(2, absTypeListOffset) + (self.numTypes,) = struct.unpack(">H", numTypesData) + absTypeListOffset2 = absTypeListOffset + 2 + for i in range(self.numTypes + 1): + resTypeItemOffset = absTypeListOffset2 + ResourceTypeItemSize * i + resTypeItemData = self._read(ResourceTypeItemSize, resTypeItemOffset) + item = sstruct.unpack(ResourceTypeItem, resTypeItemData) + resType = tostr(item["type"], encoding="mac-roman") + refListOffset = absTypeListOffset + item["refListOffset"] + numRes = item["numRes"] + 1 + resources = self._readReferenceList(resType, refListOffset, numRes) + self._resources[resType] = resources + + def _readReferenceList(self, resType, refListOffset, numRes): + resources = [] + for i in range(numRes): + refOffset = refListOffset + ResourceRefItemSize * i + refData = self._read(ResourceRefItemSize, refOffset) + res = Resource(resType) + res.decompile(refData, self) + resources.append(res) + return resources + + def __getitem__(self, resType): + return self._resources[resType] + + def __delitem__(self, resType): + del self._resources[resType] + + def __setitem__(self, resType, resources): + self._resources[resType] = resources + + def __len__(self): + return len(self._resources) + + def __iter__(self): + return iter(self._resources) + + def keys(self): + return self._resources.keys() + + @property + def types(self): + """A list of the types of resources in the resource fork.""" + return list(self._resources.keys()) + + def countResources(self, resType): + """Return the number of resources of a given type.""" + try: + return len(self[resType]) + except KeyError: + return 0 + + def getIndices(self, resType): + """Returns a list of indices of resources of a given type.""" + numRes = self.countResources(resType) + if numRes: + return list(range(1, numRes + 1)) + else: + return [] + + def getNames(self, resType): + """Return list of names of all resources of a given type.""" + return [res.name for res in self.get(resType, []) if res.name is not None] + + def getIndResource(self, resType, index): + """Return resource of given type located at an index ranging from 1 + to the number of resources for that type, or None if not found. + """ + if index < 1: + return None + try: + res = self[resType][index - 1] + except (KeyError, IndexError): + return None + return res + + def getNamedResource(self, resType, name): + """Return the named resource of given type, else return None.""" + name = tostr(name, encoding="mac-roman") + for res in self.get(resType, []): + if res.name == name: + return res + return None + + def close(self): + if not self.file.closed: + self.file.close() class Resource(object): - """Represents a resource stored within a resource fork. - - Attributes: - type: resource type. - data: resource data. - id: ID. - name: resource name. - attr: attributes. - """ - - def __init__(self, resType=None, resData=None, resID=None, resName=None, - resAttr=None): - self.type = resType - self.data = resData - self.id = resID - self.name = resName - self.attr = resAttr - - def decompile(self, refData, reader): - sstruct.unpack(ResourceRefItem, refData, self) - # interpret 3-byte dataOffset as (padded) ULONG to unpack it with struct - self.dataOffset, = struct.unpack('>L', bytesjoin([b"\0", self.dataOffset])) - absDataOffset = reader.dataOffset + self.dataOffset - dataLength, = struct.unpack(">L", reader._read(4, absDataOffset)) - self.data = reader._read(dataLength) - if self.nameOffset == -1: - return - absNameOffset = reader.absNameListOffset + self.nameOffset - nameLength, = struct.unpack('B', reader._read(1, absNameOffset)) - name, = struct.unpack('>%ss' % nameLength, reader._read(nameLength)) - self.name = tostr(name, encoding='mac-roman') + """Represents a resource stored within a resource fork. + + Attributes: + type: resource type. + data: resource data. + id: ID. + name: resource name. + attr: attributes. + """ + + def __init__( + self, resType=None, resData=None, resID=None, resName=None, resAttr=None + ): + self.type = resType + self.data = resData + self.id = resID + self.name = resName + self.attr = resAttr + + def decompile(self, refData, reader): + sstruct.unpack(ResourceRefItem, refData, self) + # interpret 3-byte dataOffset as (padded) ULONG to unpack it with struct + (self.dataOffset,) = struct.unpack(">L", bytesjoin([b"\0", self.dataOffset])) + absDataOffset = reader.dataOffset + self.dataOffset + (dataLength,) = struct.unpack(">L", reader._read(4, absDataOffset)) + self.data = reader._read(dataLength) + if self.nameOffset == -1: + return + absNameOffset = reader.absNameListOffset + self.nameOffset + (nameLength,) = struct.unpack("B", reader._read(1, absNameOffset)) + (name,) = struct.unpack(">%ss" % nameLength, reader._read(nameLength)) + self.name = tostr(name, encoding="mac-roman") ResourceForkHeader = """ diff --git a/Lib/fontTools/misc/plistlib/__init__.py b/Lib/fontTools/misc/plistlib/__init__.py index eb4b5259..066eef38 100644 --- a/Lib/fontTools/misc/plistlib/__init__.py +++ b/Lib/fontTools/misc/plistlib/__init__.py @@ -176,7 +176,7 @@ class PlistTarget: True Links: - https://github.com/python/cpython/blob/master/Lib/plistlib.py + https://github.com/python/cpython/blob/main/Lib/plistlib.py http://lxml.de/parsing.html#the-target-parser-interface """ @@ -353,7 +353,9 @@ def _real_element(value: float, ctx: SimpleNamespace) -> etree.Element: return el -def _dict_element(d: Mapping[str, PlistEncodable], ctx: SimpleNamespace) -> etree.Element: +def _dict_element( + d: Mapping[str, PlistEncodable], ctx: SimpleNamespace +) -> etree.Element: el = etree.Element("dict") items = d.items() if ctx.sort_keys: @@ -371,7 +373,9 @@ def _dict_element(d: Mapping[str, PlistEncodable], ctx: SimpleNamespace) -> etre return el -def _array_element(array: Sequence[PlistEncodable], ctx: SimpleNamespace) -> etree.Element: +def _array_element( + array: Sequence[PlistEncodable], ctx: SimpleNamespace +) -> etree.Element: el = etree.Element("array") if len(array) == 0: return el diff --git a/Lib/fontTools/misc/psCharStrings.py b/Lib/fontTools/misc/psCharStrings.py index 549dae25..cc9ca01c 100644 --- a/Lib/fontTools/misc/psCharStrings.py +++ b/Lib/fontTools/misc/psCharStrings.py @@ -3,7 +3,10 @@ CFF dictionary data and Type1/Type2 CharStrings. """ from fontTools.misc.fixedTools import ( - fixedToFloat, floatToFixed, floatToFixedToStr, strToFixedToFloat, + fixedToFloat, + floatToFixed, + floatToFixedToStr, + strToFixedToFloat, ) from fontTools.misc.textTools import bytechr, byteord, bytesjoin, strjoin from fontTools.pens.boundsPen import BoundsPen @@ -15,59 +18,67 @@ log = logging.getLogger(__name__) def read_operator(self, b0, data, index): - if b0 == 12: - op = (b0, byteord(data[index])) - index = index+1 - else: - op = b0 - try: - operator = self.operators[op] - except KeyError: - return None, index - value = self.handle_operator(operator) - return value, index + if b0 == 12: + op = (b0, byteord(data[index])) + index = index + 1 + else: + op = b0 + try: + operator = self.operators[op] + except KeyError: + return None, index + value = self.handle_operator(operator) + return value, index + def read_byte(self, b0, data, index): - return b0 - 139, index + return b0 - 139, index + def read_smallInt1(self, b0, data, index): - b1 = byteord(data[index]) - return (b0-247)*256 + b1 + 108, index+1 + b1 = byteord(data[index]) + return (b0 - 247) * 256 + b1 + 108, index + 1 + def read_smallInt2(self, b0, data, index): - b1 = byteord(data[index]) - return -(b0-251)*256 - b1 - 108, index+1 + b1 = byteord(data[index]) + return -(b0 - 251) * 256 - b1 - 108, index + 1 + def read_shortInt(self, b0, data, index): - value, = struct.unpack(">h", data[index:index+2]) - return value, index+2 + (value,) = struct.unpack(">h", data[index : index + 2]) + return value, index + 2 + def read_longInt(self, b0, data, index): - value, = struct.unpack(">l", data[index:index+4]) - return value, index+4 + (value,) = struct.unpack(">l", data[index : index + 4]) + return value, index + 4 + def read_fixed1616(self, b0, data, index): - value, = struct.unpack(">l", data[index:index+4]) - return fixedToFloat(value, precisionBits=16), index+4 + (value,) = struct.unpack(">l", data[index : index + 4]) + return fixedToFloat(value, precisionBits=16), index + 4 + def read_reserved(self, b0, data, index): - assert NotImplementedError - return NotImplemented, index + assert NotImplementedError + return NotImplemented, index + def read_realNumber(self, b0, data, index): - number = '' - while True: - b = byteord(data[index]) - index = index + 1 - nibble0 = (b & 0xf0) >> 4 - nibble1 = b & 0x0f - if nibble0 == 0xf: - break - number = number + realNibbles[nibble0] - if nibble1 == 0xf: - break - number = number + realNibbles[nibble1] - return float(number), index + number = "" + while True: + b = byteord(data[index]) + index = index + 1 + nibble0 = (b & 0xF0) >> 4 + nibble1 = b & 0x0F + if nibble0 == 0xF: + break + number = number + realNibbles[nibble0] + if nibble1 == 0xF: + break + number = number + realNibbles[nibble1] + return float(number), index t1OperandEncoding = [None] * 256 @@ -88,1229 +99,1378 @@ cffDictOperandEncoding[30] = read_realNumber cffDictOperandEncoding[255] = read_reserved -realNibbles = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '.', 'E', 'E-', None, '-'] -realNibblesDict = {v:i for i,v in enumerate(realNibbles)} +realNibbles = [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ".", + "E", + "E-", + None, + "-", +] +realNibblesDict = {v: i for i, v in enumerate(realNibbles)} maxOpStack = 193 def buildOperatorDict(operatorList): - oper = {} - opc = {} - for item in operatorList: - if len(item) == 2: - oper[item[0]] = item[1] - else: - oper[item[0]] = item[1:] - if isinstance(item[0], tuple): - opc[item[1]] = item[0] - else: - opc[item[1]] = (item[0],) - return oper, opc + oper = {} + opc = {} + for item in operatorList: + if len(item) == 2: + oper[item[0]] = item[1] + else: + oper[item[0]] = item[1:] + if isinstance(item[0], tuple): + opc[item[1]] = item[0] + else: + opc[item[1]] = (item[0],) + return oper, opc t2Operators = [ -# opcode name - (1, 'hstem'), - (3, 'vstem'), - (4, 'vmoveto'), - (5, 'rlineto'), - (6, 'hlineto'), - (7, 'vlineto'), - (8, 'rrcurveto'), - (10, 'callsubr'), - (11, 'return'), - (14, 'endchar'), - (15, 'vsindex'), - (16, 'blend'), - (18, 'hstemhm'), - (19, 'hintmask'), - (20, 'cntrmask'), - (21, 'rmoveto'), - (22, 'hmoveto'), - (23, 'vstemhm'), - (24, 'rcurveline'), - (25, 'rlinecurve'), - (26, 'vvcurveto'), - (27, 'hhcurveto'), -# (28, 'shortint'), # not really an operator - (29, 'callgsubr'), - (30, 'vhcurveto'), - (31, 'hvcurveto'), - ((12, 0), 'ignore'), # dotsection. Yes, there a few very early OTF/CFF - # fonts with this deprecated operator. Just ignore it. - ((12, 3), 'and'), - ((12, 4), 'or'), - ((12, 5), 'not'), - ((12, 8), 'store'), - ((12, 9), 'abs'), - ((12, 10), 'add'), - ((12, 11), 'sub'), - ((12, 12), 'div'), - ((12, 13), 'load'), - ((12, 14), 'neg'), - ((12, 15), 'eq'), - ((12, 18), 'drop'), - ((12, 20), 'put'), - ((12, 21), 'get'), - ((12, 22), 'ifelse'), - ((12, 23), 'random'), - ((12, 24), 'mul'), - ((12, 26), 'sqrt'), - ((12, 27), 'dup'), - ((12, 28), 'exch'), - ((12, 29), 'index'), - ((12, 30), 'roll'), - ((12, 34), 'hflex'), - ((12, 35), 'flex'), - ((12, 36), 'hflex1'), - ((12, 37), 'flex1'), + # opcode name + (1, "hstem"), + (3, "vstem"), + (4, "vmoveto"), + (5, "rlineto"), + (6, "hlineto"), + (7, "vlineto"), + (8, "rrcurveto"), + (10, "callsubr"), + (11, "return"), + (14, "endchar"), + (15, "vsindex"), + (16, "blend"), + (18, "hstemhm"), + (19, "hintmask"), + (20, "cntrmask"), + (21, "rmoveto"), + (22, "hmoveto"), + (23, "vstemhm"), + (24, "rcurveline"), + (25, "rlinecurve"), + (26, "vvcurveto"), + (27, "hhcurveto"), + # (28, 'shortint'), # not really an operator + (29, "callgsubr"), + (30, "vhcurveto"), + (31, "hvcurveto"), + ((12, 0), "ignore"), # dotsection. Yes, there a few very early OTF/CFF + # fonts with this deprecated operator. Just ignore it. + ((12, 3), "and"), + ((12, 4), "or"), + ((12, 5), "not"), + ((12, 8), "store"), + ((12, 9), "abs"), + ((12, 10), "add"), + ((12, 11), "sub"), + ((12, 12), "div"), + ((12, 13), "load"), + ((12, 14), "neg"), + ((12, 15), "eq"), + ((12, 18), "drop"), + ((12, 20), "put"), + ((12, 21), "get"), + ((12, 22), "ifelse"), + ((12, 23), "random"), + ((12, 24), "mul"), + ((12, 26), "sqrt"), + ((12, 27), "dup"), + ((12, 28), "exch"), + ((12, 29), "index"), + ((12, 30), "roll"), + ((12, 34), "hflex"), + ((12, 35), "flex"), + ((12, 36), "hflex1"), + ((12, 37), "flex1"), ] + def getIntEncoder(format): - if format == "cff": - fourByteOp = bytechr(29) - elif format == "t1": - fourByteOp = bytechr(255) - else: - assert format == "t2" - fourByteOp = None - - def encodeInt(value, fourByteOp=fourByteOp, bytechr=bytechr, - pack=struct.pack, unpack=struct.unpack): - if -107 <= value <= 107: - code = bytechr(value + 139) - elif 108 <= value <= 1131: - value = value - 108 - code = bytechr((value >> 8) + 247) + bytechr(value & 0xFF) - elif -1131 <= value <= -108: - value = -value - 108 - code = bytechr((value >> 8) + 251) + bytechr(value & 0xFF) - elif fourByteOp is None: - # T2 only supports 2 byte ints - if -32768 <= value <= 32767: - code = bytechr(28) + pack(">h", value) - else: - # Backwards compatible hack: due to a previous bug in FontTools, - # 16.16 fixed numbers were written out as 4-byte ints. When - # these numbers were small, they were wrongly written back as - # small ints instead of 4-byte ints, breaking round-tripping. - # This here workaround doesn't do it any better, since we can't - # distinguish anymore between small ints that were supposed to - # be small fixed numbers and small ints that were just small - # ints. Hence the warning. - log.warning("4-byte T2 number got passed to the " - "IntType handler. This should happen only when reading in " - "old XML files.\n") - code = bytechr(255) + pack(">l", value) - else: - code = fourByteOp + pack(">l", value) - return code - - return encodeInt + if format == "cff": + twoByteOp = bytechr(28) + fourByteOp = bytechr(29) + elif format == "t1": + twoByteOp = None + fourByteOp = bytechr(255) + else: + assert format == "t2" + twoByteOp = bytechr(28) + fourByteOp = None + + def encodeInt( + value, + fourByteOp=fourByteOp, + bytechr=bytechr, + pack=struct.pack, + unpack=struct.unpack, + twoByteOp=twoByteOp, + ): + if -107 <= value <= 107: + code = bytechr(value + 139) + elif 108 <= value <= 1131: + value = value - 108 + code = bytechr((value >> 8) + 247) + bytechr(value & 0xFF) + elif -1131 <= value <= -108: + value = -value - 108 + code = bytechr((value >> 8) + 251) + bytechr(value & 0xFF) + elif twoByteOp is not None and -32768 <= value <= 32767: + code = twoByteOp + pack(">h", value) + elif fourByteOp is None: + # Backwards compatible hack: due to a previous bug in FontTools, + # 16.16 fixed numbers were written out as 4-byte ints. When + # these numbers were small, they were wrongly written back as + # small ints instead of 4-byte ints, breaking round-tripping. + # This here workaround doesn't do it any better, since we can't + # distinguish anymore between small ints that were supposed to + # be small fixed numbers and small ints that were just small + # ints. Hence the warning. + log.warning( + "4-byte T2 number got passed to the " + "IntType handler. This should happen only when reading in " + "old XML files.\n" + ) + code = bytechr(255) + pack(">l", value) + else: + code = fourByteOp + pack(">l", value) + return code + + return encodeInt encodeIntCFF = getIntEncoder("cff") encodeIntT1 = getIntEncoder("t1") encodeIntT2 = getIntEncoder("t2") + def encodeFixed(f, pack=struct.pack): - """For T2 only""" - value = floatToFixed(f, precisionBits=16) - if value & 0xFFFF == 0: # check if the fractional part is zero - return encodeIntT2(value >> 16) # encode only the integer part - else: - return b"\xff" + pack(">l", value) # encode the entire fixed point value + """For T2 only""" + value = floatToFixed(f, precisionBits=16) + if value & 0xFFFF == 0: # check if the fractional part is zero + return encodeIntT2(value >> 16) # encode only the integer part + else: + return b"\xff" + pack(">l", value) # encode the entire fixed point value + +realZeroBytes = bytechr(30) + bytechr(0xF) -realZeroBytes = bytechr(30) + bytechr(0xf) def encodeFloat(f): - # For CFF only, used in cffLib - if f == 0.0: # 0.0 == +0.0 == -0.0 - return realZeroBytes - # Note: 14 decimal digits seems to be the limitation for CFF real numbers - # in macOS. However, we use 8 here to match the implementation of AFDKO. - s = "%.8G" % f - if s[:2] == "0.": - s = s[1:] - elif s[:3] == "-0.": - s = "-" + s[2:] - nibbles = [] - while s: - c = s[0] - s = s[1:] - if c == "E": - c2 = s[:1] - if c2 == "-": - s = s[1:] - c = "E-" - elif c2 == "+": - s = s[1:] - nibbles.append(realNibblesDict[c]) - nibbles.append(0xf) - if len(nibbles) % 2: - nibbles.append(0xf) - d = bytechr(30) - for i in range(0, len(nibbles), 2): - d = d + bytechr(nibbles[i] << 4 | nibbles[i+1]) - return d - - -class CharStringCompileError(Exception): pass + # For CFF only, used in cffLib + if f == 0.0: # 0.0 == +0.0 == -0.0 + return realZeroBytes + # Note: 14 decimal digits seems to be the limitation for CFF real numbers + # in macOS. However, we use 8 here to match the implementation of AFDKO. + s = "%.8G" % f + if s[:2] == "0.": + s = s[1:] + elif s[:3] == "-0.": + s = "-" + s[2:] + nibbles = [] + while s: + c = s[0] + s = s[1:] + if c == "E": + c2 = s[:1] + if c2 == "-": + s = s[1:] + c = "E-" + elif c2 == "+": + s = s[1:] + nibbles.append(realNibblesDict[c]) + nibbles.append(0xF) + if len(nibbles) % 2: + nibbles.append(0xF) + d = bytechr(30) + for i in range(0, len(nibbles), 2): + d = d + bytechr(nibbles[i] << 4 | nibbles[i + 1]) + return d + + +class CharStringCompileError(Exception): + pass class SimpleT2Decompiler(object): + def __init__(self, localSubrs, globalSubrs, private=None, blender=None): + self.localSubrs = localSubrs + self.localBias = calcSubrBias(localSubrs) + self.globalSubrs = globalSubrs + self.globalBias = calcSubrBias(globalSubrs) + self.private = private + self.blender = blender + self.reset() + + def reset(self): + self.callingStack = [] + self.operandStack = [] + self.hintCount = 0 + self.hintMaskBytes = 0 + self.numRegions = 0 + self.vsIndex = 0 + + def execute(self, charString): + self.callingStack.append(charString) + needsDecompilation = charString.needsDecompilation() + if needsDecompilation: + program = [] + pushToProgram = program.append + else: + pushToProgram = lambda x: None + pushToStack = self.operandStack.append + index = 0 + while True: + token, isOperator, index = charString.getToken(index) + if token is None: + break # we're done! + pushToProgram(token) + if isOperator: + handlerName = "op_" + token + handler = getattr(self, handlerName, None) + if handler is not None: + rv = handler(index) + if rv: + hintMaskBytes, index = rv + pushToProgram(hintMaskBytes) + else: + self.popall() + else: + pushToStack(token) + if needsDecompilation: + charString.setProgram(program) + del self.callingStack[-1] + + def pop(self): + value = self.operandStack[-1] + del self.operandStack[-1] + return value + + def popall(self): + stack = self.operandStack[:] + self.operandStack[:] = [] + return stack + + def push(self, value): + self.operandStack.append(value) + + def op_return(self, index): + if self.operandStack: + pass + + def op_endchar(self, index): + pass + + def op_ignore(self, index): + pass + + def op_callsubr(self, index): + subrIndex = self.pop() + subr = self.localSubrs[subrIndex + self.localBias] + self.execute(subr) + + def op_callgsubr(self, index): + subrIndex = self.pop() + subr = self.globalSubrs[subrIndex + self.globalBias] + self.execute(subr) + + def op_hstem(self, index): + self.countHints() + + def op_vstem(self, index): + self.countHints() + + def op_hstemhm(self, index): + self.countHints() + + def op_vstemhm(self, index): + self.countHints() + + def op_hintmask(self, index): + if not self.hintMaskBytes: + self.countHints() + self.hintMaskBytes = (self.hintCount + 7) // 8 + hintMaskBytes, index = self.callingStack[-1].getBytes(index, self.hintMaskBytes) + return hintMaskBytes, index + + op_cntrmask = op_hintmask + + def countHints(self): + args = self.popall() + self.hintCount = self.hintCount + len(args) // 2 + + # misc + def op_and(self, index): + raise NotImplementedError + + def op_or(self, index): + raise NotImplementedError + + def op_not(self, index): + raise NotImplementedError + + def op_store(self, index): + raise NotImplementedError + + def op_abs(self, index): + raise NotImplementedError + + def op_add(self, index): + raise NotImplementedError + + def op_sub(self, index): + raise NotImplementedError + + def op_div(self, index): + raise NotImplementedError + + def op_load(self, index): + raise NotImplementedError - def __init__(self, localSubrs, globalSubrs, private=None): - self.localSubrs = localSubrs - self.localBias = calcSubrBias(localSubrs) - self.globalSubrs = globalSubrs - self.globalBias = calcSubrBias(globalSubrs) - self.private = private - self.reset() - - def reset(self): - self.callingStack = [] - self.operandStack = [] - self.hintCount = 0 - self.hintMaskBytes = 0 - self.numRegions = 0 - - def execute(self, charString): - self.callingStack.append(charString) - needsDecompilation = charString.needsDecompilation() - if needsDecompilation: - program = [] - pushToProgram = program.append - else: - pushToProgram = lambda x: None - pushToStack = self.operandStack.append - index = 0 - while True: - token, isOperator, index = charString.getToken(index) - if token is None: - break # we're done! - pushToProgram(token) - if isOperator: - handlerName = "op_" + token - handler = getattr(self, handlerName, None) - if handler is not None: - rv = handler(index) - if rv: - hintMaskBytes, index = rv - pushToProgram(hintMaskBytes) - else: - self.popall() - else: - pushToStack(token) - if needsDecompilation: - charString.setProgram(program) - del self.callingStack[-1] - - def pop(self): - value = self.operandStack[-1] - del self.operandStack[-1] - return value - - def popall(self): - stack = self.operandStack[:] - self.operandStack[:] = [] - return stack - - def push(self, value): - self.operandStack.append(value) - - def op_return(self, index): - if self.operandStack: - pass - - def op_endchar(self, index): - pass - - def op_ignore(self, index): - pass - - def op_callsubr(self, index): - subrIndex = self.pop() - subr = self.localSubrs[subrIndex+self.localBias] - self.execute(subr) - - def op_callgsubr(self, index): - subrIndex = self.pop() - subr = self.globalSubrs[subrIndex+self.globalBias] - self.execute(subr) - - def op_hstem(self, index): - self.countHints() - def op_vstem(self, index): - self.countHints() - def op_hstemhm(self, index): - self.countHints() - def op_vstemhm(self, index): - self.countHints() - - def op_hintmask(self, index): - if not self.hintMaskBytes: - self.countHints() - self.hintMaskBytes = (self.hintCount + 7) // 8 - hintMaskBytes, index = self.callingStack[-1].getBytes(index, self.hintMaskBytes) - return hintMaskBytes, index - - op_cntrmask = op_hintmask - - def countHints(self): - args = self.popall() - self.hintCount = self.hintCount + len(args) // 2 - - # misc - def op_and(self, index): - raise NotImplementedError - def op_or(self, index): - raise NotImplementedError - def op_not(self, index): - raise NotImplementedError - def op_store(self, index): - raise NotImplementedError - def op_abs(self, index): - raise NotImplementedError - def op_add(self, index): - raise NotImplementedError - def op_sub(self, index): - raise NotImplementedError - def op_div(self, index): - raise NotImplementedError - def op_load(self, index): - raise NotImplementedError - def op_neg(self, index): - raise NotImplementedError - def op_eq(self, index): - raise NotImplementedError - def op_drop(self, index): - raise NotImplementedError - def op_put(self, index): - raise NotImplementedError - def op_get(self, index): - raise NotImplementedError - def op_ifelse(self, index): - raise NotImplementedError - def op_random(self, index): - raise NotImplementedError - def op_mul(self, index): - raise NotImplementedError - def op_sqrt(self, index): - raise NotImplementedError - def op_dup(self, index): - raise NotImplementedError - def op_exch(self, index): - raise NotImplementedError - def op_index(self, index): - raise NotImplementedError - def op_roll(self, index): - raise NotImplementedError - - # TODO(behdad): move to T2OutlineExtractor and add a 'setVariation' - # method that takes VarStoreData and a location - def op_blend(self, index): - if self.numRegions == 0: - self.numRegions = self.private.getNumRegions() - numBlends = self.pop() - numOps = numBlends * (self.numRegions + 1) - del self.operandStack[-(numOps-numBlends):] # Leave the default operands on the stack. - - def op_vsindex(self, index): - vi = self.pop() - self.numRegions = self.private.getNumRegions(vi) + def op_neg(self, index): + raise NotImplementedError + def op_eq(self, index): + raise NotImplementedError -t1Operators = [ -# opcode name - (1, 'hstem'), - (3, 'vstem'), - (4, 'vmoveto'), - (5, 'rlineto'), - (6, 'hlineto'), - (7, 'vlineto'), - (8, 'rrcurveto'), - (9, 'closepath'), - (10, 'callsubr'), - (11, 'return'), - (13, 'hsbw'), - (14, 'endchar'), - (21, 'rmoveto'), - (22, 'hmoveto'), - (30, 'vhcurveto'), - (31, 'hvcurveto'), - ((12, 0), 'dotsection'), - ((12, 1), 'vstem3'), - ((12, 2), 'hstem3'), - ((12, 6), 'seac'), - ((12, 7), 'sbw'), - ((12, 12), 'div'), - ((12, 16), 'callothersubr'), - ((12, 17), 'pop'), - ((12, 33), 'setcurrentpoint'), -] + def op_drop(self, index): + raise NotImplementedError + def op_put(self, index): + raise NotImplementedError -class T2WidthExtractor(SimpleT2Decompiler): + def op_get(self, index): + raise NotImplementedError + + def op_ifelse(self, index): + raise NotImplementedError + + def op_random(self, index): + raise NotImplementedError + + def op_mul(self, index): + raise NotImplementedError + + def op_sqrt(self, index): + raise NotImplementedError - def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None): - SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private) - self.nominalWidthX = nominalWidthX - self.defaultWidthX = defaultWidthX + def op_dup(self, index): + raise NotImplementedError - def reset(self): - SimpleT2Decompiler.reset(self) - self.gotWidth = 0 - self.width = 0 + def op_exch(self, index): + raise NotImplementedError - def popallWidth(self, evenOdd=0): - args = self.popall() - if not self.gotWidth: - if evenOdd ^ (len(args) % 2): - # For CFF2 charstrings, this should never happen - assert self.defaultWidthX is not None, "CFF2 CharStrings must not have an initial width value" - self.width = self.nominalWidthX + args[0] - args = args[1:] - else: - self.width = self.defaultWidthX - self.gotWidth = 1 - return args + def op_index(self, index): + raise NotImplementedError - def countHints(self): - args = self.popallWidth() - self.hintCount = self.hintCount + len(args) // 2 + def op_roll(self, index): + raise NotImplementedError - def op_rmoveto(self, index): - self.popallWidth() + def op_blend(self, index): + if self.numRegions == 0: + self.numRegions = self.private.getNumRegions() + numBlends = self.pop() + numOps = numBlends * (self.numRegions + 1) + if self.blender is None: + del self.operandStack[ + -(numOps - numBlends) : + ] # Leave the default operands on the stack. + else: + argi = len(self.operandStack) - numOps + end_args = tuplei = argi + numBlends + while argi < end_args: + next_ti = tuplei + self.numRegions + deltas = self.operandStack[tuplei:next_ti] + delta = self.blender(self.vsIndex, deltas) + self.operandStack[argi] += delta + tuplei = next_ti + argi += 1 + self.operandStack[end_args:] = [] - def op_hmoveto(self, index): - self.popallWidth(1) + def op_vsindex(self, index): + vi = self.pop() + self.vsIndex = vi + self.numRegions = self.private.getNumRegions(vi) - def op_vmoveto(self, index): - self.popallWidth(1) - def op_endchar(self, index): - self.popallWidth() +t1Operators = [ + # opcode name + (1, "hstem"), + (3, "vstem"), + (4, "vmoveto"), + (5, "rlineto"), + (6, "hlineto"), + (7, "vlineto"), + (8, "rrcurveto"), + (9, "closepath"), + (10, "callsubr"), + (11, "return"), + (13, "hsbw"), + (14, "endchar"), + (21, "rmoveto"), + (22, "hmoveto"), + (30, "vhcurveto"), + (31, "hvcurveto"), + ((12, 0), "dotsection"), + ((12, 1), "vstem3"), + ((12, 2), "hstem3"), + ((12, 6), "seac"), + ((12, 7), "sbw"), + ((12, 12), "div"), + ((12, 16), "callothersubr"), + ((12, 17), "pop"), + ((12, 33), "setcurrentpoint"), +] + + +class T2WidthExtractor(SimpleT2Decompiler): + def __init__( + self, + localSubrs, + globalSubrs, + nominalWidthX, + defaultWidthX, + private=None, + blender=None, + ): + SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private, blender) + self.nominalWidthX = nominalWidthX + self.defaultWidthX = defaultWidthX + + def reset(self): + SimpleT2Decompiler.reset(self) + self.gotWidth = 0 + self.width = 0 + + def popallWidth(self, evenOdd=0): + args = self.popall() + if not self.gotWidth: + if evenOdd ^ (len(args) % 2): + # For CFF2 charstrings, this should never happen + assert ( + self.defaultWidthX is not None + ), "CFF2 CharStrings must not have an initial width value" + self.width = self.nominalWidthX + args[0] + args = args[1:] + else: + self.width = self.defaultWidthX + self.gotWidth = 1 + return args + + def countHints(self): + args = self.popallWidth() + self.hintCount = self.hintCount + len(args) // 2 + + def op_rmoveto(self, index): + self.popallWidth() + + def op_hmoveto(self, index): + self.popallWidth(1) + + def op_vmoveto(self, index): + self.popallWidth(1) + + def op_endchar(self, index): + self.popallWidth() class T2OutlineExtractor(T2WidthExtractor): + def __init__( + self, + pen, + localSubrs, + globalSubrs, + nominalWidthX, + defaultWidthX, + private=None, + blender=None, + ): + T2WidthExtractor.__init__( + self, + localSubrs, + globalSubrs, + nominalWidthX, + defaultWidthX, + private, + blender, + ) + self.pen = pen + self.subrLevel = 0 + + def reset(self): + T2WidthExtractor.reset(self) + self.currentPoint = (0, 0) + self.sawMoveTo = 0 + self.subrLevel = 0 + + def execute(self, charString): + self.subrLevel += 1 + super().execute(charString) + self.subrLevel -= 1 + if self.subrLevel == 0: + self.endPath() + + def _nextPoint(self, point): + x, y = self.currentPoint + point = x + point[0], y + point[1] + self.currentPoint = point + return point + + def rMoveTo(self, point): + self.pen.moveTo(self._nextPoint(point)) + self.sawMoveTo = 1 + + def rLineTo(self, point): + if not self.sawMoveTo: + self.rMoveTo((0, 0)) + self.pen.lineTo(self._nextPoint(point)) + + def rCurveTo(self, pt1, pt2, pt3): + if not self.sawMoveTo: + self.rMoveTo((0, 0)) + nextPoint = self._nextPoint + self.pen.curveTo(nextPoint(pt1), nextPoint(pt2), nextPoint(pt3)) + + def closePath(self): + if self.sawMoveTo: + self.pen.closePath() + self.sawMoveTo = 0 + + def endPath(self): + # In T2 there are no open paths, so always do a closePath when + # finishing a sub path. We avoid spurious calls to closePath() + # because its a real T1 op we're emulating in T2 whereas + # endPath() is just a means to that emulation + if self.sawMoveTo: + self.closePath() + + # + # hint operators + # + # def op_hstem(self, index): + # self.countHints() + # def op_vstem(self, index): + # self.countHints() + # def op_hstemhm(self, index): + # self.countHints() + # def op_vstemhm(self, index): + # self.countHints() + # def op_hintmask(self, index): + # self.countHints() + # def op_cntrmask(self, index): + # self.countHints() + + # + # path constructors, moveto + # + def op_rmoveto(self, index): + self.endPath() + self.rMoveTo(self.popallWidth()) + + def op_hmoveto(self, index): + self.endPath() + self.rMoveTo((self.popallWidth(1)[0], 0)) + + def op_vmoveto(self, index): + self.endPath() + self.rMoveTo((0, self.popallWidth(1)[0])) + + def op_endchar(self, index): + self.endPath() + args = self.popallWidth() + if args: + from fontTools.encodings.StandardEncoding import StandardEncoding + + # endchar can do seac accent bulding; The T2 spec says it's deprecated, + # but recent software that shall remain nameless does output it. + adx, ady, bchar, achar = args + baseGlyph = StandardEncoding[bchar] + self.pen.addComponent(baseGlyph, (1, 0, 0, 1, 0, 0)) + accentGlyph = StandardEncoding[achar] + self.pen.addComponent(accentGlyph, (1, 0, 0, 1, adx, ady)) + + # + # path constructors, lines + # + def op_rlineto(self, index): + args = self.popall() + for i in range(0, len(args), 2): + point = args[i : i + 2] + self.rLineTo(point) + + def op_hlineto(self, index): + self.alternatingLineto(1) + + def op_vlineto(self, index): + self.alternatingLineto(0) + + # + # path constructors, curves + # + def op_rrcurveto(self, index): + """{dxa dya dxb dyb dxc dyc}+ rrcurveto""" + args = self.popall() + for i in range(0, len(args), 6): + ( + dxa, + dya, + dxb, + dyb, + dxc, + dyc, + ) = args[i : i + 6] + self.rCurveTo((dxa, dya), (dxb, dyb), (dxc, dyc)) + + def op_rcurveline(self, index): + """{dxa dya dxb dyb dxc dyc}+ dxd dyd rcurveline""" + args = self.popall() + for i in range(0, len(args) - 2, 6): + dxb, dyb, dxc, dyc, dxd, dyd = args[i : i + 6] + self.rCurveTo((dxb, dyb), (dxc, dyc), (dxd, dyd)) + self.rLineTo(args[-2:]) + + def op_rlinecurve(self, index): + """{dxa dya}+ dxb dyb dxc dyc dxd dyd rlinecurve""" + args = self.popall() + lineArgs = args[:-6] + for i in range(0, len(lineArgs), 2): + self.rLineTo(lineArgs[i : i + 2]) + dxb, dyb, dxc, dyc, dxd, dyd = args[-6:] + self.rCurveTo((dxb, dyb), (dxc, dyc), (dxd, dyd)) + + def op_vvcurveto(self, index): + "dx1? {dya dxb dyb dyc}+ vvcurveto" + args = self.popall() + if len(args) % 2: + dx1 = args[0] + args = args[1:] + else: + dx1 = 0 + for i in range(0, len(args), 4): + dya, dxb, dyb, dyc = args[i : i + 4] + self.rCurveTo((dx1, dya), (dxb, dyb), (0, dyc)) + dx1 = 0 + + def op_hhcurveto(self, index): + """dy1? {dxa dxb dyb dxc}+ hhcurveto""" + args = self.popall() + if len(args) % 2: + dy1 = args[0] + args = args[1:] + else: + dy1 = 0 + for i in range(0, len(args), 4): + dxa, dxb, dyb, dxc = args[i : i + 4] + self.rCurveTo((dxa, dy1), (dxb, dyb), (dxc, 0)) + dy1 = 0 + + def op_vhcurveto(self, index): + """dy1 dx2 dy2 dx3 {dxa dxb dyb dyc dyd dxe dye dxf}* dyf? vhcurveto (30) + {dya dxb dyb dxc dxd dxe dye dyf}+ dxf? vhcurveto + """ + args = self.popall() + while args: + args = self.vcurveto(args) + if args: + args = self.hcurveto(args) + + def op_hvcurveto(self, index): + """dx1 dx2 dy2 dy3 {dya dxb dyb dxc dxd dxe dye dyf}* dxf? + {dxa dxb dyb dyc dyd dxe dye dxf}+ dyf? + """ + args = self.popall() + while args: + args = self.hcurveto(args) + if args: + args = self.vcurveto(args) + + # + # path constructors, flex + # + def op_hflex(self, index): + dx1, dx2, dy2, dx3, dx4, dx5, dx6 = self.popall() + dy1 = dy3 = dy4 = dy6 = 0 + dy5 = -dy2 + self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) + self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) + + def op_flex(self, index): + dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, dx6, dy6, fd = self.popall() + self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) + self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) + + def op_hflex1(self, index): + dx1, dy1, dx2, dy2, dx3, dx4, dx5, dy5, dx6 = self.popall() + dy3 = dy4 = 0 + dy6 = -(dy1 + dy2 + dy3 + dy4 + dy5) + + self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) + self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) + + def op_flex1(self, index): + dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, d6 = self.popall() + dx = dx1 + dx2 + dx3 + dx4 + dx5 + dy = dy1 + dy2 + dy3 + dy4 + dy5 + if abs(dx) > abs(dy): + dx6 = d6 + dy6 = -dy + else: + dx6 = -dx + dy6 = d6 + self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) + self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) + + # misc + def op_and(self, index): + raise NotImplementedError + + def op_or(self, index): + raise NotImplementedError + + def op_not(self, index): + raise NotImplementedError + + def op_store(self, index): + raise NotImplementedError + + def op_abs(self, index): + raise NotImplementedError + + def op_add(self, index): + raise NotImplementedError + + def op_sub(self, index): + raise NotImplementedError + + def op_div(self, index): + num2 = self.pop() + num1 = self.pop() + d1 = num1 // num2 + d2 = num1 / num2 + if d1 == d2: + self.push(d1) + else: + self.push(d2) + + def op_load(self, index): + raise NotImplementedError + + def op_neg(self, index): + raise NotImplementedError + + def op_eq(self, index): + raise NotImplementedError + + def op_drop(self, index): + raise NotImplementedError + + def op_put(self, index): + raise NotImplementedError + + def op_get(self, index): + raise NotImplementedError + + def op_ifelse(self, index): + raise NotImplementedError + + def op_random(self, index): + raise NotImplementedError + + def op_mul(self, index): + raise NotImplementedError + + def op_sqrt(self, index): + raise NotImplementedError + + def op_dup(self, index): + raise NotImplementedError + + def op_exch(self, index): + raise NotImplementedError + + def op_index(self, index): + raise NotImplementedError + + def op_roll(self, index): + raise NotImplementedError + + # + # miscellaneous helpers + # + def alternatingLineto(self, isHorizontal): + args = self.popall() + for arg in args: + if isHorizontal: + point = (arg, 0) + else: + point = (0, arg) + self.rLineTo(point) + isHorizontal = not isHorizontal + + def vcurveto(self, args): + dya, dxb, dyb, dxc = args[:4] + args = args[4:] + if len(args) == 1: + dyc = args[0] + args = [] + else: + dyc = 0 + self.rCurveTo((0, dya), (dxb, dyb), (dxc, dyc)) + return args + + def hcurveto(self, args): + dxa, dxb, dyb, dyc = args[:4] + args = args[4:] + if len(args) == 1: + dxc = args[0] + args = [] + else: + dxc = 0 + self.rCurveTo((dxa, 0), (dxb, dyb), (dxc, dyc)) + return args - def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None): - T2WidthExtractor.__init__( - self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private) - self.pen = pen - self.subrLevel = 0 - - def reset(self): - T2WidthExtractor.reset(self) - self.currentPoint = (0, 0) - self.sawMoveTo = 0 - self.subrLevel = 0 - - def execute(self, charString): - self.subrLevel += 1 - super().execute(charString) - self.subrLevel -= 1 - if self.subrLevel == 0: - self.endPath() - - def _nextPoint(self, point): - x, y = self.currentPoint - point = x + point[0], y + point[1] - self.currentPoint = point - return point - - def rMoveTo(self, point): - self.pen.moveTo(self._nextPoint(point)) - self.sawMoveTo = 1 - - def rLineTo(self, point): - if not self.sawMoveTo: - self.rMoveTo((0, 0)) - self.pen.lineTo(self._nextPoint(point)) - - def rCurveTo(self, pt1, pt2, pt3): - if not self.sawMoveTo: - self.rMoveTo((0, 0)) - nextPoint = self._nextPoint - self.pen.curveTo(nextPoint(pt1), nextPoint(pt2), nextPoint(pt3)) - - def closePath(self): - if self.sawMoveTo: - self.pen.closePath() - self.sawMoveTo = 0 - - def endPath(self): - # In T2 there are no open paths, so always do a closePath when - # finishing a sub path. We avoid spurious calls to closePath() - # because its a real T1 op we're emulating in T2 whereas - # endPath() is just a means to that emulation - if self.sawMoveTo: - self.closePath() - - # - # hint operators - # - #def op_hstem(self, index): - # self.countHints() - #def op_vstem(self, index): - # self.countHints() - #def op_hstemhm(self, index): - # self.countHints() - #def op_vstemhm(self, index): - # self.countHints() - #def op_hintmask(self, index): - # self.countHints() - #def op_cntrmask(self, index): - # self.countHints() - - # - # path constructors, moveto - # - def op_rmoveto(self, index): - self.endPath() - self.rMoveTo(self.popallWidth()) - def op_hmoveto(self, index): - self.endPath() - self.rMoveTo((self.popallWidth(1)[0], 0)) - def op_vmoveto(self, index): - self.endPath() - self.rMoveTo((0, self.popallWidth(1)[0])) - def op_endchar(self, index): - self.endPath() - args = self.popallWidth() - if args: - from fontTools.encodings.StandardEncoding import StandardEncoding - # endchar can do seac accent bulding; The T2 spec says it's deprecated, - # but recent software that shall remain nameless does output it. - adx, ady, bchar, achar = args - baseGlyph = StandardEncoding[bchar] - self.pen.addComponent(baseGlyph, (1, 0, 0, 1, 0, 0)) - accentGlyph = StandardEncoding[achar] - self.pen.addComponent(accentGlyph, (1, 0, 0, 1, adx, ady)) - - # - # path constructors, lines - # - def op_rlineto(self, index): - args = self.popall() - for i in range(0, len(args), 2): - point = args[i:i+2] - self.rLineTo(point) - - def op_hlineto(self, index): - self.alternatingLineto(1) - def op_vlineto(self, index): - self.alternatingLineto(0) - - # - # path constructors, curves - # - def op_rrcurveto(self, index): - """{dxa dya dxb dyb dxc dyc}+ rrcurveto""" - args = self.popall() - for i in range(0, len(args), 6): - dxa, dya, dxb, dyb, dxc, dyc, = args[i:i+6] - self.rCurveTo((dxa, dya), (dxb, dyb), (dxc, dyc)) - - def op_rcurveline(self, index): - """{dxa dya dxb dyb dxc dyc}+ dxd dyd rcurveline""" - args = self.popall() - for i in range(0, len(args)-2, 6): - dxb, dyb, dxc, dyc, dxd, dyd = args[i:i+6] - self.rCurveTo((dxb, dyb), (dxc, dyc), (dxd, dyd)) - self.rLineTo(args[-2:]) - - def op_rlinecurve(self, index): - """{dxa dya}+ dxb dyb dxc dyc dxd dyd rlinecurve""" - args = self.popall() - lineArgs = args[:-6] - for i in range(0, len(lineArgs), 2): - self.rLineTo(lineArgs[i:i+2]) - dxb, dyb, dxc, dyc, dxd, dyd = args[-6:] - self.rCurveTo((dxb, dyb), (dxc, dyc), (dxd, dyd)) - - def op_vvcurveto(self, index): - "dx1? {dya dxb dyb dyc}+ vvcurveto" - args = self.popall() - if len(args) % 2: - dx1 = args[0] - args = args[1:] - else: - dx1 = 0 - for i in range(0, len(args), 4): - dya, dxb, dyb, dyc = args[i:i+4] - self.rCurveTo((dx1, dya), (dxb, dyb), (0, dyc)) - dx1 = 0 - - def op_hhcurveto(self, index): - """dy1? {dxa dxb dyb dxc}+ hhcurveto""" - args = self.popall() - if len(args) % 2: - dy1 = args[0] - args = args[1:] - else: - dy1 = 0 - for i in range(0, len(args), 4): - dxa, dxb, dyb, dxc = args[i:i+4] - self.rCurveTo((dxa, dy1), (dxb, dyb), (dxc, 0)) - dy1 = 0 - - def op_vhcurveto(self, index): - """dy1 dx2 dy2 dx3 {dxa dxb dyb dyc dyd dxe dye dxf}* dyf? vhcurveto (30) - {dya dxb dyb dxc dxd dxe dye dyf}+ dxf? vhcurveto - """ - args = self.popall() - while args: - args = self.vcurveto(args) - if args: - args = self.hcurveto(args) - - def op_hvcurveto(self, index): - """dx1 dx2 dy2 dy3 {dya dxb dyb dxc dxd dxe dye dyf}* dxf? - {dxa dxb dyb dyc dyd dxe dye dxf}+ dyf? - """ - args = self.popall() - while args: - args = self.hcurveto(args) - if args: - args = self.vcurveto(args) - - # - # path constructors, flex - # - def op_hflex(self, index): - dx1, dx2, dy2, dx3, dx4, dx5, dx6 = self.popall() - dy1 = dy3 = dy4 = dy6 = 0 - dy5 = -dy2 - self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) - self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) - def op_flex(self, index): - dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, dx6, dy6, fd = self.popall() - self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) - self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) - def op_hflex1(self, index): - dx1, dy1, dx2, dy2, dx3, dx4, dx5, dy5, dx6 = self.popall() - dy3 = dy4 = 0 - dy6 = -(dy1 + dy2 + dy3 + dy4 + dy5) - - self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) - self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) - def op_flex1(self, index): - dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, d6 = self.popall() - dx = dx1 + dx2 + dx3 + dx4 + dx5 - dy = dy1 + dy2 + dy3 + dy4 + dy5 - if abs(dx) > abs(dy): - dx6 = d6 - dy6 = -dy - else: - dx6 = -dx - dy6 = d6 - self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) - self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) - - # misc - def op_and(self, index): - raise NotImplementedError - def op_or(self, index): - raise NotImplementedError - def op_not(self, index): - raise NotImplementedError - def op_store(self, index): - raise NotImplementedError - def op_abs(self, index): - raise NotImplementedError - def op_add(self, index): - raise NotImplementedError - def op_sub(self, index): - raise NotImplementedError - def op_div(self, index): - num2 = self.pop() - num1 = self.pop() - d1 = num1//num2 - d2 = num1/num2 - if d1 == d2: - self.push(d1) - else: - self.push(d2) - def op_load(self, index): - raise NotImplementedError - def op_neg(self, index): - raise NotImplementedError - def op_eq(self, index): - raise NotImplementedError - def op_drop(self, index): - raise NotImplementedError - def op_put(self, index): - raise NotImplementedError - def op_get(self, index): - raise NotImplementedError - def op_ifelse(self, index): - raise NotImplementedError - def op_random(self, index): - raise NotImplementedError - def op_mul(self, index): - raise NotImplementedError - def op_sqrt(self, index): - raise NotImplementedError - def op_dup(self, index): - raise NotImplementedError - def op_exch(self, index): - raise NotImplementedError - def op_index(self, index): - raise NotImplementedError - def op_roll(self, index): - raise NotImplementedError - - # - # miscellaneous helpers - # - def alternatingLineto(self, isHorizontal): - args = self.popall() - for arg in args: - if isHorizontal: - point = (arg, 0) - else: - point = (0, arg) - self.rLineTo(point) - isHorizontal = not isHorizontal - - def vcurveto(self, args): - dya, dxb, dyb, dxc = args[:4] - args = args[4:] - if len(args) == 1: - dyc = args[0] - args = [] - else: - dyc = 0 - self.rCurveTo((0, dya), (dxb, dyb), (dxc, dyc)) - return args - - def hcurveto(self, args): - dxa, dxb, dyb, dyc = args[:4] - args = args[4:] - if len(args) == 1: - dxc = args[0] - args = [] - else: - dxc = 0 - self.rCurveTo((dxa, 0), (dxb, dyb), (dxc, dyc)) - return args class T1OutlineExtractor(T2OutlineExtractor): + def __init__(self, pen, subrs): + self.pen = pen + self.subrs = subrs + self.reset() + + def reset(self): + self.flexing = 0 + self.width = 0 + self.sbx = 0 + T2OutlineExtractor.reset(self) + + def endPath(self): + if self.sawMoveTo: + self.pen.endPath() + self.sawMoveTo = 0 + + def popallWidth(self, evenOdd=0): + return self.popall() + + def exch(self): + stack = self.operandStack + stack[-1], stack[-2] = stack[-2], stack[-1] + + # + # path constructors + # + def op_rmoveto(self, index): + if self.flexing: + return + self.endPath() + self.rMoveTo(self.popall()) + + def op_hmoveto(self, index): + if self.flexing: + # We must add a parameter to the stack if we are flexing + self.push(0) + return + self.endPath() + self.rMoveTo((self.popall()[0], 0)) + + def op_vmoveto(self, index): + if self.flexing: + # We must add a parameter to the stack if we are flexing + self.push(0) + self.exch() + return + self.endPath() + self.rMoveTo((0, self.popall()[0])) + + def op_closepath(self, index): + self.closePath() + + def op_setcurrentpoint(self, index): + args = self.popall() + x, y = args + self.currentPoint = x, y + + def op_endchar(self, index): + self.endPath() + + def op_hsbw(self, index): + sbx, wx = self.popall() + self.width = wx + self.sbx = sbx + self.currentPoint = sbx, self.currentPoint[1] + + def op_sbw(self, index): + self.popall() # XXX + + # + def op_callsubr(self, index): + subrIndex = self.pop() + subr = self.subrs[subrIndex] + self.execute(subr) + + def op_callothersubr(self, index): + subrIndex = self.pop() + nArgs = self.pop() + # print nArgs, subrIndex, "callothersubr" + if subrIndex == 0 and nArgs == 3: + self.doFlex() + self.flexing = 0 + elif subrIndex == 1 and nArgs == 0: + self.flexing = 1 + # ignore... + + def op_pop(self, index): + pass # ignore... + + def doFlex(self): + finaly = self.pop() + finalx = self.pop() + self.pop() # flex height is unused + + p3y = self.pop() + p3x = self.pop() + bcp4y = self.pop() + bcp4x = self.pop() + bcp3y = self.pop() + bcp3x = self.pop() + p2y = self.pop() + p2x = self.pop() + bcp2y = self.pop() + bcp2x = self.pop() + bcp1y = self.pop() + bcp1x = self.pop() + rpy = self.pop() + rpx = self.pop() + + # call rrcurveto + self.push(bcp1x + rpx) + self.push(bcp1y + rpy) + self.push(bcp2x) + self.push(bcp2y) + self.push(p2x) + self.push(p2y) + self.op_rrcurveto(None) + + # call rrcurveto + self.push(bcp3x) + self.push(bcp3y) + self.push(bcp4x) + self.push(bcp4y) + self.push(p3x) + self.push(p3y) + self.op_rrcurveto(None) + + # Push back final coords so subr 0 can find them + self.push(finalx) + self.push(finaly) + + def op_dotsection(self, index): + self.popall() # XXX + + def op_hstem3(self, index): + self.popall() # XXX + + def op_seac(self, index): + "asb adx ady bchar achar seac" + from fontTools.encodings.StandardEncoding import StandardEncoding + + asb, adx, ady, bchar, achar = self.popall() + baseGlyph = StandardEncoding[bchar] + self.pen.addComponent(baseGlyph, (1, 0, 0, 1, 0, 0)) + accentGlyph = StandardEncoding[achar] + adx = adx + self.sbx - asb # seac weirdness + self.pen.addComponent(accentGlyph, (1, 0, 0, 1, adx, ady)) + + def op_vstem3(self, index): + self.popall() # XXX - def __init__(self, pen, subrs): - self.pen = pen - self.subrs = subrs - self.reset() - - def reset(self): - self.flexing = 0 - self.width = 0 - self.sbx = 0 - T2OutlineExtractor.reset(self) - - def endPath(self): - if self.sawMoveTo: - self.pen.endPath() - self.sawMoveTo = 0 - - def popallWidth(self, evenOdd=0): - return self.popall() - - def exch(self): - stack = self.operandStack - stack[-1], stack[-2] = stack[-2], stack[-1] - - # - # path constructors - # - def op_rmoveto(self, index): - if self.flexing: - return - self.endPath() - self.rMoveTo(self.popall()) - def op_hmoveto(self, index): - if self.flexing: - # We must add a parameter to the stack if we are flexing - self.push(0) - return - self.endPath() - self.rMoveTo((self.popall()[0], 0)) - def op_vmoveto(self, index): - if self.flexing: - # We must add a parameter to the stack if we are flexing - self.push(0) - self.exch() - return - self.endPath() - self.rMoveTo((0, self.popall()[0])) - def op_closepath(self, index): - self.closePath() - def op_setcurrentpoint(self, index): - args = self.popall() - x, y = args - self.currentPoint = x, y - - def op_endchar(self, index): - self.endPath() - - def op_hsbw(self, index): - sbx, wx = self.popall() - self.width = wx - self.sbx = sbx - self.currentPoint = sbx, self.currentPoint[1] - def op_sbw(self, index): - self.popall() # XXX - - # - def op_callsubr(self, index): - subrIndex = self.pop() - subr = self.subrs[subrIndex] - self.execute(subr) - def op_callothersubr(self, index): - subrIndex = self.pop() - nArgs = self.pop() - #print nArgs, subrIndex, "callothersubr" - if subrIndex == 0 and nArgs == 3: - self.doFlex() - self.flexing = 0 - elif subrIndex == 1 and nArgs == 0: - self.flexing = 1 - # ignore... - def op_pop(self, index): - pass # ignore... - - def doFlex(self): - finaly = self.pop() - finalx = self.pop() - self.pop() # flex height is unused - - p3y = self.pop() - p3x = self.pop() - bcp4y = self.pop() - bcp4x = self.pop() - bcp3y = self.pop() - bcp3x = self.pop() - p2y = self.pop() - p2x = self.pop() - bcp2y = self.pop() - bcp2x = self.pop() - bcp1y = self.pop() - bcp1x = self.pop() - rpy = self.pop() - rpx = self.pop() - - # call rrcurveto - self.push(bcp1x+rpx) - self.push(bcp1y+rpy) - self.push(bcp2x) - self.push(bcp2y) - self.push(p2x) - self.push(p2y) - self.op_rrcurveto(None) - - # call rrcurveto - self.push(bcp3x) - self.push(bcp3y) - self.push(bcp4x) - self.push(bcp4y) - self.push(p3x) - self.push(p3y) - self.op_rrcurveto(None) - - # Push back final coords so subr 0 can find them - self.push(finalx) - self.push(finaly) - - def op_dotsection(self, index): - self.popall() # XXX - def op_hstem3(self, index): - self.popall() # XXX - def op_seac(self, index): - "asb adx ady bchar achar seac" - from fontTools.encodings.StandardEncoding import StandardEncoding - asb, adx, ady, bchar, achar = self.popall() - baseGlyph = StandardEncoding[bchar] - self.pen.addComponent(baseGlyph, (1, 0, 0, 1, 0, 0)) - accentGlyph = StandardEncoding[achar] - adx = adx + self.sbx - asb # seac weirdness - self.pen.addComponent(accentGlyph, (1, 0, 0, 1, adx, ady)) - def op_vstem3(self, index): - self.popall() # XXX class T2CharString(object): + operandEncoding = t2OperandEncoding + operators, opcodes = buildOperatorDict(t2Operators) + decompilerClass = SimpleT2Decompiler + outlineExtractor = T2OutlineExtractor + + def __init__(self, bytecode=None, program=None, private=None, globalSubrs=None): + if program is None: + program = [] + self.bytecode = bytecode + self.program = program + self.private = private + self.globalSubrs = globalSubrs if globalSubrs is not None else [] + self._cur_vsindex = None + + def getNumRegions(self, vsindex=None): + pd = self.private + assert pd is not None + if vsindex is not None: + self._cur_vsindex = vsindex + elif self._cur_vsindex is None: + self._cur_vsindex = pd.vsindex if hasattr(pd, "vsindex") else 0 + return pd.getNumRegions(self._cur_vsindex) + + def __repr__(self): + if self.bytecode is None: + return "<%s (source) at %x>" % (self.__class__.__name__, id(self)) + else: + return "<%s (bytecode) at %x>" % (self.__class__.__name__, id(self)) + + def getIntEncoder(self): + return encodeIntT2 + + def getFixedEncoder(self): + return encodeFixed + + def decompile(self): + if not self.needsDecompilation(): + return + subrs = getattr(self.private, "Subrs", []) + decompiler = self.decompilerClass(subrs, self.globalSubrs, self.private) + decompiler.execute(self) + + def draw(self, pen, blender=None): + subrs = getattr(self.private, "Subrs", []) + extractor = self.outlineExtractor( + pen, + subrs, + self.globalSubrs, + self.private.nominalWidthX, + self.private.defaultWidthX, + self.private, + blender, + ) + extractor.execute(self) + self.width = extractor.width + + def calcBounds(self, glyphSet): + boundsPen = BoundsPen(glyphSet) + self.draw(boundsPen) + return boundsPen.bounds + + def compile(self, isCFF2=False): + if self.bytecode is not None: + return + opcodes = self.opcodes + program = self.program + + if isCFF2: + # If present, remove return and endchar operators. + if program and program[-1] in ("return", "endchar"): + program = program[:-1] + elif program and not isinstance(program[-1], str): + raise CharStringCompileError( + "T2CharString or Subr has items on the stack after last operator." + ) + + bytecode = [] + encodeInt = self.getIntEncoder() + encodeFixed = self.getFixedEncoder() + i = 0 + end = len(program) + while i < end: + token = program[i] + i = i + 1 + if isinstance(token, str): + try: + bytecode.extend(bytechr(b) for b in opcodes[token]) + except KeyError: + raise CharStringCompileError("illegal operator: %s" % token) + if token in ("hintmask", "cntrmask"): + bytecode.append(program[i]) # hint mask + i = i + 1 + elif isinstance(token, int): + bytecode.append(encodeInt(token)) + elif isinstance(token, float): + bytecode.append(encodeFixed(token)) + else: + assert 0, "unsupported type: %s" % type(token) + try: + bytecode = bytesjoin(bytecode) + except TypeError: + log.error(bytecode) + raise + self.setBytecode(bytecode) + + def needsDecompilation(self): + return self.bytecode is not None + + def setProgram(self, program): + self.program = program + self.bytecode = None + + def setBytecode(self, bytecode): + self.bytecode = bytecode + self.program = None + + def getToken(self, index, len=len, byteord=byteord, isinstance=isinstance): + if self.bytecode is not None: + if index >= len(self.bytecode): + return None, 0, 0 + b0 = byteord(self.bytecode[index]) + index = index + 1 + handler = self.operandEncoding[b0] + token, index = handler(self, b0, self.bytecode, index) + else: + if index >= len(self.program): + return None, 0, 0 + token = self.program[index] + index = index + 1 + isOperator = isinstance(token, str) + return token, isOperator, index + + def getBytes(self, index, nBytes): + if self.bytecode is not None: + newIndex = index + nBytes + bytes = self.bytecode[index:newIndex] + index = newIndex + else: + bytes = self.program[index] + index = index + 1 + assert len(bytes) == nBytes + return bytes, index + + def handle_operator(self, operator): + return operator + + def toXML(self, xmlWriter, ttFont=None): + from fontTools.misc.textTools import num2binary + + if self.bytecode is not None: + xmlWriter.dumphex(self.bytecode) + else: + index = 0 + args = [] + while True: + token, isOperator, index = self.getToken(index) + if token is None: + break + if isOperator: + if token in ("hintmask", "cntrmask"): + hintMask, isOperator, index = self.getToken(index) + bits = [] + for byte in hintMask: + bits.append(num2binary(byteord(byte), 8)) + hintMask = strjoin(bits) + line = " ".join(args + [token, hintMask]) + else: + line = " ".join(args + [token]) + xmlWriter.write(line) + xmlWriter.newline() + args = [] + else: + if isinstance(token, float): + token = floatToFixedToStr(token, precisionBits=16) + else: + token = str(token) + args.append(token) + if args: + # NOTE: only CFF2 charstrings/subrs can have numeric arguments on + # the stack after the last operator. Compiling this would fail if + # this is part of CFF 1.0 table. + line = " ".join(args) + xmlWriter.write(line) + + def fromXML(self, name, attrs, content): + from fontTools.misc.textTools import binary2num, readHex + + if attrs.get("raw"): + self.setBytecode(readHex(content)) + return + content = strjoin(content) + content = content.split() + program = [] + end = len(content) + i = 0 + while i < end: + token = content[i] + i = i + 1 + try: + token = int(token) + except ValueError: + try: + token = strToFixedToFloat(token, precisionBits=16) + except ValueError: + program.append(token) + if token in ("hintmask", "cntrmask"): + mask = content[i] + maskBytes = b"" + for j in range(0, len(mask), 8): + maskBytes = maskBytes + bytechr(binary2num(mask[j : j + 8])) + program.append(maskBytes) + i = i + 1 + else: + program.append(token) + else: + program.append(token) + self.setProgram(program) - operandEncoding = t2OperandEncoding - operators, opcodes = buildOperatorDict(t2Operators) - decompilerClass = SimpleT2Decompiler - outlineExtractor = T2OutlineExtractor - - def __init__(self, bytecode=None, program=None, private=None, globalSubrs=None): - if program is None: - program = [] - self.bytecode = bytecode - self.program = program - self.private = private - self.globalSubrs = globalSubrs if globalSubrs is not None else [] - self._cur_vsindex = None - - def getNumRegions(self, vsindex=None): - pd = self.private - assert(pd is not None) - if vsindex is not None: - self._cur_vsindex = vsindex - elif self._cur_vsindex is None: - self._cur_vsindex = pd.vsindex if hasattr(pd, 'vsindex') else 0 - return pd.getNumRegions(self._cur_vsindex) - - def __repr__(self): - if self.bytecode is None: - return "<%s (source) at %x>" % (self.__class__.__name__, id(self)) - else: - return "<%s (bytecode) at %x>" % (self.__class__.__name__, id(self)) - - def getIntEncoder(self): - return encodeIntT2 - - def getFixedEncoder(self): - return encodeFixed - - def decompile(self): - if not self.needsDecompilation(): - return - subrs = getattr(self.private, "Subrs", []) - decompiler = self.decompilerClass(subrs, self.globalSubrs, self.private) - decompiler.execute(self) - - def draw(self, pen): - subrs = getattr(self.private, "Subrs", []) - extractor = self.outlineExtractor(pen, subrs, self.globalSubrs, - self.private.nominalWidthX, self.private.defaultWidthX, - self.private) - extractor.execute(self) - self.width = extractor.width - - def calcBounds(self, glyphSet): - boundsPen = BoundsPen(glyphSet) - self.draw(boundsPen) - return boundsPen.bounds - - def compile(self, isCFF2=False): - if self.bytecode is not None: - return - opcodes = self.opcodes - program = self.program - - if isCFF2: - # If present, remove return and endchar operators. - if program and program[-1] in ("return", "endchar"): - program = program[:-1] - elif program and not isinstance(program[-1], str): - raise CharStringCompileError( - "T2CharString or Subr has items on the stack after last operator." - ) - - bytecode = [] - encodeInt = self.getIntEncoder() - encodeFixed = self.getFixedEncoder() - i = 0 - end = len(program) - while i < end: - token = program[i] - i = i + 1 - if isinstance(token, str): - try: - bytecode.extend(bytechr(b) for b in opcodes[token]) - except KeyError: - raise CharStringCompileError("illegal operator: %s" % token) - if token in ('hintmask', 'cntrmask'): - bytecode.append(program[i]) # hint mask - i = i + 1 - elif isinstance(token, int): - bytecode.append(encodeInt(token)) - elif isinstance(token, float): - bytecode.append(encodeFixed(token)) - else: - assert 0, "unsupported type: %s" % type(token) - try: - bytecode = bytesjoin(bytecode) - except TypeError: - log.error(bytecode) - raise - self.setBytecode(bytecode) - - def needsDecompilation(self): - return self.bytecode is not None - - def setProgram(self, program): - self.program = program - self.bytecode = None - - def setBytecode(self, bytecode): - self.bytecode = bytecode - self.program = None - - def getToken(self, index, - len=len, byteord=byteord, isinstance=isinstance): - if self.bytecode is not None: - if index >= len(self.bytecode): - return None, 0, 0 - b0 = byteord(self.bytecode[index]) - index = index + 1 - handler = self.operandEncoding[b0] - token, index = handler(self, b0, self.bytecode, index) - else: - if index >= len(self.program): - return None, 0, 0 - token = self.program[index] - index = index + 1 - isOperator = isinstance(token, str) - return token, isOperator, index - - def getBytes(self, index, nBytes): - if self.bytecode is not None: - newIndex = index + nBytes - bytes = self.bytecode[index:newIndex] - index = newIndex - else: - bytes = self.program[index] - index = index + 1 - assert len(bytes) == nBytes - return bytes, index - - def handle_operator(self, operator): - return operator - - def toXML(self, xmlWriter, ttFont=None): - from fontTools.misc.textTools import num2binary - if self.bytecode is not None: - xmlWriter.dumphex(self.bytecode) - else: - index = 0 - args = [] - while True: - token, isOperator, index = self.getToken(index) - if token is None: - break - if isOperator: - if token in ('hintmask', 'cntrmask'): - hintMask, isOperator, index = self.getToken(index) - bits = [] - for byte in hintMask: - bits.append(num2binary(byteord(byte), 8)) - hintMask = strjoin(bits) - line = ' '.join(args + [token, hintMask]) - else: - line = ' '.join(args + [token]) - xmlWriter.write(line) - xmlWriter.newline() - args = [] - else: - if isinstance(token, float): - token = floatToFixedToStr(token, precisionBits=16) - else: - token = str(token) - args.append(token) - if args: - # NOTE: only CFF2 charstrings/subrs can have numeric arguments on - # the stack after the last operator. Compiling this would fail if - # this is part of CFF 1.0 table. - line = ' '.join(args) - xmlWriter.write(line) - - def fromXML(self, name, attrs, content): - from fontTools.misc.textTools import binary2num, readHex - if attrs.get("raw"): - self.setBytecode(readHex(content)) - return - content = strjoin(content) - content = content.split() - program = [] - end = len(content) - i = 0 - while i < end: - token = content[i] - i = i + 1 - try: - token = int(token) - except ValueError: - try: - token = strToFixedToFloat(token, precisionBits=16) - except ValueError: - program.append(token) - if token in ('hintmask', 'cntrmask'): - mask = content[i] - maskBytes = b"" - for j in range(0, len(mask), 8): - maskBytes = maskBytes + bytechr(binary2num(mask[j:j+8])) - program.append(maskBytes) - i = i + 1 - else: - program.append(token) - else: - program.append(token) - self.setProgram(program) class T1CharString(T2CharString): + operandEncoding = t1OperandEncoding + operators, opcodes = buildOperatorDict(t1Operators) + + def __init__(self, bytecode=None, program=None, subrs=None): + super().__init__(bytecode, program) + self.subrs = subrs + + def getIntEncoder(self): + return encodeIntT1 + + def getFixedEncoder(self): + def encodeFixed(value): + raise TypeError("Type 1 charstrings don't support floating point operands") + + def decompile(self): + if self.bytecode is None: + return + program = [] + index = 0 + while True: + token, isOperator, index = self.getToken(index) + if token is None: + break + program.append(token) + self.setProgram(program) + + def draw(self, pen): + extractor = T1OutlineExtractor(pen, self.subrs) + extractor.execute(self) + self.width = extractor.width - operandEncoding = t1OperandEncoding - operators, opcodes = buildOperatorDict(t1Operators) - - def __init__(self, bytecode=None, program=None, subrs=None): - super().__init__(bytecode, program) - self.subrs = subrs - - def getIntEncoder(self): - return encodeIntT1 - - def getFixedEncoder(self): - def encodeFixed(value): - raise TypeError("Type 1 charstrings don't support floating point operands") - - def decompile(self): - if self.bytecode is None: - return - program = [] - index = 0 - while True: - token, isOperator, index = self.getToken(index) - if token is None: - break - program.append(token) - self.setProgram(program) - - def draw(self, pen): - extractor = T1OutlineExtractor(pen, self.subrs) - extractor.execute(self) - self.width = extractor.width class DictDecompiler(object): - - operandEncoding = cffDictOperandEncoding - - def __init__(self, strings, parent=None): - self.stack = [] - self.strings = strings - self.dict = {} - self.parent = parent - - def getDict(self): - assert len(self.stack) == 0, "non-empty stack" - return self.dict - - def decompile(self, data): - index = 0 - lenData = len(data) - push = self.stack.append - while index < lenData: - b0 = byteord(data[index]) - index = index + 1 - handler = self.operandEncoding[b0] - value, index = handler(self, b0, data, index) - if value is not None: - push(value) - def pop(self): - value = self.stack[-1] - del self.stack[-1] - return value - - def popall(self): - args = self.stack[:] - del self.stack[:] - return args - - def handle_operator(self, operator): - operator, argType = operator - if isinstance(argType, tuple): - value = () - for i in range(len(argType)-1, -1, -1): - arg = argType[i] - arghandler = getattr(self, "arg_" + arg) - value = (arghandler(operator),) + value - else: - arghandler = getattr(self, "arg_" + argType) - value = arghandler(operator) - if operator == "blend": - self.stack.extend(value) - else: - self.dict[operator] = value - - def arg_number(self, name): - if isinstance(self.stack[0], list): - out = self.arg_blend_number(self.stack) - else: - out = self.pop() - return out - - def arg_blend_number(self, name): - out = [] - blendArgs = self.pop() - numMasters = len(blendArgs) - out.append(blendArgs) - out.append("blend") - dummy = self.popall() - return blendArgs - - def arg_SID(self, name): - return self.strings[self.pop()] - def arg_array(self, name): - return self.popall() - def arg_blendList(self, name): - """ - There may be non-blend args at the top of the stack. We first calculate - where the blend args start in the stack. These are the last - numMasters*numBlends) +1 args. - The blend args starts with numMasters relative coordinate values, the BlueValues in the list from the default master font. This is followed by - numBlends list of values. Each of value in one of these lists is the - Variable Font delta for the matching region. - - We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by - the delta values. We then convert the default values, the first item in each entry, to an absolute value. - """ - vsindex = self.dict.get('vsindex', 0) - numMasters = self.parent.getNumRegions(vsindex) + 1 # only a PrivateDict has blended ops. - numBlends = self.pop() - args = self.popall() - numArgs = len(args) - # The spec says that there should be no non-blended Blue Values,. - assert(numArgs == numMasters * numBlends) - value = [None]*numBlends - numDeltas = numMasters-1 - i = 0 - prevVal = 0 - while i < numBlends: - newVal = args[i] + prevVal - prevVal = newVal - masterOffset = numBlends + (i* numDeltas) - blendList = [newVal] + args[masterOffset:masterOffset+numDeltas] - value[i] = blendList - i += 1 - return value - - def arg_delta(self, name): - valueList = self.popall() - out = [] - if valueList and isinstance(valueList[0], list): - # arg_blendList() has already converted these to absolute values. - out = valueList - else: - current = 0 - for v in valueList: - current = current + v - out.append(current) - return out + operandEncoding = cffDictOperandEncoding + + def __init__(self, strings, parent=None): + self.stack = [] + self.strings = strings + self.dict = {} + self.parent = parent + + def getDict(self): + assert len(self.stack) == 0, "non-empty stack" + return self.dict + + def decompile(self, data): + index = 0 + lenData = len(data) + push = self.stack.append + while index < lenData: + b0 = byteord(data[index]) + index = index + 1 + handler = self.operandEncoding[b0] + value, index = handler(self, b0, data, index) + if value is not None: + push(value) + + def pop(self): + value = self.stack[-1] + del self.stack[-1] + return value + + def popall(self): + args = self.stack[:] + del self.stack[:] + return args + + def handle_operator(self, operator): + operator, argType = operator + if isinstance(argType, tuple): + value = () + for i in range(len(argType) - 1, -1, -1): + arg = argType[i] + arghandler = getattr(self, "arg_" + arg) + value = (arghandler(operator),) + value + else: + arghandler = getattr(self, "arg_" + argType) + value = arghandler(operator) + if operator == "blend": + self.stack.extend(value) + else: + self.dict[operator] = value + + def arg_number(self, name): + if isinstance(self.stack[0], list): + out = self.arg_blend_number(self.stack) + else: + out = self.pop() + return out + + def arg_blend_number(self, name): + out = [] + blendArgs = self.pop() + numMasters = len(blendArgs) + out.append(blendArgs) + out.append("blend") + dummy = self.popall() + return blendArgs + + def arg_SID(self, name): + return self.strings[self.pop()] + + def arg_array(self, name): + return self.popall() + + def arg_blendList(self, name): + """ + There may be non-blend args at the top of the stack. We first calculate + where the blend args start in the stack. These are the last + numMasters*numBlends) +1 args. + The blend args starts with numMasters relative coordinate values, the BlueValues in the list from the default master font. This is followed by + numBlends list of values. Each of value in one of these lists is the + Variable Font delta for the matching region. + + We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by + the delta values. We then convert the default values, the first item in each entry, to an absolute value. + """ + vsindex = self.dict.get("vsindex", 0) + numMasters = ( + self.parent.getNumRegions(vsindex) + 1 + ) # only a PrivateDict has blended ops. + numBlends = self.pop() + args = self.popall() + numArgs = len(args) + # The spec says that there should be no non-blended Blue Values,. + assert numArgs == numMasters * numBlends + value = [None] * numBlends + numDeltas = numMasters - 1 + i = 0 + prevVal = 0 + while i < numBlends: + newVal = args[i] + prevVal + prevVal = newVal + masterOffset = numBlends + (i * numDeltas) + blendList = [newVal] + args[masterOffset : masterOffset + numDeltas] + value[i] = blendList + i += 1 + return value + + def arg_delta(self, name): + valueList = self.popall() + out = [] + if valueList and isinstance(valueList[0], list): + # arg_blendList() has already converted these to absolute values. + out = valueList + else: + current = 0 + for v in valueList: + current = current + v + out.append(current) + return out def calcSubrBias(subrs): - nSubrs = len(subrs) - if nSubrs < 1240: - bias = 107 - elif nSubrs < 33900: - bias = 1131 - else: - bias = 32768 - return bias + nSubrs = len(subrs) + if nSubrs < 1240: + bias = 107 + elif nSubrs < 33900: + bias = 1131 + else: + bias = 32768 + return bias diff --git a/Lib/fontTools/misc/psLib.py b/Lib/fontTools/misc/psLib.py index a6c8b8b5..3bfdb4ae 100644 --- a/Lib/fontTools/misc/psLib.py +++ b/Lib/fontTools/misc/psLib.py @@ -1,20 +1,20 @@ from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes, tostr from fontTools.misc import eexec from .psOperators import ( - PSOperators, - ps_StandardEncoding, - ps_array, - ps_boolean, - ps_dict, - ps_integer, - ps_literal, - ps_mark, - ps_name, - ps_operator, - ps_procedure, - ps_procmark, - ps_real, - ps_string, + PSOperators, + ps_StandardEncoding, + ps_array, + ps_boolean, + ps_dict, + ps_integer, + ps_literal, + ps_mark, + ps_name, + ps_operator, + ps_procedure, + ps_procmark, + ps_real, + ps_string, ) import re from collections.abc import Callable @@ -24,7 +24,7 @@ import logging log = logging.getLogger(__name__) -ps_special = b'()<>[]{}%' # / is one too, but we take care of that one differently +ps_special = b"()<>[]{}%" # / is one too, but we take care of that one differently skipwhiteRE = re.compile(bytesjoin([b"[", whitespace, b"]*"])) endofthingPat = bytesjoin([b"[^][(){}<>/%", whitespace, b"]*"]) @@ -32,7 +32,7 @@ endofthingRE = re.compile(endofthingPat) commentRE = re.compile(b"%[^\n\r]*") # XXX This not entirely correct as it doesn't allow *nested* embedded parens: -stringPat = br""" +stringPat = rb""" \( ( ( @@ -51,335 +51,348 @@ stringRE = re.compile(stringPat) hexstringRE = re.compile(bytesjoin([b"<[", whitespace, b"0-9A-Fa-f]*>"])) -class PSTokenError(Exception): pass -class PSError(Exception): pass +class PSTokenError(Exception): + pass -class PSTokenizer(object): - def __init__(self, buf=b'', encoding="ascii"): - # Force self.buf to be a byte string - buf = tobytes(buf) - self.buf = buf - self.len = len(buf) - self.pos = 0 - self.closed = False - self.encoding = encoding - - def read(self, n=-1): - """Read at most 'n' bytes from the buffer, or less if the read - hits EOF before obtaining 'n' bytes. - If 'n' is negative or omitted, read all data until EOF is reached. - """ - if self.closed: - raise ValueError("I/O operation on closed file") - if n is None or n < 0: - newpos = self.len - else: - newpos = min(self.pos+n, self.len) - r = self.buf[self.pos:newpos] - self.pos = newpos - return r - - def close(self): - if not self.closed: - self.closed = True - del self.buf, self.pos - - def getnexttoken(self, - # localize some stuff, for performance - len=len, - ps_special=ps_special, - stringmatch=stringRE.match, - hexstringmatch=hexstringRE.match, - commentmatch=commentRE.match, - endmatch=endofthingRE.match): - - self.skipwhite() - if self.pos >= self.len: - return None, None - pos = self.pos - buf = self.buf - char = bytechr(byteord(buf[pos])) - if char in ps_special: - if char in b'{}[]': - tokentype = 'do_special' - token = char - elif char == b'%': - tokentype = 'do_comment' - _, nextpos = commentmatch(buf, pos).span() - token = buf[pos:nextpos] - elif char == b'(': - tokentype = 'do_string' - m = stringmatch(buf, pos) - if m is None: - raise PSTokenError('bad string at character %d' % pos) - _, nextpos = m.span() - token = buf[pos:nextpos] - elif char == b'<': - tokentype = 'do_hexstring' - m = hexstringmatch(buf, pos) - if m is None: - raise PSTokenError('bad hexstring at character %d' % pos) - _, nextpos = m.span() - token = buf[pos:nextpos] - else: - raise PSTokenError('bad token at character %d' % pos) - else: - if char == b'/': - tokentype = 'do_literal' - m = endmatch(buf, pos+1) - else: - tokentype = '' - m = endmatch(buf, pos) - if m is None: - raise PSTokenError('bad token at character %d' % pos) - _, nextpos = m.span() - token = buf[pos:nextpos] - self.pos = pos + len(token) - token = tostr(token, encoding=self.encoding) - return tokentype, token - - def skipwhite(self, whitematch=skipwhiteRE.match): - _, nextpos = whitematch(self.buf, self.pos).span() - self.pos = nextpos - - def starteexec(self): - self.pos = self.pos + 1 - self.dirtybuf = self.buf[self.pos:] - self.buf, R = eexec.decrypt(self.dirtybuf, 55665) - self.len = len(self.buf) - self.pos = 4 - - def stopeexec(self): - if not hasattr(self, 'dirtybuf'): - return - self.buf = self.dirtybuf - del self.dirtybuf +class PSError(Exception): + pass -class PSInterpreter(PSOperators): +class PSTokenizer(object): + def __init__(self, buf=b"", encoding="ascii"): + # Force self.buf to be a byte string + buf = tobytes(buf) + self.buf = buf + self.len = len(buf) + self.pos = 0 + self.closed = False + self.encoding = encoding + + def read(self, n=-1): + """Read at most 'n' bytes from the buffer, or less if the read + hits EOF before obtaining 'n' bytes. + If 'n' is negative or omitted, read all data until EOF is reached. + """ + if self.closed: + raise ValueError("I/O operation on closed file") + if n is None or n < 0: + newpos = self.len + else: + newpos = min(self.pos + n, self.len) + r = self.buf[self.pos : newpos] + self.pos = newpos + return r + + def close(self): + if not self.closed: + self.closed = True + del self.buf, self.pos + + def getnexttoken( + self, + # localize some stuff, for performance + len=len, + ps_special=ps_special, + stringmatch=stringRE.match, + hexstringmatch=hexstringRE.match, + commentmatch=commentRE.match, + endmatch=endofthingRE.match, + ): + self.skipwhite() + if self.pos >= self.len: + return None, None + pos = self.pos + buf = self.buf + char = bytechr(byteord(buf[pos])) + if char in ps_special: + if char in b"{}[]": + tokentype = "do_special" + token = char + elif char == b"%": + tokentype = "do_comment" + _, nextpos = commentmatch(buf, pos).span() + token = buf[pos:nextpos] + elif char == b"(": + tokentype = "do_string" + m = stringmatch(buf, pos) + if m is None: + raise PSTokenError("bad string at character %d" % pos) + _, nextpos = m.span() + token = buf[pos:nextpos] + elif char == b"<": + tokentype = "do_hexstring" + m = hexstringmatch(buf, pos) + if m is None: + raise PSTokenError("bad hexstring at character %d" % pos) + _, nextpos = m.span() + token = buf[pos:nextpos] + else: + raise PSTokenError("bad token at character %d" % pos) + else: + if char == b"/": + tokentype = "do_literal" + m = endmatch(buf, pos + 1) + else: + tokentype = "" + m = endmatch(buf, pos) + if m is None: + raise PSTokenError("bad token at character %d" % pos) + _, nextpos = m.span() + token = buf[pos:nextpos] + self.pos = pos + len(token) + token = tostr(token, encoding=self.encoding) + return tokentype, token + + def skipwhite(self, whitematch=skipwhiteRE.match): + _, nextpos = whitematch(self.buf, self.pos).span() + self.pos = nextpos + + def starteexec(self): + self.pos = self.pos + 1 + self.dirtybuf = self.buf[self.pos :] + self.buf, R = eexec.decrypt(self.dirtybuf, 55665) + self.len = len(self.buf) + self.pos = 4 + + def stopeexec(self): + if not hasattr(self, "dirtybuf"): + return + self.buf = self.dirtybuf + del self.dirtybuf - def __init__(self, encoding="ascii"): - systemdict = {} - userdict = {} - self.encoding = encoding - self.dictstack = [systemdict, userdict] - self.stack = [] - self.proclevel = 0 - self.procmark = ps_procmark() - self.fillsystemdict() - - def fillsystemdict(self): - systemdict = self.dictstack[0] - systemdict['['] = systemdict['mark'] = self.mark = ps_mark() - systemdict[']'] = ps_operator(']', self.do_makearray) - systemdict['true'] = ps_boolean(1) - systemdict['false'] = ps_boolean(0) - systemdict['StandardEncoding'] = ps_array(ps_StandardEncoding) - systemdict['FontDirectory'] = ps_dict({}) - self.suckoperators(systemdict, self.__class__) - - def suckoperators(self, systemdict, klass): - for name in dir(klass): - attr = getattr(self, name) - if isinstance(attr, Callable) and name[:3] == 'ps_': - name = name[3:] - systemdict[name] = ps_operator(name, attr) - for baseclass in klass.__bases__: - self.suckoperators(systemdict, baseclass) - - def interpret(self, data, getattr=getattr): - tokenizer = self.tokenizer = PSTokenizer(data, self.encoding) - getnexttoken = tokenizer.getnexttoken - do_token = self.do_token - handle_object = self.handle_object - try: - while 1: - tokentype, token = getnexttoken() - if not token: - break - if tokentype: - handler = getattr(self, tokentype) - object = handler(token) - else: - object = do_token(token) - if object is not None: - handle_object(object) - tokenizer.close() - self.tokenizer = None - except: - if self.tokenizer is not None: - log.debug( - 'ps error:\n' - '- - - - - - -\n' - '%s\n' - '>>>\n' - '%s\n' - '- - - - - - -', - self.tokenizer.buf[self.tokenizer.pos-50:self.tokenizer.pos], - self.tokenizer.buf[self.tokenizer.pos:self.tokenizer.pos+50]) - raise - - def handle_object(self, object): - if not (self.proclevel or object.literal or object.type == 'proceduretype'): - if object.type != 'operatortype': - object = self.resolve_name(object.value) - if object.literal: - self.push(object) - else: - if object.type == 'proceduretype': - self.call_procedure(object) - else: - object.function() - else: - self.push(object) - - def call_procedure(self, proc): - handle_object = self.handle_object - for item in proc.value: - handle_object(item) - - def resolve_name(self, name): - dictstack = self.dictstack - for i in range(len(dictstack)-1, -1, -1): - if name in dictstack[i]: - return dictstack[i][name] - raise PSError('name error: ' + str(name)) - - def do_token(self, token, - int=int, - float=float, - ps_name=ps_name, - ps_integer=ps_integer, - ps_real=ps_real): - try: - num = int(token) - except (ValueError, OverflowError): - try: - num = float(token) - except (ValueError, OverflowError): - if '#' in token: - hashpos = token.find('#') - try: - base = int(token[:hashpos]) - num = int(token[hashpos+1:], base) - except (ValueError, OverflowError): - return ps_name(token) - else: - return ps_integer(num) - else: - return ps_name(token) - else: - return ps_real(num) - else: - return ps_integer(num) - - def do_comment(self, token): - pass - - def do_literal(self, token): - return ps_literal(token[1:]) - - def do_string(self, token): - return ps_string(token[1:-1]) - - def do_hexstring(self, token): - hexStr = "".join(token[1:-1].split()) - if len(hexStr) % 2: - hexStr = hexStr + '0' - cleanstr = [] - for i in range(0, len(hexStr), 2): - cleanstr.append(chr(int(hexStr[i:i+2], 16))) - cleanstr = "".join(cleanstr) - return ps_string(cleanstr) - - def do_special(self, token): - if token == '{': - self.proclevel = self.proclevel + 1 - return self.procmark - elif token == '}': - proc = [] - while 1: - topobject = self.pop() - if topobject == self.procmark: - break - proc.append(topobject) - self.proclevel = self.proclevel - 1 - proc.reverse() - return ps_procedure(proc) - elif token == '[': - return self.mark - elif token == ']': - return ps_name(']') - else: - raise PSTokenError('huh?') - - def push(self, object): - self.stack.append(object) - - def pop(self, *types): - stack = self.stack - if not stack: - raise PSError('stack underflow') - object = stack[-1] - if types: - if object.type not in types: - raise PSError('typecheck, expected %s, found %s' % (repr(types), object.type)) - del stack[-1] - return object - - def do_makearray(self): - array = [] - while 1: - topobject = self.pop() - if topobject == self.mark: - break - array.append(topobject) - array.reverse() - self.push(ps_array(array)) - - def close(self): - """Remove circular references.""" - del self.stack - del self.dictstack + +class PSInterpreter(PSOperators): + def __init__(self, encoding="ascii"): + systemdict = {} + userdict = {} + self.encoding = encoding + self.dictstack = [systemdict, userdict] + self.stack = [] + self.proclevel = 0 + self.procmark = ps_procmark() + self.fillsystemdict() + + def fillsystemdict(self): + systemdict = self.dictstack[0] + systemdict["["] = systemdict["mark"] = self.mark = ps_mark() + systemdict["]"] = ps_operator("]", self.do_makearray) + systemdict["true"] = ps_boolean(1) + systemdict["false"] = ps_boolean(0) + systemdict["StandardEncoding"] = ps_array(ps_StandardEncoding) + systemdict["FontDirectory"] = ps_dict({}) + self.suckoperators(systemdict, self.__class__) + + def suckoperators(self, systemdict, klass): + for name in dir(klass): + attr = getattr(self, name) + if isinstance(attr, Callable) and name[:3] == "ps_": + name = name[3:] + systemdict[name] = ps_operator(name, attr) + for baseclass in klass.__bases__: + self.suckoperators(systemdict, baseclass) + + def interpret(self, data, getattr=getattr): + tokenizer = self.tokenizer = PSTokenizer(data, self.encoding) + getnexttoken = tokenizer.getnexttoken + do_token = self.do_token + handle_object = self.handle_object + try: + while 1: + tokentype, token = getnexttoken() + if not token: + break + if tokentype: + handler = getattr(self, tokentype) + object = handler(token) + else: + object = do_token(token) + if object is not None: + handle_object(object) + tokenizer.close() + self.tokenizer = None + except: + if self.tokenizer is not None: + log.debug( + "ps error:\n" + "- - - - - - -\n" + "%s\n" + ">>>\n" + "%s\n" + "- - - - - - -", + self.tokenizer.buf[self.tokenizer.pos - 50 : self.tokenizer.pos], + self.tokenizer.buf[self.tokenizer.pos : self.tokenizer.pos + 50], + ) + raise + + def handle_object(self, object): + if not (self.proclevel or object.literal or object.type == "proceduretype"): + if object.type != "operatortype": + object = self.resolve_name(object.value) + if object.literal: + self.push(object) + else: + if object.type == "proceduretype": + self.call_procedure(object) + else: + object.function() + else: + self.push(object) + + def call_procedure(self, proc): + handle_object = self.handle_object + for item in proc.value: + handle_object(item) + + def resolve_name(self, name): + dictstack = self.dictstack + for i in range(len(dictstack) - 1, -1, -1): + if name in dictstack[i]: + return dictstack[i][name] + raise PSError("name error: " + str(name)) + + def do_token( + self, + token, + int=int, + float=float, + ps_name=ps_name, + ps_integer=ps_integer, + ps_real=ps_real, + ): + try: + num = int(token) + except (ValueError, OverflowError): + try: + num = float(token) + except (ValueError, OverflowError): + if "#" in token: + hashpos = token.find("#") + try: + base = int(token[:hashpos]) + num = int(token[hashpos + 1 :], base) + except (ValueError, OverflowError): + return ps_name(token) + else: + return ps_integer(num) + else: + return ps_name(token) + else: + return ps_real(num) + else: + return ps_integer(num) + + def do_comment(self, token): + pass + + def do_literal(self, token): + return ps_literal(token[1:]) + + def do_string(self, token): + return ps_string(token[1:-1]) + + def do_hexstring(self, token): + hexStr = "".join(token[1:-1].split()) + if len(hexStr) % 2: + hexStr = hexStr + "0" + cleanstr = [] + for i in range(0, len(hexStr), 2): + cleanstr.append(chr(int(hexStr[i : i + 2], 16))) + cleanstr = "".join(cleanstr) + return ps_string(cleanstr) + + def do_special(self, token): + if token == "{": + self.proclevel = self.proclevel + 1 + return self.procmark + elif token == "}": + proc = [] + while 1: + topobject = self.pop() + if topobject == self.procmark: + break + proc.append(topobject) + self.proclevel = self.proclevel - 1 + proc.reverse() + return ps_procedure(proc) + elif token == "[": + return self.mark + elif token == "]": + return ps_name("]") + else: + raise PSTokenError("huh?") + + def push(self, object): + self.stack.append(object) + + def pop(self, *types): + stack = self.stack + if not stack: + raise PSError("stack underflow") + object = stack[-1] + if types: + if object.type not in types: + raise PSError( + "typecheck, expected %s, found %s" % (repr(types), object.type) + ) + del stack[-1] + return object + + def do_makearray(self): + array = [] + while 1: + topobject = self.pop() + if topobject == self.mark: + break + array.append(topobject) + array.reverse() + self.push(ps_array(array)) + + def close(self): + """Remove circular references.""" + del self.stack + del self.dictstack def unpack_item(item): - tp = type(item.value) - if tp == dict: - newitem = {} - for key, value in item.value.items(): - newitem[key] = unpack_item(value) - elif tp == list: - newitem = [None] * len(item.value) - for i in range(len(item.value)): - newitem[i] = unpack_item(item.value[i]) - if item.type == 'proceduretype': - newitem = tuple(newitem) - else: - newitem = item.value - return newitem + tp = type(item.value) + if tp == dict: + newitem = {} + for key, value in item.value.items(): + newitem[key] = unpack_item(value) + elif tp == list: + newitem = [None] * len(item.value) + for i in range(len(item.value)): + newitem[i] = unpack_item(item.value[i]) + if item.type == "proceduretype": + newitem = tuple(newitem) + else: + newitem = item.value + return newitem + def suckfont(data, encoding="ascii"): - m = re.search(br"/FontName\s+/([^ \t\n\r]+)\s+def", data) - if m: - fontName = m.group(1) - fontName = fontName.decode() - else: - fontName = None - interpreter = PSInterpreter(encoding=encoding) - interpreter.interpret(b"/Helvetica 4 dict dup /Encoding StandardEncoding put definefont pop") - interpreter.interpret(data) - fontdir = interpreter.dictstack[0]['FontDirectory'].value - if fontName in fontdir: - rawfont = fontdir[fontName] - else: - # fall back, in case fontName wasn't found - fontNames = list(fontdir.keys()) - if len(fontNames) > 1: - fontNames.remove("Helvetica") - fontNames.sort() - rawfont = fontdir[fontNames[0]] - interpreter.close() - return unpack_item(rawfont) + m = re.search(rb"/FontName\s+/([^ \t\n\r]+)\s+def", data) + if m: + fontName = m.group(1) + fontName = fontName.decode() + else: + fontName = None + interpreter = PSInterpreter(encoding=encoding) + interpreter.interpret( + b"/Helvetica 4 dict dup /Encoding StandardEncoding put definefont pop" + ) + interpreter.interpret(data) + fontdir = interpreter.dictstack[0]["FontDirectory"].value + if fontName in fontdir: + rawfont = fontdir[fontName] + else: + # fall back, in case fontName wasn't found + fontNames = list(fontdir.keys()) + if len(fontNames) > 1: + fontNames.remove("Helvetica") + fontNames.sort() + rawfont = fontdir[fontNames[0]] + interpreter.close() + return unpack_item(rawfont) diff --git a/Lib/fontTools/misc/psOperators.py b/Lib/fontTools/misc/psOperators.py index 3b378f59..d0b975ea 100644 --- a/Lib/fontTools/misc/psOperators.py +++ b/Lib/fontTools/misc/psOperators.py @@ -2,536 +2,571 @@ _accessstrings = {0: "", 1: "readonly", 2: "executeonly", 3: "noaccess"} class ps_object(object): + literal = 1 + access = 0 + value = None - literal = 1 - access = 0 - value = None + def __init__(self, value): + self.value = value + self.type = self.__class__.__name__[3:] + "type" - def __init__(self, value): - self.value = value - self.type = self.__class__.__name__[3:] + "type" - - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__[3:], repr(self.value)) + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__[3:], repr(self.value)) class ps_operator(ps_object): + literal = 0 + + def __init__(self, name, function): + self.name = name + self.function = function + self.type = self.__class__.__name__[3:] + "type" - literal = 0 + def __repr__(self): + return "<operator %s>" % self.name - def __init__(self, name, function): - self.name = name - self.function = function - self.type = self.__class__.__name__[3:] + "type" - def __repr__(self): - return "<operator %s>" % self.name class ps_procedure(ps_object): - literal = 0 - def __repr__(self): - return "<procedure>" - def __str__(self): - psstring = '{' - for i in range(len(self.value)): - if i: - psstring = psstring + ' ' + str(self.value[i]) - else: - psstring = psstring + str(self.value[i]) - return psstring + '}' + literal = 0 + + def __repr__(self): + return "<procedure>" + + def __str__(self): + psstring = "{" + for i in range(len(self.value)): + if i: + psstring = psstring + " " + str(self.value[i]) + else: + psstring = psstring + str(self.value[i]) + return psstring + "}" + class ps_name(ps_object): - literal = 0 - def __str__(self): - if self.literal: - return '/' + self.value - else: - return self.value + literal = 0 + + def __str__(self): + if self.literal: + return "/" + self.value + else: + return self.value + class ps_literal(ps_object): - def __str__(self): - return '/' + self.value + def __str__(self): + return "/" + self.value + class ps_array(ps_object): - def __str__(self): - psstring = '[' - for i in range(len(self.value)): - item = self.value[i] - access = _accessstrings[item.access] - if access: - access = ' ' + access - if i: - psstring = psstring + ' ' + str(item) + access - else: - psstring = psstring + str(item) + access - return psstring + ']' - def __repr__(self): - return "<array>" + def __str__(self): + psstring = "[" + for i in range(len(self.value)): + item = self.value[i] + access = _accessstrings[item.access] + if access: + access = " " + access + if i: + psstring = psstring + " " + str(item) + access + else: + psstring = psstring + str(item) + access + return psstring + "]" + + def __repr__(self): + return "<array>" + _type1_pre_eexec_order = [ - "FontInfo", - "FontName", - "Encoding", - "PaintType", - "FontType", - "FontMatrix", - "FontBBox", - "UniqueID", - "Metrics", - "StrokeWidth" - ] + "FontInfo", + "FontName", + "Encoding", + "PaintType", + "FontType", + "FontMatrix", + "FontBBox", + "UniqueID", + "Metrics", + "StrokeWidth", +] _type1_fontinfo_order = [ - "version", - "Notice", - "FullName", - "FamilyName", - "Weight", - "ItalicAngle", - "isFixedPitch", - "UnderlinePosition", - "UnderlineThickness" - ] - -_type1_post_eexec_order = [ - "Private", - "CharStrings", - "FID" - ] + "version", + "Notice", + "FullName", + "FamilyName", + "Weight", + "ItalicAngle", + "isFixedPitch", + "UnderlinePosition", + "UnderlineThickness", +] + +_type1_post_eexec_order = ["Private", "CharStrings", "FID"] + def _type1_item_repr(key, value): - psstring = "" - access = _accessstrings[value.access] - if access: - access = access + ' ' - if key == 'CharStrings': - psstring = psstring + "/%s %s def\n" % (key, _type1_CharString_repr(value.value)) - elif key == 'Encoding': - psstring = psstring + _type1_Encoding_repr(value, access) - else: - psstring = psstring + "/%s %s %sdef\n" % (str(key), str(value), access) - return psstring + psstring = "" + access = _accessstrings[value.access] + if access: + access = access + " " + if key == "CharStrings": + psstring = psstring + "/%s %s def\n" % ( + key, + _type1_CharString_repr(value.value), + ) + elif key == "Encoding": + psstring = psstring + _type1_Encoding_repr(value, access) + else: + psstring = psstring + "/%s %s %sdef\n" % (str(key), str(value), access) + return psstring + def _type1_Encoding_repr(encoding, access): - encoding = encoding.value - psstring = "/Encoding 256 array\n0 1 255 {1 index exch /.notdef put} for\n" - for i in range(256): - name = encoding[i].value - if name != '.notdef': - psstring = psstring + "dup %d /%s put\n" % (i, name) - return psstring + access + "def\n" + encoding = encoding.value + psstring = "/Encoding 256 array\n0 1 255 {1 index exch /.notdef put} for\n" + for i in range(256): + name = encoding[i].value + if name != ".notdef": + psstring = psstring + "dup %d /%s put\n" % (i, name) + return psstring + access + "def\n" + def _type1_CharString_repr(charstrings): - items = sorted(charstrings.items()) - return 'xxx' + items = sorted(charstrings.items()) + return "xxx" + class ps_font(ps_object): - def __str__(self): - psstring = "%d dict dup begin\n" % len(self.value) - for key in _type1_pre_eexec_order: - try: - value = self.value[key] - except KeyError: - pass - else: - psstring = psstring + _type1_item_repr(key, value) - items = sorted(self.value.items()) - for key, value in items: - if key not in _type1_pre_eexec_order + _type1_post_eexec_order: - psstring = psstring + _type1_item_repr(key, value) - psstring = psstring + "currentdict end\ncurrentfile eexec\ndup " - for key in _type1_post_eexec_order: - try: - value = self.value[key] - except KeyError: - pass - else: - psstring = psstring + _type1_item_repr(key, value) - return psstring + 'dup/FontName get exch definefont pop\nmark currentfile closefile\n' + \ - 8 * (64 * '0' + '\n') + 'cleartomark' + '\n' - def __repr__(self): - return '<font>' + def __str__(self): + psstring = "%d dict dup begin\n" % len(self.value) + for key in _type1_pre_eexec_order: + try: + value = self.value[key] + except KeyError: + pass + else: + psstring = psstring + _type1_item_repr(key, value) + items = sorted(self.value.items()) + for key, value in items: + if key not in _type1_pre_eexec_order + _type1_post_eexec_order: + psstring = psstring + _type1_item_repr(key, value) + psstring = psstring + "currentdict end\ncurrentfile eexec\ndup " + for key in _type1_post_eexec_order: + try: + value = self.value[key] + except KeyError: + pass + else: + psstring = psstring + _type1_item_repr(key, value) + return ( + psstring + + "dup/FontName get exch definefont pop\nmark currentfile closefile\n" + + 8 * (64 * "0" + "\n") + + "cleartomark" + + "\n" + ) + + def __repr__(self): + return "<font>" + class ps_file(ps_object): - pass + pass + class ps_dict(ps_object): - def __str__(self): - psstring = "%d dict dup begin\n" % len(self.value) - items = sorted(self.value.items()) - for key, value in items: - access = _accessstrings[value.access] - if access: - access = access + ' ' - psstring = psstring + "/%s %s %sdef\n" % (str(key), str(value), access) - return psstring + 'end ' - def __repr__(self): - return "<dict>" + def __str__(self): + psstring = "%d dict dup begin\n" % len(self.value) + items = sorted(self.value.items()) + for key, value in items: + access = _accessstrings[value.access] + if access: + access = access + " " + psstring = psstring + "/%s %s %sdef\n" % (str(key), str(value), access) + return psstring + "end " + + def __repr__(self): + return "<dict>" + class ps_mark(ps_object): - def __init__(self): - self.value = 'mark' - self.type = self.__class__.__name__[3:] + "type" + def __init__(self): + self.value = "mark" + self.type = self.__class__.__name__[3:] + "type" + class ps_procmark(ps_object): - def __init__(self): - self.value = 'procmark' - self.type = self.__class__.__name__[3:] + "type" + def __init__(self): + self.value = "procmark" + self.type = self.__class__.__name__[3:] + "type" + class ps_null(ps_object): - def __init__(self): - self.type = self.__class__.__name__[3:] + "type" + def __init__(self): + self.type = self.__class__.__name__[3:] + "type" + class ps_boolean(ps_object): - def __str__(self): - if self.value: - return 'true' - else: - return 'false' + def __str__(self): + if self.value: + return "true" + else: + return "false" + class ps_string(ps_object): - def __str__(self): - return "(%s)" % repr(self.value)[1:-1] + def __str__(self): + return "(%s)" % repr(self.value)[1:-1] + class ps_integer(ps_object): - def __str__(self): - return repr(self.value) + def __str__(self): + return repr(self.value) + class ps_real(ps_object): - def __str__(self): - return repr(self.value) + def __str__(self): + return repr(self.value) class PSOperators(object): - - def ps_def(self): - obj = self.pop() - name = self.pop() - self.dictstack[-1][name.value] = obj - - def ps_bind(self): - proc = self.pop('proceduretype') - self.proc_bind(proc) - self.push(proc) - - def proc_bind(self, proc): - for i in range(len(proc.value)): - item = proc.value[i] - if item.type == 'proceduretype': - self.proc_bind(item) - else: - if not item.literal: - try: - obj = self.resolve_name(item.value) - except: - pass - else: - if obj.type == 'operatortype': - proc.value[i] = obj - - def ps_exch(self): - if len(self.stack) < 2: - raise RuntimeError('stack underflow') - obj1 = self.pop() - obj2 = self.pop() - self.push(obj1) - self.push(obj2) - - def ps_dup(self): - if not self.stack: - raise RuntimeError('stack underflow') - self.push(self.stack[-1]) - - def ps_exec(self): - obj = self.pop() - if obj.type == 'proceduretype': - self.call_procedure(obj) - else: - self.handle_object(obj) - - def ps_count(self): - self.push(ps_integer(len(self.stack))) - - def ps_eq(self): - any1 = self.pop() - any2 = self.pop() - self.push(ps_boolean(any1.value == any2.value)) - - def ps_ne(self): - any1 = self.pop() - any2 = self.pop() - self.push(ps_boolean(any1.value != any2.value)) - - def ps_cvx(self): - obj = self.pop() - obj.literal = 0 - self.push(obj) - - def ps_matrix(self): - matrix = [ps_real(1.0), ps_integer(0), ps_integer(0), ps_real(1.0), ps_integer(0), ps_integer(0)] - self.push(ps_array(matrix)) - - def ps_string(self): - num = self.pop('integertype').value - self.push(ps_string('\0' * num)) - - def ps_type(self): - obj = self.pop() - self.push(ps_string(obj.type)) - - def ps_store(self): - value = self.pop() - key = self.pop() - name = key.value - for i in range(len(self.dictstack)-1, -1, -1): - if name in self.dictstack[i]: - self.dictstack[i][name] = value - break - self.dictstack[-1][name] = value - - def ps_where(self): - name = self.pop() - # XXX - self.push(ps_boolean(0)) - - def ps_systemdict(self): - self.push(ps_dict(self.dictstack[0])) - - def ps_userdict(self): - self.push(ps_dict(self.dictstack[1])) - - def ps_currentdict(self): - self.push(ps_dict(self.dictstack[-1])) - - def ps_currentfile(self): - self.push(ps_file(self.tokenizer)) - - def ps_eexec(self): - f = self.pop('filetype').value - f.starteexec() - - def ps_closefile(self): - f = self.pop('filetype').value - f.skipwhite() - f.stopeexec() - - def ps_cleartomark(self): - obj = self.pop() - while obj != self.mark: - obj = self.pop() - - def ps_readstring(self, - ps_boolean=ps_boolean, - len=len): - s = self.pop('stringtype') - oldstr = s.value - f = self.pop('filetype') - #pad = file.value.read(1) - # for StringIO, this is faster - f.value.pos = f.value.pos + 1 - newstr = f.value.read(len(oldstr)) - s.value = newstr - self.push(s) - self.push(ps_boolean(len(oldstr) == len(newstr))) - - def ps_known(self): - key = self.pop() - d = self.pop('dicttype', 'fonttype') - self.push(ps_boolean(key.value in d.value)) - - def ps_if(self): - proc = self.pop('proceduretype') - if self.pop('booleantype').value: - self.call_procedure(proc) - - def ps_ifelse(self): - proc2 = self.pop('proceduretype') - proc1 = self.pop('proceduretype') - if self.pop('booleantype').value: - self.call_procedure(proc1) - else: - self.call_procedure(proc2) - - def ps_readonly(self): - obj = self.pop() - if obj.access < 1: - obj.access = 1 - self.push(obj) - - def ps_executeonly(self): - obj = self.pop() - if obj.access < 2: - obj.access = 2 - self.push(obj) - - def ps_noaccess(self): - obj = self.pop() - if obj.access < 3: - obj.access = 3 - self.push(obj) - - def ps_not(self): - obj = self.pop('booleantype', 'integertype') - if obj.type == 'booleantype': - self.push(ps_boolean(not obj.value)) - else: - self.push(ps_integer(~obj.value)) - - def ps_print(self): - str = self.pop('stringtype') - print('PS output --->', str.value) - - def ps_anchorsearch(self): - seek = self.pop('stringtype') - s = self.pop('stringtype') - seeklen = len(seek.value) - if s.value[:seeklen] == seek.value: - self.push(ps_string(s.value[seeklen:])) - self.push(seek) - self.push(ps_boolean(1)) - else: - self.push(s) - self.push(ps_boolean(0)) - - def ps_array(self): - num = self.pop('integertype') - array = ps_array([None] * num.value) - self.push(array) - - def ps_astore(self): - array = self.pop('arraytype') - for i in range(len(array.value)-1, -1, -1): - array.value[i] = self.pop() - self.push(array) - - def ps_load(self): - name = self.pop() - self.push(self.resolve_name(name.value)) - - def ps_put(self): - obj1 = self.pop() - obj2 = self.pop() - obj3 = self.pop('arraytype', 'dicttype', 'stringtype', 'proceduretype') - tp = obj3.type - if tp == 'arraytype' or tp == 'proceduretype': - obj3.value[obj2.value] = obj1 - elif tp == 'dicttype': - obj3.value[obj2.value] = obj1 - elif tp == 'stringtype': - index = obj2.value - obj3.value = obj3.value[:index] + chr(obj1.value) + obj3.value[index+1:] - - def ps_get(self): - obj1 = self.pop() - if obj1.value == "Encoding": - pass - obj2 = self.pop('arraytype', 'dicttype', 'stringtype', 'proceduretype', 'fonttype') - tp = obj2.type - if tp in ('arraytype', 'proceduretype'): - self.push(obj2.value[obj1.value]) - elif tp in ('dicttype', 'fonttype'): - self.push(obj2.value[obj1.value]) - elif tp == 'stringtype': - self.push(ps_integer(ord(obj2.value[obj1.value]))) - else: - assert False, "shouldn't get here" - - def ps_getinterval(self): - obj1 = self.pop('integertype') - obj2 = self.pop('integertype') - obj3 = self.pop('arraytype', 'stringtype') - tp = obj3.type - if tp == 'arraytype': - self.push(ps_array(obj3.value[obj2.value:obj2.value + obj1.value])) - elif tp == 'stringtype': - self.push(ps_string(obj3.value[obj2.value:obj2.value + obj1.value])) - - def ps_putinterval(self): - obj1 = self.pop('arraytype', 'stringtype') - obj2 = self.pop('integertype') - obj3 = self.pop('arraytype', 'stringtype') - tp = obj3.type - if tp == 'arraytype': - obj3.value[obj2.value:obj2.value + len(obj1.value)] = obj1.value - elif tp == 'stringtype': - newstr = obj3.value[:obj2.value] - newstr = newstr + obj1.value - newstr = newstr + obj3.value[obj2.value + len(obj1.value):] - obj3.value = newstr - - def ps_cvn(self): - self.push(ps_name(self.pop('stringtype').value)) - - def ps_index(self): - n = self.pop('integertype').value - if n < 0: - raise RuntimeError('index may not be negative') - self.push(self.stack[-1-n]) - - def ps_for(self): - proc = self.pop('proceduretype') - limit = self.pop('integertype', 'realtype').value - increment = self.pop('integertype', 'realtype').value - i = self.pop('integertype', 'realtype').value - while 1: - if increment > 0: - if i > limit: - break - else: - if i < limit: - break - if type(i) == type(0.0): - self.push(ps_real(i)) - else: - self.push(ps_integer(i)) - self.call_procedure(proc) - i = i + increment - - def ps_forall(self): - proc = self.pop('proceduretype') - obj = self.pop('arraytype', 'stringtype', 'dicttype') - tp = obj.type - if tp == 'arraytype': - for item in obj.value: - self.push(item) - self.call_procedure(proc) - elif tp == 'stringtype': - for item in obj.value: - self.push(ps_integer(ord(item))) - self.call_procedure(proc) - elif tp == 'dicttype': - for key, value in obj.value.items(): - self.push(ps_name(key)) - self.push(value) - self.call_procedure(proc) - - def ps_definefont(self): - font = self.pop('dicttype') - name = self.pop() - font = ps_font(font.value) - self.dictstack[0]['FontDirectory'].value[name.value] = font - self.push(font) - - def ps_findfont(self): - name = self.pop() - font = self.dictstack[0]['FontDirectory'].value[name.value] - self.push(font) - - def ps_pop(self): - self.pop() - - def ps_dict(self): - self.pop('integertype') - self.push(ps_dict({})) - - def ps_begin(self): - self.dictstack.append(self.pop('dicttype').value) - - def ps_end(self): - if len(self.dictstack) > 2: - del self.dictstack[-1] - else: - raise RuntimeError('dictstack underflow') - -notdef = '.notdef' + def ps_def(self): + obj = self.pop() + name = self.pop() + self.dictstack[-1][name.value] = obj + + def ps_bind(self): + proc = self.pop("proceduretype") + self.proc_bind(proc) + self.push(proc) + + def proc_bind(self, proc): + for i in range(len(proc.value)): + item = proc.value[i] + if item.type == "proceduretype": + self.proc_bind(item) + else: + if not item.literal: + try: + obj = self.resolve_name(item.value) + except: + pass + else: + if obj.type == "operatortype": + proc.value[i] = obj + + def ps_exch(self): + if len(self.stack) < 2: + raise RuntimeError("stack underflow") + obj1 = self.pop() + obj2 = self.pop() + self.push(obj1) + self.push(obj2) + + def ps_dup(self): + if not self.stack: + raise RuntimeError("stack underflow") + self.push(self.stack[-1]) + + def ps_exec(self): + obj = self.pop() + if obj.type == "proceduretype": + self.call_procedure(obj) + else: + self.handle_object(obj) + + def ps_count(self): + self.push(ps_integer(len(self.stack))) + + def ps_eq(self): + any1 = self.pop() + any2 = self.pop() + self.push(ps_boolean(any1.value == any2.value)) + + def ps_ne(self): + any1 = self.pop() + any2 = self.pop() + self.push(ps_boolean(any1.value != any2.value)) + + def ps_cvx(self): + obj = self.pop() + obj.literal = 0 + self.push(obj) + + def ps_matrix(self): + matrix = [ + ps_real(1.0), + ps_integer(0), + ps_integer(0), + ps_real(1.0), + ps_integer(0), + ps_integer(0), + ] + self.push(ps_array(matrix)) + + def ps_string(self): + num = self.pop("integertype").value + self.push(ps_string("\0" * num)) + + def ps_type(self): + obj = self.pop() + self.push(ps_string(obj.type)) + + def ps_store(self): + value = self.pop() + key = self.pop() + name = key.value + for i in range(len(self.dictstack) - 1, -1, -1): + if name in self.dictstack[i]: + self.dictstack[i][name] = value + break + self.dictstack[-1][name] = value + + def ps_where(self): + name = self.pop() + # XXX + self.push(ps_boolean(0)) + + def ps_systemdict(self): + self.push(ps_dict(self.dictstack[0])) + + def ps_userdict(self): + self.push(ps_dict(self.dictstack[1])) + + def ps_currentdict(self): + self.push(ps_dict(self.dictstack[-1])) + + def ps_currentfile(self): + self.push(ps_file(self.tokenizer)) + + def ps_eexec(self): + f = self.pop("filetype").value + f.starteexec() + + def ps_closefile(self): + f = self.pop("filetype").value + f.skipwhite() + f.stopeexec() + + def ps_cleartomark(self): + obj = self.pop() + while obj != self.mark: + obj = self.pop() + + def ps_readstring(self, ps_boolean=ps_boolean, len=len): + s = self.pop("stringtype") + oldstr = s.value + f = self.pop("filetype") + # pad = file.value.read(1) + # for StringIO, this is faster + f.value.pos = f.value.pos + 1 + newstr = f.value.read(len(oldstr)) + s.value = newstr + self.push(s) + self.push(ps_boolean(len(oldstr) == len(newstr))) + + def ps_known(self): + key = self.pop() + d = self.pop("dicttype", "fonttype") + self.push(ps_boolean(key.value in d.value)) + + def ps_if(self): + proc = self.pop("proceduretype") + if self.pop("booleantype").value: + self.call_procedure(proc) + + def ps_ifelse(self): + proc2 = self.pop("proceduretype") + proc1 = self.pop("proceduretype") + if self.pop("booleantype").value: + self.call_procedure(proc1) + else: + self.call_procedure(proc2) + + def ps_readonly(self): + obj = self.pop() + if obj.access < 1: + obj.access = 1 + self.push(obj) + + def ps_executeonly(self): + obj = self.pop() + if obj.access < 2: + obj.access = 2 + self.push(obj) + + def ps_noaccess(self): + obj = self.pop() + if obj.access < 3: + obj.access = 3 + self.push(obj) + + def ps_not(self): + obj = self.pop("booleantype", "integertype") + if obj.type == "booleantype": + self.push(ps_boolean(not obj.value)) + else: + self.push(ps_integer(~obj.value)) + + def ps_print(self): + str = self.pop("stringtype") + print("PS output --->", str.value) + + def ps_anchorsearch(self): + seek = self.pop("stringtype") + s = self.pop("stringtype") + seeklen = len(seek.value) + if s.value[:seeklen] == seek.value: + self.push(ps_string(s.value[seeklen:])) + self.push(seek) + self.push(ps_boolean(1)) + else: + self.push(s) + self.push(ps_boolean(0)) + + def ps_array(self): + num = self.pop("integertype") + array = ps_array([None] * num.value) + self.push(array) + + def ps_astore(self): + array = self.pop("arraytype") + for i in range(len(array.value) - 1, -1, -1): + array.value[i] = self.pop() + self.push(array) + + def ps_load(self): + name = self.pop() + self.push(self.resolve_name(name.value)) + + def ps_put(self): + obj1 = self.pop() + obj2 = self.pop() + obj3 = self.pop("arraytype", "dicttype", "stringtype", "proceduretype") + tp = obj3.type + if tp == "arraytype" or tp == "proceduretype": + obj3.value[obj2.value] = obj1 + elif tp == "dicttype": + obj3.value[obj2.value] = obj1 + elif tp == "stringtype": + index = obj2.value + obj3.value = obj3.value[:index] + chr(obj1.value) + obj3.value[index + 1 :] + + def ps_get(self): + obj1 = self.pop() + if obj1.value == "Encoding": + pass + obj2 = self.pop( + "arraytype", "dicttype", "stringtype", "proceduretype", "fonttype" + ) + tp = obj2.type + if tp in ("arraytype", "proceduretype"): + self.push(obj2.value[obj1.value]) + elif tp in ("dicttype", "fonttype"): + self.push(obj2.value[obj1.value]) + elif tp == "stringtype": + self.push(ps_integer(ord(obj2.value[obj1.value]))) + else: + assert False, "shouldn't get here" + + def ps_getinterval(self): + obj1 = self.pop("integertype") + obj2 = self.pop("integertype") + obj3 = self.pop("arraytype", "stringtype") + tp = obj3.type + if tp == "arraytype": + self.push(ps_array(obj3.value[obj2.value : obj2.value + obj1.value])) + elif tp == "stringtype": + self.push(ps_string(obj3.value[obj2.value : obj2.value + obj1.value])) + + def ps_putinterval(self): + obj1 = self.pop("arraytype", "stringtype") + obj2 = self.pop("integertype") + obj3 = self.pop("arraytype", "stringtype") + tp = obj3.type + if tp == "arraytype": + obj3.value[obj2.value : obj2.value + len(obj1.value)] = obj1.value + elif tp == "stringtype": + newstr = obj3.value[: obj2.value] + newstr = newstr + obj1.value + newstr = newstr + obj3.value[obj2.value + len(obj1.value) :] + obj3.value = newstr + + def ps_cvn(self): + self.push(ps_name(self.pop("stringtype").value)) + + def ps_index(self): + n = self.pop("integertype").value + if n < 0: + raise RuntimeError("index may not be negative") + self.push(self.stack[-1 - n]) + + def ps_for(self): + proc = self.pop("proceduretype") + limit = self.pop("integertype", "realtype").value + increment = self.pop("integertype", "realtype").value + i = self.pop("integertype", "realtype").value + while 1: + if increment > 0: + if i > limit: + break + else: + if i < limit: + break + if type(i) == type(0.0): + self.push(ps_real(i)) + else: + self.push(ps_integer(i)) + self.call_procedure(proc) + i = i + increment + + def ps_forall(self): + proc = self.pop("proceduretype") + obj = self.pop("arraytype", "stringtype", "dicttype") + tp = obj.type + if tp == "arraytype": + for item in obj.value: + self.push(item) + self.call_procedure(proc) + elif tp == "stringtype": + for item in obj.value: + self.push(ps_integer(ord(item))) + self.call_procedure(proc) + elif tp == "dicttype": + for key, value in obj.value.items(): + self.push(ps_name(key)) + self.push(value) + self.call_procedure(proc) + + def ps_definefont(self): + font = self.pop("dicttype") + name = self.pop() + font = ps_font(font.value) + self.dictstack[0]["FontDirectory"].value[name.value] = font + self.push(font) + + def ps_findfont(self): + name = self.pop() + font = self.dictstack[0]["FontDirectory"].value[name.value] + self.push(font) + + def ps_pop(self): + self.pop() + + def ps_dict(self): + self.pop("integertype") + self.push(ps_dict({})) + + def ps_begin(self): + self.dictstack.append(self.pop("dicttype").value) + + def ps_end(self): + if len(self.dictstack) > 2: + del self.dictstack[-1] + else: + raise RuntimeError("dictstack underflow") + + +notdef = ".notdef" from fontTools.encodings.StandardEncoding import StandardEncoding + ps_StandardEncoding = list(map(ps_name, StandardEncoding)) diff --git a/Lib/fontTools/misc/roundTools.py b/Lib/fontTools/misc/roundTools.py index 6f4aa634..a4d45c31 100644 --- a/Lib/fontTools/misc/roundTools.py +++ b/Lib/fontTools/misc/roundTools.py @@ -9,41 +9,46 @@ import logging log = logging.getLogger(__name__) __all__ = [ - "noRound", - "otRound", - "maybeRound", - "roundFunc", + "noRound", + "otRound", + "maybeRound", + "roundFunc", + "nearestMultipleShortestRepr", ] + def noRound(value): - return value + return value + def otRound(value): - """Round float value to nearest integer towards ``+Infinity``. + """Round float value to nearest integer towards ``+Infinity``. + + The OpenType spec (in the section on `"normalization" of OpenType Font Variations <https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview#coordinate-scales-and-normalization>`_) + defines the required method for converting floating point values to + fixed-point. In particular it specifies the following rounding strategy: - The OpenType spec (in the section on `"normalization" of OpenType Font Variations <https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview#coordinate-scales-and-normalization>`_) - defines the required method for converting floating point values to - fixed-point. In particular it specifies the following rounding strategy: + for fractional values of 0.5 and higher, take the next higher integer; + for other fractional values, truncate. - for fractional values of 0.5 and higher, take the next higher integer; - for other fractional values, truncate. + This function rounds the floating-point value according to this strategy + in preparation for conversion to fixed-point. - This function rounds the floating-point value according to this strategy - in preparation for conversion to fixed-point. + Args: + value (float): The input floating-point value. - Args: - value (float): The input floating-point value. + Returns + float: The rounded value. + """ + # See this thread for how we ended up with this implementation: + # https://github.com/fonttools/fonttools/issues/1248#issuecomment-383198166 + return int(math.floor(value + 0.5)) - Returns - float: The rounded value. - """ - # See this thread for how we ended up with this implementation: - # https://github.com/fonttools/fonttools/issues/1248#issuecomment-383198166 - return int(math.floor(value + 0.5)) def maybeRound(v, tolerance, round=otRound): - rounded = round(v) - return rounded if abs(rounded - v) <= tolerance else v + rounded = round(v) + return rounded if abs(rounded - v) <= tolerance else v + def roundFunc(tolerance, round=otRound): if tolerance < 0: @@ -52,7 +57,7 @@ def roundFunc(tolerance, round=otRound): if tolerance == 0: return noRound - if tolerance >= .5: + if tolerance >= 0.5: return round return functools.partial(maybeRound, tolerance=tolerance, round=round) @@ -85,7 +90,7 @@ def nearestMultipleShortestRepr(value: float, factor: float) -> str: return "0.0" value = otRound(value / factor) * factor - eps = .5 * factor + eps = 0.5 * factor lo = value - eps hi = value + eps # If the range of valid choices spans an integer, return the integer. @@ -99,7 +104,7 @@ def nearestMultipleShortestRepr(value: float, factor: float) -> str: for i in range(len(lo)): if lo[i] != hi[i]: break - period = lo.find('.') + period = lo.find(".") assert period < i fmt = "%%.%df" % (i - period) return fmt % value diff --git a/Lib/fontTools/misc/sstruct.py b/Lib/fontTools/misc/sstruct.py index 6db8b515..d35bc9a5 100644 --- a/Lib/fontTools/misc/sstruct.py +++ b/Lib/fontTools/misc/sstruct.py @@ -56,68 +56,72 @@ __copyright__ = "Copyright 1998, Just van Rossum <just@letterror.com>" class Error(Exception): - pass + pass + def pack(fmt, obj): - formatstring, names, fixes = getformat(fmt, keep_pad_byte=True) - elements = [] - if not isinstance(obj, dict): - obj = obj.__dict__ - for name in names: - value = obj[name] - if name in fixes: - # fixed point conversion - value = fl2fi(value, fixes[name]) - elif isinstance(value, str): - value = tobytes(value) - elements.append(value) - data = struct.pack(*(formatstring,) + tuple(elements)) - return data + formatstring, names, fixes = getformat(fmt, keep_pad_byte=True) + elements = [] + if not isinstance(obj, dict): + obj = obj.__dict__ + for name in names: + value = obj[name] + if name in fixes: + # fixed point conversion + value = fl2fi(value, fixes[name]) + elif isinstance(value, str): + value = tobytes(value) + elements.append(value) + data = struct.pack(*(formatstring,) + tuple(elements)) + return data + def unpack(fmt, data, obj=None): - if obj is None: - obj = {} - data = tobytes(data) - formatstring, names, fixes = getformat(fmt) - if isinstance(obj, dict): - d = obj - else: - d = obj.__dict__ - elements = struct.unpack(formatstring, data) - for i in range(len(names)): - name = names[i] - value = elements[i] - if name in fixes: - # fixed point conversion - value = fi2fl(value, fixes[name]) - elif isinstance(value, bytes): - try: - value = tostr(value) - except UnicodeDecodeError: - pass - d[name] = value - return obj + if obj is None: + obj = {} + data = tobytes(data) + formatstring, names, fixes = getformat(fmt) + if isinstance(obj, dict): + d = obj + else: + d = obj.__dict__ + elements = struct.unpack(formatstring, data) + for i in range(len(names)): + name = names[i] + value = elements[i] + if name in fixes: + # fixed point conversion + value = fi2fl(value, fixes[name]) + elif isinstance(value, bytes): + try: + value = tostr(value) + except UnicodeDecodeError: + pass + d[name] = value + return obj + def unpack2(fmt, data, obj=None): - length = calcsize(fmt) - return unpack(fmt, data[:length], obj), data[length:] + length = calcsize(fmt) + return unpack(fmt, data[:length], obj), data[length:] + def calcsize(fmt): - formatstring, names, fixes = getformat(fmt) - return struct.calcsize(formatstring) + formatstring, names, fixes = getformat(fmt) + return struct.calcsize(formatstring) # matches "name:formatchar" (whitespace is allowed) _elementRE = re.compile( - r"\s*" # whitespace - r"([A-Za-z_][A-Za-z_0-9]*)" # name (python identifier) - r"\s*:\s*" # whitespace : whitespace - r"([xcbB?hHiIlLqQfd]|" # formatchar... - r"[0-9]+[ps]|" # ...formatchar... - r"([0-9]+)\.([0-9]+)(F))" # ...formatchar - r"\s*" # whitespace - r"(#.*)?$" # [comment] + end of string - ) + r"\s*" # whitespace + r"([A-Za-z_][A-Za-z_0-9]*)" # name (python identifier) + r"\s*:\s*" # whitespace : whitespace + r"([xcbB?hHiIlLqQfd]|" # formatchar... + r"[0-9]+[ps]|" # ...formatchar... + r"([0-9]+)\.([0-9]+)(F))" # ...formatchar + r"\s*" # whitespace + r"(#.*)?$" # [comment] + end of string +) # matches the special struct fmt chars and 'x' (pad byte) _extraRE = re.compile(r"\s*([x@=<>!])\s*(#.*)?$") @@ -125,54 +129,53 @@ _extraRE = re.compile(r"\s*([x@=<>!])\s*(#.*)?$") # matches an "empty" string, possibly containing whitespace and/or a comment _emptyRE = re.compile(r"\s*(#.*)?$") -_fixedpointmappings = { - 8: "b", - 16: "h", - 32: "l"} +_fixedpointmappings = {8: "b", 16: "h", 32: "l"} _formatcache = {} + def getformat(fmt, keep_pad_byte=False): - fmt = tostr(fmt, encoding="ascii") - try: - formatstring, names, fixes = _formatcache[fmt] - except KeyError: - lines = re.split("[\n;]", fmt) - formatstring = "" - names = [] - fixes = {} - for line in lines: - if _emptyRE.match(line): - continue - m = _extraRE.match(line) - if m: - formatchar = m.group(1) - if formatchar != 'x' and formatstring: - raise Error("a special fmt char must be first") - else: - m = _elementRE.match(line) - if not m: - raise Error("syntax error in fmt: '%s'" % line) - name = m.group(1) - formatchar = m.group(2) - if keep_pad_byte or formatchar != "x": - names.append(name) - if m.group(3): - # fixed point - before = int(m.group(3)) - after = int(m.group(4)) - bits = before + after - if bits not in [8, 16, 32]: - raise Error("fixed point must be 8, 16 or 32 bits long") - formatchar = _fixedpointmappings[bits] - assert m.group(5) == "F" - fixes[name] = after - formatstring = formatstring + formatchar - _formatcache[fmt] = formatstring, names, fixes - return formatstring, names, fixes + fmt = tostr(fmt, encoding="ascii") + try: + formatstring, names, fixes = _formatcache[fmt] + except KeyError: + lines = re.split("[\n;]", fmt) + formatstring = "" + names = [] + fixes = {} + for line in lines: + if _emptyRE.match(line): + continue + m = _extraRE.match(line) + if m: + formatchar = m.group(1) + if formatchar != "x" and formatstring: + raise Error("a special fmt char must be first") + else: + m = _elementRE.match(line) + if not m: + raise Error("syntax error in fmt: '%s'" % line) + name = m.group(1) + formatchar = m.group(2) + if keep_pad_byte or formatchar != "x": + names.append(name) + if m.group(3): + # fixed point + before = int(m.group(3)) + after = int(m.group(4)) + bits = before + after + if bits not in [8, 16, 32]: + raise Error("fixed point must be 8, 16 or 32 bits long") + formatchar = _fixedpointmappings[bits] + assert m.group(5) == "F" + fixes[name] = after + formatstring = formatstring + formatchar + _formatcache[fmt] = formatstring, names, fixes + return formatstring, names, fixes + def _test(): - fmt = """ + fmt = """ # comments are allowed > # big endian (see documentation for struct) # empty lines are allowed: @@ -188,29 +191,30 @@ def _test(): apad: x """ - print('size:', calcsize(fmt)) + print("size:", calcsize(fmt)) + + class foo(object): + pass - class foo(object): - pass + i = foo() - i = foo() + i.ashort = 0x7FFF + i.along = 0x7FFFFFFF + i.abyte = 0x7F + i.achar = "a" + i.astr = "12345" + i.afloat = 0.5 + i.adouble = 0.5 + i.afixed = 1.5 + i.abool = True - i.ashort = 0x7fff - i.along = 0x7fffffff - i.abyte = 0x7f - i.achar = "a" - i.astr = "12345" - i.afloat = 0.5 - i.adouble = 0.5 - i.afixed = 1.5 - i.abool = True + data = pack(fmt, i) + print("data:", repr(data)) + print(unpack(fmt, data)) + i2 = foo() + unpack(fmt, data, i2) + print(vars(i2)) - data = pack(fmt, i) - print('data:', repr(data)) - print(unpack(fmt, data)) - i2 = foo() - unpack(fmt, data, i2) - print(vars(i2)) if __name__ == "__main__": - _test() + _test() diff --git a/Lib/fontTools/misc/symfont.py b/Lib/fontTools/misc/symfont.py index 3ff2b5df..fb9e20a4 100644 --- a/Lib/fontTools/misc/symfont.py +++ b/Lib/fontTools/misc/symfont.py @@ -4,98 +4,103 @@ from itertools import count import sympy as sp import sys -n = 3 # Max Bezier degree; 3 for cubic, 2 for quadratic +n = 3 # Max Bezier degree; 3 for cubic, 2 for quadratic -t, x, y = sp.symbols('t x y', real=True) -c = sp.symbols('c', real=False) # Complex representation instead of x/y +t, x, y = sp.symbols("t x y", real=True) +c = sp.symbols("c", real=False) # Complex representation instead of x/y -X = tuple(sp.symbols('x:%d'%(n+1), real=True)) -Y = tuple(sp.symbols('y:%d'%(n+1), real=True)) -P = tuple(zip(*(sp.symbols('p:%d[%s]'%(n+1,w), real=True) for w in '01'))) -C = tuple(sp.symbols('c:%d'%(n+1), real=False)) +X = tuple(sp.symbols("x:%d" % (n + 1), real=True)) +Y = tuple(sp.symbols("y:%d" % (n + 1), real=True)) +P = tuple(zip(*(sp.symbols("p:%d[%s]" % (n + 1, w), real=True) for w in "01"))) +C = tuple(sp.symbols("c:%d" % (n + 1), real=False)) # Cubic Bernstein basis functions BinomialCoefficient = [(1, 0)] -for i in range(1, n+1): - last = BinomialCoefficient[-1] - this = tuple(last[j-1]+last[j] for j in range(len(last)))+(0,) - BinomialCoefficient.append(this) +for i in range(1, n + 1): + last = BinomialCoefficient[-1] + this = tuple(last[j - 1] + last[j] for j in range(len(last))) + (0,) + BinomialCoefficient.append(this) BinomialCoefficient = tuple(tuple(item[:-1]) for item in BinomialCoefficient) del last, this BernsteinPolynomial = tuple( - tuple(c * t**i * (1-t)**(n-i) for i,c in enumerate(coeffs)) - for n,coeffs in enumerate(BinomialCoefficient)) + tuple(c * t**i * (1 - t) ** (n - i) for i, c in enumerate(coeffs)) + for n, coeffs in enumerate(BinomialCoefficient) +) BezierCurve = tuple( - tuple(sum(P[i][j]*bernstein for i,bernstein in enumerate(bernsteins)) - for j in range(2)) - for n,bernsteins in enumerate(BernsteinPolynomial)) + tuple( + sum(P[i][j] * bernstein for i, bernstein in enumerate(bernsteins)) + for j in range(2) + ) + for n, bernsteins in enumerate(BernsteinPolynomial) +) BezierCurveC = tuple( - sum(C[i]*bernstein for i,bernstein in enumerate(bernsteins)) - for n,bernsteins in enumerate(BernsteinPolynomial)) + sum(C[i] * bernstein for i, bernstein in enumerate(bernsteins)) + for n, bernsteins in enumerate(BernsteinPolynomial) +) def green(f, curveXY): - f = -sp.integrate(sp.sympify(f), y) - f = f.subs({x:curveXY[0], y:curveXY[1]}) - f = sp.integrate(f * sp.diff(curveXY[0], t), (t, 0, 1)) - return f + f = -sp.integrate(sp.sympify(f), y) + f = f.subs({x: curveXY[0], y: curveXY[1]}) + f = sp.integrate(f * sp.diff(curveXY[0], t), (t, 0, 1)) + return f class _BezierFuncsLazy(dict): + def __init__(self, symfunc): + self._symfunc = symfunc + self._bezfuncs = {} - def __init__(self, symfunc): - self._symfunc = symfunc - self._bezfuncs = {} + def __missing__(self, i): + args = ["p%d" % d for d in range(i + 1)] + f = green(self._symfunc, BezierCurve[i]) + f = sp.gcd_terms(f.collect(sum(P, ()))) # Optimize + return sp.lambdify(args, f) - def __missing__(self, i): - args = ['p%d'%d for d in range(i+1)] - f = green(self._symfunc, BezierCurve[i]) - f = sp.gcd_terms(f.collect(sum(P,()))) # Optimize - return sp.lambdify(args, f) class GreenPen(BasePen): + _BezierFuncs = {} - _BezierFuncs = {} + @classmethod + def _getGreenBezierFuncs(celf, func): + funcstr = str(func) + if not funcstr in celf._BezierFuncs: + celf._BezierFuncs[funcstr] = _BezierFuncsLazy(func) + return celf._BezierFuncs[funcstr] - @classmethod - def _getGreenBezierFuncs(celf, func): - funcstr = str(func) - if not funcstr in celf._BezierFuncs: - celf._BezierFuncs[funcstr] = _BezierFuncsLazy(func) - return celf._BezierFuncs[funcstr] + def __init__(self, func, glyphset=None): + BasePen.__init__(self, glyphset) + self._funcs = self._getGreenBezierFuncs(func) + self.value = 0 - def __init__(self, func, glyphset=None): - BasePen.__init__(self, glyphset) - self._funcs = self._getGreenBezierFuncs(func) - self.value = 0 + def _moveTo(self, p0): + self.__startPoint = p0 - def _moveTo(self, p0): - self.__startPoint = p0 + def _closePath(self): + p0 = self._getCurrentPoint() + if p0 != self.__startPoint: + self._lineTo(self.__startPoint) - def _closePath(self): - p0 = self._getCurrentPoint() - if p0 != self.__startPoint: - self._lineTo(self.__startPoint) + def _endPath(self): + p0 = self._getCurrentPoint() + if p0 != self.__startPoint: + # Green theorem is not defined on open contours. + raise NotImplementedError - def _endPath(self): - p0 = self._getCurrentPoint() - if p0 != self.__startPoint: - # Green theorem is not defined on open contours. - raise NotImplementedError + def _lineTo(self, p1): + p0 = self._getCurrentPoint() + self.value += self._funcs[1](p0, p1) - def _lineTo(self, p1): - p0 = self._getCurrentPoint() - self.value += self._funcs[1](p0, p1) + def _qCurveToOne(self, p1, p2): + p0 = self._getCurrentPoint() + self.value += self._funcs[2](p0, p1, p2) - def _qCurveToOne(self, p1, p2): - p0 = self._getCurrentPoint() - self.value += self._funcs[2](p0, p1, p2) + def _curveToOne(self, p1, p2, p3): + p0 = self._getCurrentPoint() + self.value += self._funcs[3](p0, p1, p2, p3) - def _curveToOne(self, p1, p2, p3): - p0 = self._getCurrentPoint() - self.value += self._funcs[3](p0, p1, p2, p3) # Sample pens. # Do not use this in real code. @@ -103,29 +108,25 @@ class GreenPen(BasePen): AreaPen = partial(GreenPen, func=1) MomentXPen = partial(GreenPen, func=x) MomentYPen = partial(GreenPen, func=y) -MomentXXPen = partial(GreenPen, func=x*x) -MomentYYPen = partial(GreenPen, func=y*y) -MomentXYPen = partial(GreenPen, func=x*y) +MomentXXPen = partial(GreenPen, func=x * x) +MomentYYPen = partial(GreenPen, func=y * y) +MomentXYPen = partial(GreenPen, func=x * y) def printGreenPen(penName, funcs, file=sys.stdout, docstring=None): + if docstring is not None: + print('"""%s"""' % docstring) - if docstring is not None: - print('"""%s"""' % docstring) - - print( -'''from fontTools.pens.basePen import BasePen, OpenContourError + print( + """from fontTools.pens.basePen import BasePen, OpenContourError try: import cython -except ImportError: + + COMPILED = cython.compiled +except (AttributeError, ImportError): # if cython not installed, use mock module with no-op decorators and types from fontTools.misc import cython -if cython.compiled: - # Yep, I'm compiled. - COMPILED = True -else: - # Just a lowly interpreted script. COMPILED = False @@ -135,10 +136,14 @@ class %s(BasePen): def __init__(self, glyphset=None): BasePen.__init__(self, glyphset) -'''% (penName, penName), file=file) - for name,f in funcs: - print(' self.%s = 0' % name, file=file) - print(''' +""" + % (penName, penName), + file=file, + ) + for name, f in funcs: + print(" self.%s = 0" % name, file=file) + print( + """ def _moveTo(self, p0): self.__startPoint = p0 @@ -154,32 +159,39 @@ class %s(BasePen): raise OpenContourError( "Green theorem is not defined on open contours." ) -''', end='', file=file) - - for n in (1, 2, 3): - - - subs = {P[i][j]: [X, Y][j][i] for i in range(n+1) for j in range(2)} - greens = [green(f, BezierCurve[n]) for name,f in funcs] - greens = [sp.gcd_terms(f.collect(sum(P,()))) for f in greens] # Optimize - greens = [f.subs(subs) for f in greens] # Convert to p to x/y - defs, exprs = sp.cse(greens, - optimizations='basic', - symbols=(sp.Symbol('r%d'%i) for i in count())) - - print() - for name,value in defs: - print(' @cython.locals(%s=cython.double)' % name, file=file) - if n == 1: - print('''\ +""", + end="", + file=file, + ) + + for n in (1, 2, 3): + subs = {P[i][j]: [X, Y][j][i] for i in range(n + 1) for j in range(2)} + greens = [green(f, BezierCurve[n]) for name, f in funcs] + greens = [sp.gcd_terms(f.collect(sum(P, ()))) for f in greens] # Optimize + greens = [f.subs(subs) for f in greens] # Convert to p to x/y + defs, exprs = sp.cse( + greens, + optimizations="basic", + symbols=(sp.Symbol("r%d" % i) for i in count()), + ) + + print() + for name, value in defs: + print(" @cython.locals(%s=cython.double)" % name, file=file) + if n == 1: + print( + """\ @cython.locals(x0=cython.double, y0=cython.double) @cython.locals(x1=cython.double, y1=cython.double) def _lineTo(self, p1): x0,y0 = self._getCurrentPoint() x1,y1 = p1 -''', file=file) - elif n == 2: - print('''\ +""", + file=file, + ) + elif n == 2: + print( + """\ @cython.locals(x0=cython.double, y0=cython.double) @cython.locals(x1=cython.double, y1=cython.double) @cython.locals(x2=cython.double, y2=cython.double) @@ -187,9 +199,12 @@ class %s(BasePen): x0,y0 = self._getCurrentPoint() x1,y1 = p1 x2,y2 = p2 -''', file=file) - elif n == 3: - print('''\ +""", + file=file, + ) + elif n == 3: + print( + """\ @cython.locals(x0=cython.double, y0=cython.double) @cython.locals(x1=cython.double, y1=cython.double) @cython.locals(x2=cython.double, y2=cython.double) @@ -199,29 +214,35 @@ class %s(BasePen): x1,y1 = p1 x2,y2 = p2 x3,y3 = p3 -''', file=file) - for name,value in defs: - print(' %s = %s' % (name, value), file=file) - - print(file=file) - for name,value in zip([f[0] for f in funcs], exprs): - print(' self.%s += %s' % (name, value), file=file) - - print(''' +""", + file=file, + ) + for name, value in defs: + print(" %s = %s" % (name, value), file=file) + + print(file=file) + for name, value in zip([f[0] for f in funcs], exprs): + print(" self.%s += %s" % (name, value), file=file) + + print( + """ if __name__ == '__main__': from fontTools.misc.symfont import x, y, printGreenPen - printGreenPen('%s', ['''%penName, file=file) - for name,f in funcs: - print(" ('%s', %s)," % (name, str(f)), file=file) - print(' ])', file=file) - - -if __name__ == '__main__': - pen = AreaPen() - pen.moveTo((100,100)) - pen.lineTo((100,200)) - pen.lineTo((200,200)) - pen.curveTo((200,250),(300,300),(250,350)) - pen.lineTo((200,100)) - pen.closePath() - print(pen.value) + printGreenPen('%s', [""" + % penName, + file=file, + ) + for name, f in funcs: + print(" ('%s', %s)," % (name, str(f)), file=file) + print(" ])", file=file) + + +if __name__ == "__main__": + pen = AreaPen() + pen.moveTo((100, 100)) + pen.lineTo((100, 200)) + pen.lineTo((200, 200)) + pen.curveTo((200, 250), (300, 300), (250, 350)) + pen.lineTo((200, 100)) + pen.closePath() + print(pen.value) diff --git a/Lib/fontTools/misc/testTools.py b/Lib/fontTools/misc/testTools.py index 871a9951..be611613 100644 --- a/Lib/fontTools/misc/testTools.py +++ b/Lib/fontTools/misc/testTools.py @@ -29,12 +29,14 @@ def parseXML(xmlSnippet): if isinstance(xmlSnippet, bytes): xml += xmlSnippet elif isinstance(xmlSnippet, str): - xml += tobytes(xmlSnippet, 'utf-8') + xml += tobytes(xmlSnippet, "utf-8") elif isinstance(xmlSnippet, Iterable): - xml += b"".join(tobytes(s, 'utf-8') for s in xmlSnippet) + xml += b"".join(tobytes(s, "utf-8") for s in xmlSnippet) else: - raise TypeError("expected string or sequence of strings; found %r" - % type(xmlSnippet).__name__) + raise TypeError( + "expected string or sequence of strings; found %r" + % type(xmlSnippet).__name__ + ) xml += b"</root>" reader.parser.Parse(xml, 0) return reader.root[2] @@ -76,6 +78,7 @@ class FakeFont: return self.glyphOrder_[glyphID] else: return "glyph%.5d" % glyphID + def getGlyphNameMany(self, lst): return [self.getGlyphName(gid) for gid in lst] @@ -92,6 +95,7 @@ class FakeFont: class TestXMLReader_(object): def __init__(self): from xml.parsers.expat import ParserCreate + self.parser = ParserCreate() self.parser.StartElementHandler = self.startElement_ self.parser.EndElementHandler = self.endElement_ @@ -114,7 +118,7 @@ class TestXMLReader_(object): self.stack[-1][2].append(data) -def makeXMLWriter(newlinestr='\n'): +def makeXMLWriter(newlinestr="\n"): # don't write OS-specific new lines writer = XMLWriter(BytesIO(), newlinestr=newlinestr) # erase XML declaration @@ -166,7 +170,7 @@ class MockFont(object): to its glyphOrder.""" def __init__(self): - self._glyphOrder = ['.notdef'] + self._glyphOrder = [".notdef"] class AllocatingDict(dict): def __missing__(reverseDict, key): @@ -174,7 +178,8 @@ class MockFont(object): gid = len(reverseDict) reverseDict[key] = gid return gid - self._reverseGlyphOrder = AllocatingDict({'.notdef': 0}) + + self._reverseGlyphOrder = AllocatingDict({".notdef": 0}) self.lazy = False def getGlyphID(self, glyph): @@ -192,7 +197,6 @@ class MockFont(object): class TestCase(_TestCase): - def __init__(self, methodName): _TestCase.__init__(self, methodName) # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, @@ -202,7 +206,6 @@ class TestCase(_TestCase): class DataFilesHandler(TestCase): - def setUp(self): self.tempdir = None self.num_tempfiles = 0 diff --git a/Lib/fontTools/misc/textTools.py b/Lib/fontTools/misc/textTools.py index bf75bcbd..f7ca1acc 100644 --- a/Lib/fontTools/misc/textTools.py +++ b/Lib/fontTools/misc/textTools.py @@ -33,90 +33,90 @@ class Tag(str): def readHex(content): - """Convert a list of hex strings to binary data.""" - return deHexStr(strjoin(chunk for chunk in content if isinstance(chunk, str))) + """Convert a list of hex strings to binary data.""" + return deHexStr(strjoin(chunk for chunk in content if isinstance(chunk, str))) def deHexStr(hexdata): - """Convert a hex string to binary data.""" - hexdata = strjoin(hexdata.split()) - if len(hexdata) % 2: - hexdata = hexdata + "0" - data = [] - for i in range(0, len(hexdata), 2): - data.append(bytechr(int(hexdata[i:i+2], 16))) - return bytesjoin(data) + """Convert a hex string to binary data.""" + hexdata = strjoin(hexdata.split()) + if len(hexdata) % 2: + hexdata = hexdata + "0" + data = [] + for i in range(0, len(hexdata), 2): + data.append(bytechr(int(hexdata[i : i + 2], 16))) + return bytesjoin(data) def hexStr(data): - """Convert binary data to a hex string.""" - h = string.hexdigits - r = '' - for c in data: - i = byteord(c) - r = r + h[(i >> 4) & 0xF] + h[i & 0xF] - return r + """Convert binary data to a hex string.""" + h = string.hexdigits + r = "" + for c in data: + i = byteord(c) + r = r + h[(i >> 4) & 0xF] + h[i & 0xF] + return r def num2binary(l, bits=32): - items = [] - binary = "" - for i in range(bits): - if l & 0x1: - binary = "1" + binary - else: - binary = "0" + binary - l = l >> 1 - if not ((i+1) % 8): - items.append(binary) - binary = "" - if binary: - items.append(binary) - items.reverse() - assert l in (0, -1), "number doesn't fit in number of bits" - return ' '.join(items) + items = [] + binary = "" + for i in range(bits): + if l & 0x1: + binary = "1" + binary + else: + binary = "0" + binary + l = l >> 1 + if not ((i + 1) % 8): + items.append(binary) + binary = "" + if binary: + items.append(binary) + items.reverse() + assert l in (0, -1), "number doesn't fit in number of bits" + return " ".join(items) def binary2num(bin): - bin = strjoin(bin.split()) - l = 0 - for digit in bin: - l = l << 1 - if digit != "0": - l = l | 0x1 - return l + bin = strjoin(bin.split()) + l = 0 + for digit in bin: + l = l << 1 + if digit != "0": + l = l | 0x1 + return l def caselessSort(alist): - """Return a sorted copy of a list. If there are only strings - in the list, it will not consider case. - """ + """Return a sorted copy of a list. If there are only strings + in the list, it will not consider case. + """ - try: - return sorted(alist, key=lambda a: (a.lower(), a)) - except TypeError: - return sorted(alist) + try: + return sorted(alist, key=lambda a: (a.lower(), a)) + except TypeError: + return sorted(alist) def pad(data, size): - r""" Pad byte string 'data' with null bytes until its length is a - multiple of 'size'. - - >>> len(pad(b'abcd', 4)) - 4 - >>> len(pad(b'abcde', 2)) - 6 - >>> len(pad(b'abcde', 4)) - 8 - >>> pad(b'abcdef', 4) == b'abcdef\x00\x00' - True - """ - data = tobytes(data) - if size > 1: - remainder = len(data) % size - if remainder: - data += b"\0" * (size - remainder) - return data + r"""Pad byte string 'data' with null bytes until its length is a + multiple of 'size'. + + >>> len(pad(b'abcd', 4)) + 4 + >>> len(pad(b'abcde', 2)) + 6 + >>> len(pad(b'abcde', 4)) + 8 + >>> pad(b'abcdef', 4) == b'abcdef\x00\x00' + True + """ + data = tobytes(data) + if size > 1: + remainder = len(data) % size + if remainder: + data += b"\0" * (size - remainder) + return data def tostr(s, encoding="ascii", errors="strict"): @@ -150,5 +150,6 @@ def bytesjoin(iterable, joiner=b""): if __name__ == "__main__": - import doctest, sys - sys.exit(doctest.testmod().failed) + import doctest, sys + + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/timeTools.py b/Lib/fontTools/misc/timeTools.py index f4b84f6e..175ce815 100644 --- a/Lib/fontTools/misc/timeTools.py +++ b/Lib/fontTools/misc/timeTools.py @@ -10,59 +10,79 @@ import calendar epoch_diff = calendar.timegm((1904, 1, 1, 0, 0, 0, 0, 0, 0)) DAYNAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] -MONTHNAMES = [None, "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +MONTHNAMES = [ + None, + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +] def asctime(t=None): - """ - Convert a tuple or struct_time representing a time as returned by gmtime() - or localtime() to a 24-character string of the following form: - - >>> asctime(time.gmtime(0)) - 'Thu Jan 1 00:00:00 1970' - - If t is not provided, the current time as returned by localtime() is used. - Locale information is not used by asctime(). - - This is meant to normalise the output of the built-in time.asctime() across - different platforms and Python versions. - In Python 3.x, the day of the month is right-justified, whereas on Windows - Python 2.7 it is padded with zeros. - - See https://github.com/fonttools/fonttools/issues/455 - """ - if t is None: - t = time.localtime() - s = "%s %s %2s %s" % ( - DAYNAMES[t.tm_wday], MONTHNAMES[t.tm_mon], t.tm_mday, - time.strftime("%H:%M:%S %Y", t)) - return s + """ + Convert a tuple or struct_time representing a time as returned by gmtime() + or localtime() to a 24-character string of the following form: + + >>> asctime(time.gmtime(0)) + 'Thu Jan 1 00:00:00 1970' + + If t is not provided, the current time as returned by localtime() is used. + Locale information is not used by asctime(). + + This is meant to normalise the output of the built-in time.asctime() across + different platforms and Python versions. + In Python 3.x, the day of the month is right-justified, whereas on Windows + Python 2.7 it is padded with zeros. + + See https://github.com/fonttools/fonttools/issues/455 + """ + if t is None: + t = time.localtime() + s = "%s %s %2s %s" % ( + DAYNAMES[t.tm_wday], + MONTHNAMES[t.tm_mon], + t.tm_mday, + time.strftime("%H:%M:%S %Y", t), + ) + return s def timestampToString(value): - return asctime(time.gmtime(max(0, value + epoch_diff))) + return asctime(time.gmtime(max(0, value + epoch_diff))) + def timestampFromString(value): - wkday, mnth = value[:7].split() - t = datetime.strptime(value[7:], ' %d %H:%M:%S %Y') - t = t.replace(month=MONTHNAMES.index(mnth), tzinfo=timezone.utc) - wkday_idx = DAYNAMES.index(wkday) - assert t.weekday() == wkday_idx, '"' + value + '" has inconsistent weekday' - return int(t.timestamp()) - epoch_diff + wkday, mnth = value[:7].split() + t = datetime.strptime(value[7:], " %d %H:%M:%S %Y") + t = t.replace(month=MONTHNAMES.index(mnth), tzinfo=timezone.utc) + wkday_idx = DAYNAMES.index(wkday) + assert t.weekday() == wkday_idx, '"' + value + '" has inconsistent weekday' + return int(t.timestamp()) - epoch_diff + def timestampNow(): - # https://reproducible-builds.org/specs/source-date-epoch/ - source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") - if source_date_epoch is not None: - return int(source_date_epoch) - epoch_diff - return int(time.time() - epoch_diff) + # https://reproducible-builds.org/specs/source-date-epoch/ + source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") + if source_date_epoch is not None: + return int(source_date_epoch) - epoch_diff + return int(time.time() - epoch_diff) + def timestampSinceEpoch(value): - return int(value - epoch_diff) + return int(value - epoch_diff) if __name__ == "__main__": - import sys - import doctest - sys.exit(doctest.testmod().failed) + import sys + import doctest + + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py index 94e1f622..f85b54b7 100644 --- a/Lib/fontTools/misc/transform.py +++ b/Lib/fontTools/misc/transform.py @@ -19,6 +19,9 @@ Offset Scale Convenience function that returns a scaling transformation +The DecomposedTransform class implements a transformation with separate +translate, rotation, scale, skew, and transformation-center components. + :Example: >>> t = Transform(2, 0, 0, 3, 0, 0) @@ -49,10 +52,12 @@ Scale >>> """ +import math from typing import NamedTuple +from dataclasses import dataclass -__all__ = ["Transform", "Identity", "Offset", "Scale"] +__all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"] _EPSILON = 1e-15 @@ -61,338 +66,430 @@ _MINUS_ONE_EPSILON = -1 + _EPSILON def _normSinCos(v): - if abs(v) < _EPSILON: - v = 0 - elif v > _ONE_EPSILON: - v = 1 - elif v < _MINUS_ONE_EPSILON: - v = -1 - return v + if abs(v) < _EPSILON: + v = 0 + elif v > _ONE_EPSILON: + v = 1 + elif v < _MINUS_ONE_EPSILON: + v = -1 + return v class Transform(NamedTuple): - """2x2 transformation matrix plus offset, a.k.a. Affine transform. - Transform instances are immutable: all transforming methods, eg. - rotate(), return a new Transform instance. - - :Example: - - >>> t = Transform() - >>> t - <Transform [1 0 0 1 0 0]> - >>> t.scale(2) - <Transform [2 0 0 2 0 0]> - >>> t.scale(2.5, 5.5) - <Transform [2.5 0 0 5.5 0 0]> - >>> - >>> t.scale(2, 3).transformPoint((100, 100)) - (200, 300) - - Transform's constructor takes six arguments, all of which are - optional, and can be used as keyword arguments:: - - >>> Transform(12) - <Transform [12 0 0 1 0 0]> - >>> Transform(dx=12) - <Transform [1 0 0 1 12 0]> - >>> Transform(yx=12) - <Transform [1 0 12 1 0 0]> - - Transform instances also behave like sequences of length 6:: - - >>> len(Identity) - 6 - >>> list(Identity) - [1, 0, 0, 1, 0, 0] - >>> tuple(Identity) - (1, 0, 0, 1, 0, 0) - - Transform instances are comparable:: - - >>> t1 = Identity.scale(2, 3).translate(4, 6) - >>> t2 = Identity.translate(8, 18).scale(2, 3) - >>> t1 == t2 - 1 - - But beware of floating point rounding errors:: - - >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) - >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) - >>> t1 - <Transform [0.2 0 0 0.3 0.08 0.18]> - >>> t2 - <Transform [0.2 0 0 0.3 0.08 0.18]> - >>> t1 == t2 - 0 - - Transform instances are hashable, meaning you can use them as - keys in dictionaries:: - - >>> d = {Scale(12, 13): None} - >>> d - {<Transform [12 0 0 13 0 0]>: None} - - But again, beware of floating point rounding errors:: - - >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) - >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) - >>> t1 - <Transform [0.2 0 0 0.3 0.08 0.18]> - >>> t2 - <Transform [0.2 0 0 0.3 0.08 0.18]> - >>> d = {t1: None} - >>> d - {<Transform [0.2 0 0 0.3 0.08 0.18]>: None} - >>> d[t2] - Traceback (most recent call last): - File "<stdin>", line 1, in ? - KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]> - """ - - xx: float = 1 - xy: float = 0 - yx: float = 0 - yy: float = 1 - dx: float = 0 - dy: float = 0 - - def transformPoint(self, p): - """Transform a point. - - :Example: - - >>> t = Transform() - >>> t = t.scale(2.5, 5.5) - >>> t.transformPoint((100, 100)) - (250.0, 550.0) - """ - (x, y) = p - xx, xy, yx, yy, dx, dy = self - return (xx*x + yx*y + dx, xy*x + yy*y + dy) - - def transformPoints(self, points): - """Transform a list of points. - - :Example: - - >>> t = Scale(2, 3) - >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)]) - [(0, 0), (0, 300), (200, 300), (200, 0)] - >>> - """ - xx, xy, yx, yy, dx, dy = self - return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points] - - def transformVector(self, v): - """Transform an (dx, dy) vector, treating translation as zero. - - :Example: - - >>> t = Transform(2, 0, 0, 2, 10, 20) - >>> t.transformVector((3, -4)) - (6, -8) - >>> - """ - (dx, dy) = v - xx, xy, yx, yy = self[:4] - return (xx*dx + yx*dy, xy*dx + yy*dy) - - def transformVectors(self, vectors): - """Transform a list of (dx, dy) vector, treating translation as zero. - - :Example: - >>> t = Transform(2, 0, 0, 2, 10, 20) - >>> t.transformVectors([(3, -4), (5, -6)]) - [(6, -8), (10, -12)] - >>> - """ - xx, xy, yx, yy = self[:4] - return [(xx*dx + yx*dy, xy*dx + yy*dy) for dx, dy in vectors] - - def translate(self, x=0, y=0): - """Return a new transformation, translated (offset) by x, y. - - :Example: - >>> t = Transform() - >>> t.translate(20, 30) - <Transform [1 0 0 1 20 30]> - >>> - """ - return self.transform((1, 0, 0, 1, x, y)) - - def scale(self, x=1, y=None): - """Return a new transformation, scaled by x, y. The 'y' argument - may be None, which implies to use the x value for y as well. - - :Example: - >>> t = Transform() - >>> t.scale(5) - <Transform [5 0 0 5 0 0]> - >>> t.scale(5, 6) - <Transform [5 0 0 6 0 0]> - >>> - """ - if y is None: - y = x - return self.transform((x, 0, 0, y, 0, 0)) - - def rotate(self, angle): - """Return a new transformation, rotated by 'angle' (radians). - - :Example: - >>> import math - >>> t = Transform() - >>> t.rotate(math.pi / 2) - <Transform [0 1 -1 0 0 0]> - >>> - """ - import math - c = _normSinCos(math.cos(angle)) - s = _normSinCos(math.sin(angle)) - return self.transform((c, s, -s, c, 0, 0)) - - def skew(self, x=0, y=0): - """Return a new transformation, skewed by x and y. - - :Example: - >>> import math - >>> t = Transform() - >>> t.skew(math.pi / 4) - <Transform [1 0 1 1 0 0]> - >>> - """ - import math - return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0)) - - def transform(self, other): - """Return a new transformation, transformed by another - transformation. - - :Example: - >>> t = Transform(2, 0, 0, 3, 1, 6) - >>> t.transform((4, 3, 2, 1, 5, 6)) - <Transform [8 9 4 3 11 24]> - >>> - """ - xx1, xy1, yx1, yy1, dx1, dy1 = other - xx2, xy2, yx2, yy2, dx2, dy2 = self - return self.__class__( - xx1*xx2 + xy1*yx2, - xx1*xy2 + xy1*yy2, - yx1*xx2 + yy1*yx2, - yx1*xy2 + yy1*yy2, - xx2*dx1 + yx2*dy1 + dx2, - xy2*dx1 + yy2*dy1 + dy2) - - def reverseTransform(self, other): - """Return a new transformation, which is the other transformation - transformed by self. self.reverseTransform(other) is equivalent to - other.transform(self). - - :Example: - >>> t = Transform(2, 0, 0, 3, 1, 6) - >>> t.reverseTransform((4, 3, 2, 1, 5, 6)) - <Transform [8 6 6 3 21 15]> - >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6)) - <Transform [8 6 6 3 21 15]> - >>> - """ - xx1, xy1, yx1, yy1, dx1, dy1 = self - xx2, xy2, yx2, yy2, dx2, dy2 = other - return self.__class__( - xx1*xx2 + xy1*yx2, - xx1*xy2 + xy1*yy2, - yx1*xx2 + yy1*yx2, - yx1*xy2 + yy1*yy2, - xx2*dx1 + yx2*dy1 + dx2, - xy2*dx1 + yy2*dy1 + dy2) - - def inverse(self): - """Return the inverse transformation. - - :Example: - >>> t = Identity.translate(2, 3).scale(4, 5) - >>> t.transformPoint((10, 20)) - (42, 103) - >>> it = t.inverse() - >>> it.transformPoint((42, 103)) - (10.0, 20.0) - >>> - """ - if self == Identity: - return self - xx, xy, yx, yy, dx, dy = self - det = xx*yy - yx*xy - xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det - dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy - return self.__class__(xx, xy, yx, yy, dx, dy) - - def toPS(self): - """Return a PostScript representation - - :Example: - - >>> t = Identity.scale(2, 3).translate(4, 5) - >>> t.toPS() - '[2 0 0 3 8 15]' - >>> - """ - return "[%s %s %s %s %s %s]" % self - - def __bool__(self): - """Returns True if transform is not identity, False otherwise. - - :Example: - - >>> bool(Identity) - False - >>> bool(Transform()) - False - >>> bool(Scale(1.)) - False - >>> bool(Scale(2)) - True - >>> bool(Offset()) - False - >>> bool(Offset(0)) - False - >>> bool(Offset(2)) - True - """ - return self != Identity - - def __repr__(self): - return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self) + """2x2 transformation matrix plus offset, a.k.a. Affine transform. + Transform instances are immutable: all transforming methods, eg. + rotate(), return a new Transform instance. + + :Example: + + >>> t = Transform() + >>> t + <Transform [1 0 0 1 0 0]> + >>> t.scale(2) + <Transform [2 0 0 2 0 0]> + >>> t.scale(2.5, 5.5) + <Transform [2.5 0 0 5.5 0 0]> + >>> + >>> t.scale(2, 3).transformPoint((100, 100)) + (200, 300) + + Transform's constructor takes six arguments, all of which are + optional, and can be used as keyword arguments:: + + >>> Transform(12) + <Transform [12 0 0 1 0 0]> + >>> Transform(dx=12) + <Transform [1 0 0 1 12 0]> + >>> Transform(yx=12) + <Transform [1 0 12 1 0 0]> + + Transform instances also behave like sequences of length 6:: + + >>> len(Identity) + 6 + >>> list(Identity) + [1, 0, 0, 1, 0, 0] + >>> tuple(Identity) + (1, 0, 0, 1, 0, 0) + + Transform instances are comparable:: + + >>> t1 = Identity.scale(2, 3).translate(4, 6) + >>> t2 = Identity.translate(8, 18).scale(2, 3) + >>> t1 == t2 + 1 + + But beware of floating point rounding errors:: + + >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) + >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) + >>> t1 + <Transform [0.2 0 0 0.3 0.08 0.18]> + >>> t2 + <Transform [0.2 0 0 0.3 0.08 0.18]> + >>> t1 == t2 + 0 + + Transform instances are hashable, meaning you can use them as + keys in dictionaries:: + + >>> d = {Scale(12, 13): None} + >>> d + {<Transform [12 0 0 13 0 0]>: None} + + But again, beware of floating point rounding errors:: + + >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) + >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) + >>> t1 + <Transform [0.2 0 0 0.3 0.08 0.18]> + >>> t2 + <Transform [0.2 0 0 0.3 0.08 0.18]> + >>> d = {t1: None} + >>> d + {<Transform [0.2 0 0 0.3 0.08 0.18]>: None} + >>> d[t2] + Traceback (most recent call last): + File "<stdin>", line 1, in ? + KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]> + """ + + xx: float = 1 + xy: float = 0 + yx: float = 0 + yy: float = 1 + dx: float = 0 + dy: float = 0 + + def transformPoint(self, p): + """Transform a point. + + :Example: + + >>> t = Transform() + >>> t = t.scale(2.5, 5.5) + >>> t.transformPoint((100, 100)) + (250.0, 550.0) + """ + (x, y) = p + xx, xy, yx, yy, dx, dy = self + return (xx * x + yx * y + dx, xy * x + yy * y + dy) + + def transformPoints(self, points): + """Transform a list of points. + + :Example: + + >>> t = Scale(2, 3) + >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)]) + [(0, 0), (0, 300), (200, 300), (200, 0)] + >>> + """ + xx, xy, yx, yy, dx, dy = self + return [(xx * x + yx * y + dx, xy * x + yy * y + dy) for x, y in points] + + def transformVector(self, v): + """Transform an (dx, dy) vector, treating translation as zero. + + :Example: + + >>> t = Transform(2, 0, 0, 2, 10, 20) + >>> t.transformVector((3, -4)) + (6, -8) + >>> + """ + (dx, dy) = v + xx, xy, yx, yy = self[:4] + return (xx * dx + yx * dy, xy * dx + yy * dy) + + def transformVectors(self, vectors): + """Transform a list of (dx, dy) vector, treating translation as zero. + + :Example: + >>> t = Transform(2, 0, 0, 2, 10, 20) + >>> t.transformVectors([(3, -4), (5, -6)]) + [(6, -8), (10, -12)] + >>> + """ + xx, xy, yx, yy = self[:4] + return [(xx * dx + yx * dy, xy * dx + yy * dy) for dx, dy in vectors] + + def translate(self, x=0, y=0): + """Return a new transformation, translated (offset) by x, y. + + :Example: + >>> t = Transform() + >>> t.translate(20, 30) + <Transform [1 0 0 1 20 30]> + >>> + """ + return self.transform((1, 0, 0, 1, x, y)) + + def scale(self, x=1, y=None): + """Return a new transformation, scaled by x, y. The 'y' argument + may be None, which implies to use the x value for y as well. + + :Example: + >>> t = Transform() + >>> t.scale(5) + <Transform [5 0 0 5 0 0]> + >>> t.scale(5, 6) + <Transform [5 0 0 6 0 0]> + >>> + """ + if y is None: + y = x + return self.transform((x, 0, 0, y, 0, 0)) + + def rotate(self, angle): + """Return a new transformation, rotated by 'angle' (radians). + + :Example: + >>> import math + >>> t = Transform() + >>> t.rotate(math.pi / 2) + <Transform [0 1 -1 0 0 0]> + >>> + """ + import math + + c = _normSinCos(math.cos(angle)) + s = _normSinCos(math.sin(angle)) + return self.transform((c, s, -s, c, 0, 0)) + + def skew(self, x=0, y=0): + """Return a new transformation, skewed by x and y. + + :Example: + >>> import math + >>> t = Transform() + >>> t.skew(math.pi / 4) + <Transform [1 0 1 1 0 0]> + >>> + """ + import math + + return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0)) + + def transform(self, other): + """Return a new transformation, transformed by another + transformation. + + :Example: + >>> t = Transform(2, 0, 0, 3, 1, 6) + >>> t.transform((4, 3, 2, 1, 5, 6)) + <Transform [8 9 4 3 11 24]> + >>> + """ + xx1, xy1, yx1, yy1, dx1, dy1 = other + xx2, xy2, yx2, yy2, dx2, dy2 = self + return self.__class__( + xx1 * xx2 + xy1 * yx2, + xx1 * xy2 + xy1 * yy2, + yx1 * xx2 + yy1 * yx2, + yx1 * xy2 + yy1 * yy2, + xx2 * dx1 + yx2 * dy1 + dx2, + xy2 * dx1 + yy2 * dy1 + dy2, + ) + + def reverseTransform(self, other): + """Return a new transformation, which is the other transformation + transformed by self. self.reverseTransform(other) is equivalent to + other.transform(self). + + :Example: + >>> t = Transform(2, 0, 0, 3, 1, 6) + >>> t.reverseTransform((4, 3, 2, 1, 5, 6)) + <Transform [8 6 6 3 21 15]> + >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6)) + <Transform [8 6 6 3 21 15]> + >>> + """ + xx1, xy1, yx1, yy1, dx1, dy1 = self + xx2, xy2, yx2, yy2, dx2, dy2 = other + return self.__class__( + xx1 * xx2 + xy1 * yx2, + xx1 * xy2 + xy1 * yy2, + yx1 * xx2 + yy1 * yx2, + yx1 * xy2 + yy1 * yy2, + xx2 * dx1 + yx2 * dy1 + dx2, + xy2 * dx1 + yy2 * dy1 + dy2, + ) + + def inverse(self): + """Return the inverse transformation. + + :Example: + >>> t = Identity.translate(2, 3).scale(4, 5) + >>> t.transformPoint((10, 20)) + (42, 103) + >>> it = t.inverse() + >>> it.transformPoint((42, 103)) + (10.0, 20.0) + >>> + """ + if self == Identity: + return self + xx, xy, yx, yy, dx, dy = self + det = xx * yy - yx * xy + xx, xy, yx, yy = yy / det, -xy / det, -yx / det, xx / det + dx, dy = -xx * dx - yx * dy, -xy * dx - yy * dy + return self.__class__(xx, xy, yx, yy, dx, dy) + + def toPS(self): + """Return a PostScript representation + + :Example: + + >>> t = Identity.scale(2, 3).translate(4, 5) + >>> t.toPS() + '[2 0 0 3 8 15]' + >>> + """ + return "[%s %s %s %s %s %s]" % self + + def toDecomposed(self) -> "DecomposedTransform": + """Decompose into a DecomposedTransform.""" + return DecomposedTransform.fromTransform(self) + + def __bool__(self): + """Returns True if transform is not identity, False otherwise. + + :Example: + + >>> bool(Identity) + False + >>> bool(Transform()) + False + >>> bool(Scale(1.)) + False + >>> bool(Scale(2)) + True + >>> bool(Offset()) + False + >>> bool(Offset(0)) + False + >>> bool(Offset(2)) + True + """ + return self != Identity + + def __repr__(self): + return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self) Identity = Transform() + def Offset(x=0, y=0): - """Return the identity transformation offset by x, y. + """Return the identity transformation offset by x, y. - :Example: - >>> Offset(2, 3) - <Transform [1 0 0 1 2 3]> - >>> - """ - return Transform(1, 0, 0, 1, x, y) + :Example: + >>> Offset(2, 3) + <Transform [1 0 0 1 2 3]> + >>> + """ + return Transform(1, 0, 0, 1, x, y) -def Scale(x, y=None): - """Return the identity transformation scaled by x, y. The 'y' argument - may be None, which implies to use the x value for y as well. - :Example: - >>> Scale(2, 3) - <Transform [2 0 0 3 0 0]> - >>> - """ - if y is None: - y = x - return Transform(x, 0, 0, y, 0, 0) +def Scale(x, y=None): + """Return the identity transformation scaled by x, y. The 'y' argument + may be None, which implies to use the x value for y as well. + + :Example: + >>> Scale(2, 3) + <Transform [2 0 0 3 0 0]> + >>> + """ + if y is None: + y = x + return Transform(x, 0, 0, y, 0, 0) + + +@dataclass +class DecomposedTransform: + """The DecomposedTransform class implements a transformation with separate + translate, rotation, scale, skew, and transformation-center components. + """ + + translateX: float = 0 + translateY: float = 0 + rotation: float = 0 # in degrees, counter-clockwise + scaleX: float = 1 + scaleY: float = 1 + skewX: float = 0 # in degrees, clockwise + skewY: float = 0 # in degrees, counter-clockwise + tCenterX: float = 0 + tCenterY: float = 0 + + @classmethod + def fromTransform(self, transform): + # Adapted from an answer on + # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix + a, b, c, d, x, y = transform + + sx = math.copysign(1, a) + if sx < 0: + a *= sx + b *= sx + + delta = a * d - b * c + + rotation = 0 + scaleX = scaleY = 0 + skewX = skewY = 0 + + # Apply the QR-like decomposition. + if a != 0 or b != 0: + r = math.sqrt(a * a + b * b) + rotation = math.acos(a / r) if b >= 0 else -math.acos(a / r) + scaleX, scaleY = (r, delta / r) + skewX, skewY = (math.atan((a * c + b * d) / (r * r)), 0) + elif c != 0 or d != 0: + s = math.sqrt(c * c + d * d) + rotation = math.pi / 2 - ( + math.acos(-c / s) if d >= 0 else -math.acos(c / s) + ) + scaleX, scaleY = (delta / s, s) + skewX, skewY = (0, math.atan((a * c + b * d) / (s * s))) + else: + # a = b = c = d = 0 + pass + + return DecomposedTransform( + x, + y, + math.degrees(rotation), + scaleX * sx, + scaleY, + math.degrees(skewX) * sx, + math.degrees(skewY), + 0, + 0, + ) + + def toTransform(self): + """Return the Transform() equivalent of this transformation. + + :Example: + >>> DecomposedTransform(scaleX=2, scaleY=2).toTransform() + <Transform [2 0 0 2 0 0]> + >>> + """ + t = Transform() + t = t.translate( + self.translateX + self.tCenterX, self.translateY + self.tCenterY + ) + t = t.rotate(math.radians(self.rotation)) + t = t.scale(self.scaleX, self.scaleY) + t = t.skew(math.radians(self.skewX), math.radians(self.skewY)) + t = t.translate(-self.tCenterX, -self.tCenterY) + return t if __name__ == "__main__": - import sys - import doctest - sys.exit(doctest.testmod().failed) + import sys + import doctest + + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/vector.py b/Lib/fontTools/misc/vector.py index 81c14841..666ff15c 100644 --- a/Lib/fontTools/misc/vector.py +++ b/Lib/fontTools/misc/vector.py @@ -134,6 +134,11 @@ class Vector(tuple): "can't set attribute, the 'values' attribute has been deprecated", ) + def isclose(self, other: "Vector", **kwargs) -> bool: + """Return True if the vector is close to another Vector.""" + assert len(self) == len(other) + return all(math.isclose(a, b, **kwargs) for a, b in zip(self, other)) + def _operator_rsub(a, b): return operator.sub(b, a) diff --git a/Lib/fontTools/misc/visitor.py b/Lib/fontTools/misc/visitor.py index 3d28135f..d2898954 100644 --- a/Lib/fontTools/misc/visitor.py +++ b/Lib/fontTools/misc/visitor.py @@ -4,7 +4,6 @@ import enum class Visitor(object): - defaultStop = False @classmethod @@ -58,7 +57,6 @@ class Visitor(object): typ = type(thing) for celf in celf.mro(): - _visitors = getattr(celf, "_visitors", None) if _visitors is None: break diff --git a/Lib/fontTools/misc/xmlReader.py b/Lib/fontTools/misc/xmlReader.py index 6ec50de4..d8e502f1 100644 --- a/Lib/fontTools/misc/xmlReader.py +++ b/Lib/fontTools/misc/xmlReader.py @@ -8,164 +8,181 @@ import logging log = logging.getLogger(__name__) -class TTXParseError(Exception): pass + +class TTXParseError(Exception): + pass + BUFSIZE = 0x4000 class XMLReader(object): - - def __init__(self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False): - if fileOrPath == '-': - fileOrPath = sys.stdin - if not hasattr(fileOrPath, "read"): - self.file = open(fileOrPath, "rb") - self._closeStream = True - else: - # assume readable file object - self.file = fileOrPath - self._closeStream = False - self.ttFont = ttFont - self.progress = progress - if quiet is not None: - from fontTools.misc.loggingTools import deprecateArgument - deprecateArgument("quiet", "configure logging instead") - self.quiet = quiet - self.root = None - self.contentStack = [] - self.contentOnly = contentOnly - self.stackSize = 0 - - def read(self, rootless=False): - if rootless: - self.stackSize += 1 - if self.progress: - self.file.seek(0, 2) - fileSize = self.file.tell() - self.progress.set(0, fileSize // 100 or 1) - self.file.seek(0) - self._parseFile(self.file) - if self._closeStream: - self.close() - if rootless: - self.stackSize -= 1 - - def close(self): - self.file.close() - - def _parseFile(self, file): - from xml.parsers.expat import ParserCreate - parser = ParserCreate() - parser.StartElementHandler = self._startElementHandler - parser.EndElementHandler = self._endElementHandler - parser.CharacterDataHandler = self._characterDataHandler - - pos = 0 - while True: - chunk = file.read(BUFSIZE) - if not chunk: - parser.Parse(chunk, 1) - break - pos = pos + len(chunk) - if self.progress: - self.progress.set(pos // 100) - parser.Parse(chunk, 0) - - def _startElementHandler(self, name, attrs): - if self.stackSize == 1 and self.contentOnly: - # We already know the table we're parsing, skip - # parsing the table tag and continue to - # stack '2' which begins parsing content - self.contentStack.append([]) - self.stackSize = 2 - return - stackSize = self.stackSize - self.stackSize = stackSize + 1 - subFile = attrs.get("src") - if subFile is not None: - if hasattr(self.file, 'name'): - # if file has a name, get its parent directory - dirname = os.path.dirname(self.file.name) - else: - # else fall back to using the current working directory - dirname = os.getcwd() - subFile = os.path.join(dirname, subFile) - if not stackSize: - if name != "ttFont": - raise TTXParseError("illegal root tag: %s" % name) - if self.ttFont.reader is None and not self.ttFont.tables: - sfntVersion = attrs.get("sfntVersion") - if sfntVersion is not None: - if len(sfntVersion) != 4: - sfntVersion = safeEval('"' + sfntVersion + '"') - self.ttFont.sfntVersion = sfntVersion - self.contentStack.append([]) - elif stackSize == 1: - if subFile is not None: - subReader = XMLReader(subFile, self.ttFont, self.progress) - subReader.read() - self.contentStack.append([]) - return - tag = ttLib.xmlToTag(name) - msg = "Parsing '%s' table..." % tag - if self.progress: - self.progress.setLabel(msg) - log.info(msg) - if tag == "GlyphOrder": - tableClass = ttLib.GlyphOrder - elif "ERROR" in attrs or ('raw' in attrs and safeEval(attrs['raw'])): - tableClass = DefaultTable - else: - tableClass = ttLib.getTableClass(tag) - if tableClass is None: - tableClass = DefaultTable - if tag == 'loca' and tag in self.ttFont: - # Special-case the 'loca' table as we need the - # original if the 'glyf' table isn't recompiled. - self.currentTable = self.ttFont[tag] - else: - self.currentTable = tableClass(tag) - self.ttFont[tag] = self.currentTable - self.contentStack.append([]) - elif stackSize == 2 and subFile is not None: - subReader = XMLReader(subFile, self.ttFont, self.progress, contentOnly=True) - subReader.read() - self.contentStack.append([]) - self.root = subReader.root - elif stackSize == 2: - self.contentStack.append([]) - self.root = (name, attrs, self.contentStack[-1]) - else: - l = [] - self.contentStack[-1].append((name, attrs, l)) - self.contentStack.append(l) - - def _characterDataHandler(self, data): - if self.stackSize > 1: - self.contentStack[-1].append(data) - - def _endElementHandler(self, name): - self.stackSize = self.stackSize - 1 - del self.contentStack[-1] - if not self.contentOnly: - if self.stackSize == 1: - self.root = None - elif self.stackSize == 2: - name, attrs, content = self.root - self.currentTable.fromXML(name, attrs, content, self.ttFont) - self.root = None + def __init__( + self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False + ): + if fileOrPath == "-": + fileOrPath = sys.stdin + if not hasattr(fileOrPath, "read"): + self.file = open(fileOrPath, "rb") + self._closeStream = True + else: + # assume readable file object + self.file = fileOrPath + self._closeStream = False + self.ttFont = ttFont + self.progress = progress + if quiet is not None: + from fontTools.misc.loggingTools import deprecateArgument + + deprecateArgument("quiet", "configure logging instead") + self.quiet = quiet + self.root = None + self.contentStack = [] + self.contentOnly = contentOnly + self.stackSize = 0 + + def read(self, rootless=False): + if rootless: + self.stackSize += 1 + if self.progress: + self.file.seek(0, 2) + fileSize = self.file.tell() + self.progress.set(0, fileSize // 100 or 1) + self.file.seek(0) + self._parseFile(self.file) + if self._closeStream: + self.close() + if rootless: + self.stackSize -= 1 + + def close(self): + self.file.close() + + def _parseFile(self, file): + from xml.parsers.expat import ParserCreate + + parser = ParserCreate() + parser.StartElementHandler = self._startElementHandler + parser.EndElementHandler = self._endElementHandler + parser.CharacterDataHandler = self._characterDataHandler + + pos = 0 + while True: + chunk = file.read(BUFSIZE) + if not chunk: + parser.Parse(chunk, 1) + break + pos = pos + len(chunk) + if self.progress: + self.progress.set(pos // 100) + parser.Parse(chunk, 0) + + def _startElementHandler(self, name, attrs): + if self.stackSize == 1 and self.contentOnly: + # We already know the table we're parsing, skip + # parsing the table tag and continue to + # stack '2' which begins parsing content + self.contentStack.append([]) + self.stackSize = 2 + return + stackSize = self.stackSize + self.stackSize = stackSize + 1 + subFile = attrs.get("src") + if subFile is not None: + if hasattr(self.file, "name"): + # if file has a name, get its parent directory + dirname = os.path.dirname(self.file.name) + else: + # else fall back to using the current working directory + dirname = os.getcwd() + subFile = os.path.join(dirname, subFile) + if not stackSize: + if name != "ttFont": + raise TTXParseError("illegal root tag: %s" % name) + if self.ttFont.reader is None and not self.ttFont.tables: + sfntVersion = attrs.get("sfntVersion") + if sfntVersion is not None: + if len(sfntVersion) != 4: + sfntVersion = safeEval('"' + sfntVersion + '"') + self.ttFont.sfntVersion = sfntVersion + self.contentStack.append([]) + elif stackSize == 1: + if subFile is not None: + subReader = XMLReader(subFile, self.ttFont, self.progress) + subReader.read() + self.contentStack.append([]) + return + tag = ttLib.xmlToTag(name) + msg = "Parsing '%s' table..." % tag + if self.progress: + self.progress.setLabel(msg) + log.info(msg) + if tag == "GlyphOrder": + tableClass = ttLib.GlyphOrder + elif "ERROR" in attrs or ("raw" in attrs and safeEval(attrs["raw"])): + tableClass = DefaultTable + else: + tableClass = ttLib.getTableClass(tag) + if tableClass is None: + tableClass = DefaultTable + if tag == "loca" and tag in self.ttFont: + # Special-case the 'loca' table as we need the + # original if the 'glyf' table isn't recompiled. + self.currentTable = self.ttFont[tag] + else: + self.currentTable = tableClass(tag) + self.ttFont[tag] = self.currentTable + self.contentStack.append([]) + elif stackSize == 2 and subFile is not None: + subReader = XMLReader(subFile, self.ttFont, self.progress, contentOnly=True) + subReader.read() + self.contentStack.append([]) + self.root = subReader.root + elif stackSize == 2: + self.contentStack.append([]) + self.root = (name, attrs, self.contentStack[-1]) + else: + l = [] + self.contentStack[-1].append((name, attrs, l)) + self.contentStack.append(l) + + def _characterDataHandler(self, data): + if self.stackSize > 1: + # parser parses in chunks, so we may get multiple calls + # for the same text node; thus we need to append the data + # to the last item in the content stack: + # https://github.com/fonttools/fonttools/issues/2614 + if ( + data != "\n" + and self.contentStack[-1] + and isinstance(self.contentStack[-1][-1], str) + and self.contentStack[-1][-1] != "\n" + ): + self.contentStack[-1][-1] += data + else: + self.contentStack[-1].append(data) + + def _endElementHandler(self, name): + self.stackSize = self.stackSize - 1 + del self.contentStack[-1] + if not self.contentOnly: + if self.stackSize == 1: + self.root = None + elif self.stackSize == 2: + name, attrs, content = self.root + self.currentTable.fromXML(name, attrs, content, self.ttFont) + self.root = None class ProgressPrinter(object): + def __init__(self, title, maxval=100): + print(title) - def __init__(self, title, maxval=100): - print(title) - - def set(self, val, maxval=None): - pass + def set(self, val, maxval=None): + pass - def increment(self, val=1): - pass + def increment(self, val=1): + pass - def setLabel(self, text): - print(text) + def setLabel(self, text): + print(text) diff --git a/Lib/fontTools/misc/xmlWriter.py b/Lib/fontTools/misc/xmlWriter.py index 9e30fa33..9a8dc3e3 100644 --- a/Lib/fontTools/misc/xmlWriter.py +++ b/Lib/fontTools/misc/xmlWriter.py @@ -9,186 +9,196 @@ INDENT = " " class XMLWriter(object): - - def __init__(self, fileOrPath, indentwhite=INDENT, idlefunc=None, encoding="utf_8", - newlinestr="\n"): - if encoding.lower().replace('-','').replace('_','') != 'utf8': - raise Exception('Only UTF-8 encoding is supported.') - if fileOrPath == '-': - fileOrPath = sys.stdout - if not hasattr(fileOrPath, "write"): - self.filename = fileOrPath - self.file = open(fileOrPath, "wb") - self._closeStream = True - else: - self.filename = None - # assume writable file object - self.file = fileOrPath - self._closeStream = False - - # Figure out if writer expects bytes or unicodes - try: - # The bytes check should be first. See: - # https://github.com/fonttools/fonttools/pull/233 - self.file.write(b'') - self.totype = tobytes - except TypeError: - # This better not fail. - self.file.write('') - self.totype = tostr - self.indentwhite = self.totype(indentwhite) - if newlinestr is None: - self.newlinestr = self.totype(os.linesep) - else: - self.newlinestr = self.totype(newlinestr) - self.indentlevel = 0 - self.stack = [] - self.needindent = 1 - self.idlefunc = idlefunc - self.idlecounter = 0 - self._writeraw('<?xml version="1.0" encoding="UTF-8"?>') - self.newline() - - def __enter__(self): - return self - - def __exit__(self, exception_type, exception_value, traceback): - self.close() - - def close(self): - if self._closeStream: - self.file.close() - - def write(self, string, indent=True): - """Writes text.""" - self._writeraw(escape(string), indent=indent) - - def writecdata(self, string): - """Writes text in a CDATA section.""" - self._writeraw("<![CDATA[" + string + "]]>") - - def write8bit(self, data, strip=False): - """Writes a bytes() sequence into the XML, escaping - non-ASCII bytes. When this is read in xmlReader, - the original bytes can be recovered by encoding to - 'latin-1'.""" - self._writeraw(escape8bit(data), strip=strip) - - def write_noindent(self, string): - """Writes text without indentation.""" - self._writeraw(escape(string), indent=False) - - def _writeraw(self, data, indent=True, strip=False): - """Writes bytes, possibly indented.""" - if indent and self.needindent: - self.file.write(self.indentlevel * self.indentwhite) - self.needindent = 0 - s = self.totype(data, encoding="utf_8") - if (strip): - s = s.strip() - self.file.write(s) - - def newline(self): - self.file.write(self.newlinestr) - self.needindent = 1 - idlecounter = self.idlecounter - if not idlecounter % 100 and self.idlefunc is not None: - self.idlefunc() - self.idlecounter = idlecounter + 1 - - def comment(self, data): - data = escape(data) - lines = data.split("\n") - self._writeraw("<!-- " + lines[0]) - for line in lines[1:]: - self.newline() - self._writeraw(" " + line) - self._writeraw(" -->") - - def simpletag(self, _TAG_, *args, **kwargs): - attrdata = self.stringifyattrs(*args, **kwargs) - data = "<%s%s/>" % (_TAG_, attrdata) - self._writeraw(data) - - def begintag(self, _TAG_, *args, **kwargs): - attrdata = self.stringifyattrs(*args, **kwargs) - data = "<%s%s>" % (_TAG_, attrdata) - self._writeraw(data) - self.stack.append(_TAG_) - self.indent() - - def endtag(self, _TAG_): - assert self.stack and self.stack[-1] == _TAG_, "nonmatching endtag" - del self.stack[-1] - self.dedent() - data = "</%s>" % _TAG_ - self._writeraw(data) - - def dumphex(self, data): - linelength = 16 - hexlinelength = linelength * 2 - chunksize = 8 - for i in range(0, len(data), linelength): - hexline = hexStr(data[i:i+linelength]) - line = "" - white = "" - for j in range(0, hexlinelength, chunksize): - line = line + white + hexline[j:j+chunksize] - white = " " - self._writeraw(line) - self.newline() - - def indent(self): - self.indentlevel = self.indentlevel + 1 - - def dedent(self): - assert self.indentlevel > 0 - self.indentlevel = self.indentlevel - 1 - - def stringifyattrs(self, *args, **kwargs): - if kwargs: - assert not args - attributes = sorted(kwargs.items()) - elif args: - assert len(args) == 1 - attributes = args[0] - else: - return "" - data = "" - for attr, value in attributes: - if not isinstance(value, (bytes, str)): - value = str(value) - data = data + ' %s="%s"' % (attr, escapeattr(value)) - return data + def __init__( + self, + fileOrPath, + indentwhite=INDENT, + idlefunc=None, + encoding="utf_8", + newlinestr="\n", + ): + if encoding.lower().replace("-", "").replace("_", "") != "utf8": + raise Exception("Only UTF-8 encoding is supported.") + if fileOrPath == "-": + fileOrPath = sys.stdout + if not hasattr(fileOrPath, "write"): + self.filename = fileOrPath + self.file = open(fileOrPath, "wb") + self._closeStream = True + else: + self.filename = None + # assume writable file object + self.file = fileOrPath + self._closeStream = False + + # Figure out if writer expects bytes or unicodes + try: + # The bytes check should be first. See: + # https://github.com/fonttools/fonttools/pull/233 + self.file.write(b"") + self.totype = tobytes + except TypeError: + # This better not fail. + self.file.write("") + self.totype = tostr + self.indentwhite = self.totype(indentwhite) + if newlinestr is None: + self.newlinestr = self.totype(os.linesep) + else: + self.newlinestr = self.totype(newlinestr) + self.indentlevel = 0 + self.stack = [] + self.needindent = 1 + self.idlefunc = idlefunc + self.idlecounter = 0 + self._writeraw('<?xml version="1.0" encoding="UTF-8"?>') + self.newline() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.close() + + def close(self): + if self._closeStream: + self.file.close() + + def write(self, string, indent=True): + """Writes text.""" + self._writeraw(escape(string), indent=indent) + + def writecdata(self, string): + """Writes text in a CDATA section.""" + self._writeraw("<![CDATA[" + string + "]]>") + + def write8bit(self, data, strip=False): + """Writes a bytes() sequence into the XML, escaping + non-ASCII bytes. When this is read in xmlReader, + the original bytes can be recovered by encoding to + 'latin-1'.""" + self._writeraw(escape8bit(data), strip=strip) + + def write_noindent(self, string): + """Writes text without indentation.""" + self._writeraw(escape(string), indent=False) + + def _writeraw(self, data, indent=True, strip=False): + """Writes bytes, possibly indented.""" + if indent and self.needindent: + self.file.write(self.indentlevel * self.indentwhite) + self.needindent = 0 + s = self.totype(data, encoding="utf_8") + if strip: + s = s.strip() + self.file.write(s) + + def newline(self): + self.file.write(self.newlinestr) + self.needindent = 1 + idlecounter = self.idlecounter + if not idlecounter % 100 and self.idlefunc is not None: + self.idlefunc() + self.idlecounter = idlecounter + 1 + + def comment(self, data): + data = escape(data) + lines = data.split("\n") + self._writeraw("<!-- " + lines[0]) + for line in lines[1:]: + self.newline() + self._writeraw(" " + line) + self._writeraw(" -->") + + def simpletag(self, _TAG_, *args, **kwargs): + attrdata = self.stringifyattrs(*args, **kwargs) + data = "<%s%s/>" % (_TAG_, attrdata) + self._writeraw(data) + + def begintag(self, _TAG_, *args, **kwargs): + attrdata = self.stringifyattrs(*args, **kwargs) + data = "<%s%s>" % (_TAG_, attrdata) + self._writeraw(data) + self.stack.append(_TAG_) + self.indent() + + def endtag(self, _TAG_): + assert self.stack and self.stack[-1] == _TAG_, "nonmatching endtag" + del self.stack[-1] + self.dedent() + data = "</%s>" % _TAG_ + self._writeraw(data) + + def dumphex(self, data): + linelength = 16 + hexlinelength = linelength * 2 + chunksize = 8 + for i in range(0, len(data), linelength): + hexline = hexStr(data[i : i + linelength]) + line = "" + white = "" + for j in range(0, hexlinelength, chunksize): + line = line + white + hexline[j : j + chunksize] + white = " " + self._writeraw(line) + self.newline() + + def indent(self): + self.indentlevel = self.indentlevel + 1 + + def dedent(self): + assert self.indentlevel > 0 + self.indentlevel = self.indentlevel - 1 + + def stringifyattrs(self, *args, **kwargs): + if kwargs: + assert not args + attributes = sorted(kwargs.items()) + elif args: + assert len(args) == 1 + attributes = args[0] + else: + return "" + data = "" + for attr, value in attributes: + if not isinstance(value, (bytes, str)): + value = str(value) + data = data + ' %s="%s"' % (attr, escapeattr(value)) + return data def escape(data): - data = tostr(data, 'utf_8') - data = data.replace("&", "&") - data = data.replace("<", "<") - data = data.replace(">", ">") - data = data.replace("\r", " ") - return data + data = tostr(data, "utf_8") + data = data.replace("&", "&") + data = data.replace("<", "<") + data = data.replace(">", ">") + data = data.replace("\r", " ") + return data + def escapeattr(data): - data = escape(data) - data = data.replace('"', """) - return data + data = escape(data) + data = data.replace('"', """) + return data + def escape8bit(data): - """Input is Unicode string.""" - def escapechar(c): - n = ord(c) - if 32 <= n <= 127 and c not in "<&>": - return c - else: - return "&#" + repr(n) + ";" - return strjoin(map(escapechar, data.decode('latin-1'))) + """Input is Unicode string.""" + + def escapechar(c): + n = ord(c) + if 32 <= n <= 127 and c not in "<&>": + return c + else: + return "&#" + repr(n) + ";" + + return strjoin(map(escapechar, data.decode("latin-1"))) + def hexStr(s): - h = string.hexdigits - r = '' - for c in s: - i = byteord(c) - r = r + h[(i >> 4) & 0xF] + h[i & 0xF] - return r + h = string.hexdigits + r = "" + for c in s: + i = byteord(c) + r = r + h[(i >> 4) & 0xF] + h[i & 0xF] + return r |