aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/misc
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/misc')
-rw-r--r--Lib/fontTools/misc/arrayTools.py65
-rw-r--r--Lib/fontTools/misc/bezierTools.py269
-rw-r--r--Lib/fontTools/misc/classifyTools.py318
-rw-r--r--Lib/fontTools/misc/cliTools.py7
-rw-r--r--Lib/fontTools/misc/cython.py2
-rw-r--r--Lib/fontTools/misc/dictTools.py56
-rw-r--r--Lib/fontTools/misc/eexec.py166
-rw-r--r--Lib/fontTools/misc/encodingTools.py123
-rw-r--r--Lib/fontTools/misc/etree.py7
-rw-r--r--Lib/fontTools/misc/filenames.py412
-rw-r--r--Lib/fontTools/misc/fixedTools.py320
-rw-r--r--Lib/fontTools/misc/intTools.py4
-rw-r--r--Lib/fontTools/misc/loggingTools.py989
-rw-r--r--Lib/fontTools/misc/macCreatorType.py86
-rw-r--r--Lib/fontTools/misc/macRes.py410
-rw-r--r--Lib/fontTools/misc/plistlib/__init__.py10
-rw-r--r--Lib/fontTools/misc/psCharStrings.py2576
-rw-r--r--Lib/fontTools/misc/psLib.py687
-rw-r--r--Lib/fontTools/misc/psOperators.py1001
-rw-r--r--Lib/fontTools/misc/roundTools.py57
-rw-r--r--Lib/fontTools/misc/sstruct.py228
-rw-r--r--Lib/fontTools/misc/symfont.py275
-rw-r--r--Lib/fontTools/misc/testTools.py21
-rw-r--r--Lib/fontTools/misc/textTools.py135
-rw-r--r--Lib/fontTools/misc/timeTools.py102
-rw-r--r--Lib/fontTools/misc/transform.py739
-rw-r--r--Lib/fontTools/misc/vector.py5
-rw-r--r--Lib/fontTools/misc/visitor.py2
-rw-r--r--Lib/fontTools/misc/xmlReader.py317
-rw-r--r--Lib/fontTools/misc/xmlWriter.py358
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("&", "&amp;")
- data = data.replace("<", "&lt;")
- data = data.replace(">", "&gt;")
- data = data.replace("\r", "&#13;")
- return data
+ data = tostr(data, "utf_8")
+ data = data.replace("&", "&amp;")
+ data = data.replace("<", "&lt;")
+ data = data.replace(">", "&gt;")
+ data = data.replace("\r", "&#13;")
+ return data
+
def escapeattr(data):
- data = escape(data)
- data = data.replace('"', "&quot;")
- return data
+ data = escape(data)
+ data = data.replace('"', "&quot;")
+ 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