venv wird nicht mehr synchronisiert.
This commit is contained in:
@@ -1,96 +0,0 @@
|
||||
"""Python 2/3 compat layer leftovers."""
|
||||
|
||||
import decimal as _decimal
|
||||
import math as _math
|
||||
import warnings
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from io import BytesIO
|
||||
from io import StringIO as UnicodeIO
|
||||
from types import SimpleNamespace
|
||||
|
||||
from .textTools import Tag, bytechr, byteord, bytesjoin, strjoin, tobytes, tostr
|
||||
|
||||
warnings.warn(
|
||||
"The py23 module has been deprecated and will be removed in a future release. "
|
||||
"Please update your code.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"basestring",
|
||||
"bytechr",
|
||||
"byteord",
|
||||
"BytesIO",
|
||||
"bytesjoin",
|
||||
"open",
|
||||
"Py23Error",
|
||||
"range",
|
||||
"RecursionError",
|
||||
"round",
|
||||
"SimpleNamespace",
|
||||
"StringIO",
|
||||
"strjoin",
|
||||
"Tag",
|
||||
"tobytes",
|
||||
"tostr",
|
||||
"tounicode",
|
||||
"unichr",
|
||||
"unicode",
|
||||
"UnicodeIO",
|
||||
"xrange",
|
||||
"zip",
|
||||
]
|
||||
|
||||
|
||||
class Py23Error(NotImplementedError):
|
||||
pass
|
||||
|
||||
|
||||
RecursionError = RecursionError
|
||||
StringIO = UnicodeIO
|
||||
|
||||
basestring = str
|
||||
isclose = _math.isclose
|
||||
isfinite = _math.isfinite
|
||||
open = open
|
||||
range = range
|
||||
round = round3 = round
|
||||
unichr = chr
|
||||
unicode = str
|
||||
zip = zip
|
||||
|
||||
tounicode = tostr
|
||||
|
||||
|
||||
def xrange(*args, **kwargs):
|
||||
raise Py23Error("'xrange' is not defined. Use 'range' instead.")
|
||||
|
||||
|
||||
def round2(number, ndigits=None):
|
||||
"""
|
||||
Implementation of Python 2 built-in round() function.
|
||||
Rounds a number to a given precision in decimal digits (default
|
||||
0 digits). The result is a floating point number. Values are rounded
|
||||
to the closest multiple of 10 to the power minus ndigits; if two
|
||||
multiples are equally close, rounding is done away from 0.
|
||||
ndigits may be negative.
|
||||
See Python 2 documentation:
|
||||
https://docs.python.org/2/library/functions.html?highlight=round#round
|
||||
"""
|
||||
if ndigits is None:
|
||||
ndigits = 0
|
||||
|
||||
if ndigits < 0:
|
||||
exponent = 10 ** (-ndigits)
|
||||
quotient, remainder = divmod(number, exponent)
|
||||
if remainder >= exponent // 2 and number >= 0:
|
||||
quotient += 1
|
||||
return float(quotient * exponent)
|
||||
else:
|
||||
exponent = _decimal.Decimal("10") ** (-ndigits)
|
||||
|
||||
d = _decimal.Decimal.from_float(number).quantize(
|
||||
exponent, rounding=_decimal.ROUND_HALF_UP
|
||||
)
|
||||
|
||||
return float(d)
|
||||
@@ -1,110 +0,0 @@
|
||||
"""
|
||||
Various round-to-integer helpers.
|
||||
"""
|
||||
|
||||
import math
|
||||
import functools
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"noRound",
|
||||
"otRound",
|
||||
"maybeRound",
|
||||
"roundFunc",
|
||||
"nearestMultipleShortestRepr",
|
||||
]
|
||||
|
||||
|
||||
def noRound(value):
|
||||
return value
|
||||
|
||||
|
||||
def otRound(value):
|
||||
"""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:
|
||||
|
||||
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.
|
||||
|
||||
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))
|
||||
|
||||
|
||||
def maybeRound(v, tolerance, round=otRound):
|
||||
rounded = round(v)
|
||||
return rounded if abs(rounded - v) <= tolerance else v
|
||||
|
||||
|
||||
def roundFunc(tolerance, round=otRound):
|
||||
if tolerance < 0:
|
||||
raise ValueError("Rounding tolerance must be positive")
|
||||
|
||||
if tolerance == 0:
|
||||
return noRound
|
||||
|
||||
if tolerance >= 0.5:
|
||||
return round
|
||||
|
||||
return functools.partial(maybeRound, tolerance=tolerance, round=round)
|
||||
|
||||
|
||||
def nearestMultipleShortestRepr(value: float, factor: float) -> str:
|
||||
"""Round to nearest multiple of factor and return shortest decimal representation.
|
||||
|
||||
This chooses the float that is closer to a multiple of the given factor while
|
||||
having the shortest decimal representation (the least number of fractional decimal
|
||||
digits).
|
||||
|
||||
For example, given the following:
|
||||
|
||||
>>> nearestMultipleShortestRepr(-0.61883544921875, 1.0/(1<<14))
|
||||
'-0.61884'
|
||||
|
||||
Useful when you need to serialize or print a fixed-point number (or multiples
|
||||
thereof, such as F2Dot14 fractions of 180 degrees in COLRv1 PaintRotate) in
|
||||
a human-readable form.
|
||||
|
||||
Args:
|
||||
value (value): The value to be rounded and serialized.
|
||||
factor (float): The value which the result is a close multiple of.
|
||||
|
||||
Returns:
|
||||
str: A compact string representation of the value.
|
||||
"""
|
||||
if not value:
|
||||
return "0.0"
|
||||
|
||||
value = otRound(value / factor) * factor
|
||||
eps = 0.5 * factor
|
||||
lo = value - eps
|
||||
hi = value + eps
|
||||
# If the range of valid choices spans an integer, return the integer.
|
||||
if int(lo) != int(hi):
|
||||
return str(float(round(value)))
|
||||
|
||||
fmt = "%.8f"
|
||||
lo = fmt % lo
|
||||
hi = fmt % hi
|
||||
assert len(lo) == len(hi) and lo != hi
|
||||
for i in range(len(lo)):
|
||||
if lo[i] != hi[i]:
|
||||
break
|
||||
period = lo.find(".")
|
||||
assert period < i
|
||||
fmt = "%%.%df" % (i - period)
|
||||
return fmt % value
|
||||
@@ -1,220 +0,0 @@
|
||||
"""sstruct.py -- SuperStruct
|
||||
|
||||
Higher level layer on top of the struct module, enabling to
|
||||
bind names to struct elements. The interface is similar to
|
||||
struct, except the objects passed and returned are not tuples
|
||||
(or argument lists), but dictionaries or instances.
|
||||
|
||||
Just like struct, we use fmt strings to describe a data
|
||||
structure, except we use one line per element. Lines are
|
||||
separated by newlines or semi-colons. Each line contains
|
||||
either one of the special struct characters ('@', '=', '<',
|
||||
'>' or '!') or a 'name:formatchar' combo (eg. 'myFloat:f').
|
||||
Repetitions, like the struct module offers them are not useful
|
||||
in this context, except for fixed length strings (eg. 'myInt:5h'
|
||||
is not allowed but 'myString:5s' is). The 'x' fmt character
|
||||
(pad byte) is treated as 'special', since it is by definition
|
||||
anonymous. Extra whitespace is allowed everywhere.
|
||||
|
||||
The sstruct module offers one feature that the "normal" struct
|
||||
module doesn't: support for fixed point numbers. These are spelled
|
||||
as "n.mF", where n is the number of bits before the point, and m
|
||||
the number of bits after the point. Fixed point numbers get
|
||||
converted to floats.
|
||||
|
||||
pack(fmt, object):
|
||||
'object' is either a dictionary or an instance (or actually
|
||||
anything that has a __dict__ attribute). If it is a dictionary,
|
||||
its keys are used for names. If it is an instance, it's
|
||||
attributes are used to grab struct elements from. Returns
|
||||
a string containing the data.
|
||||
|
||||
unpack(fmt, data, object=None)
|
||||
If 'object' is omitted (or None), a new dictionary will be
|
||||
returned. If 'object' is a dictionary, it will be used to add
|
||||
struct elements to. If it is an instance (or in fact anything
|
||||
that has a __dict__ attribute), an attribute will be added for
|
||||
each struct element. In the latter two cases, 'object' itself
|
||||
is returned.
|
||||
|
||||
unpack2(fmt, data, object=None)
|
||||
Convenience function. Same as unpack, except data may be longer
|
||||
than needed. The returned value is a tuple: (object, leftoverdata).
|
||||
|
||||
calcsize(fmt)
|
||||
like struct.calcsize(), but uses our own fmt strings:
|
||||
it returns the size of the data in bytes.
|
||||
"""
|
||||
|
||||
from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi
|
||||
from fontTools.misc.textTools import tobytes, tostr
|
||||
import struct
|
||||
import re
|
||||
|
||||
__version__ = "1.2"
|
||||
__copyright__ = "Copyright 1998, Just van Rossum <just@letterror.com>"
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def unpack2(fmt, data, obj=None):
|
||||
length = calcsize(fmt)
|
||||
return unpack(fmt, data[:length], obj), data[length:]
|
||||
|
||||
|
||||
def calcsize(fmt):
|
||||
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
|
||||
)
|
||||
|
||||
# matches the special struct fmt chars and 'x' (pad byte)
|
||||
_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"}
|
||||
|
||||
_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
|
||||
|
||||
|
||||
def _test():
|
||||
fmt = """
|
||||
# comments are allowed
|
||||
> # big endian (see documentation for struct)
|
||||
# empty lines are allowed:
|
||||
|
||||
ashort: h
|
||||
along: l
|
||||
abyte: b # a byte
|
||||
achar: c
|
||||
astr: 5s
|
||||
afloat: f; adouble: d # multiple "statements" are allowed
|
||||
afixed: 16.16F
|
||||
abool: ?
|
||||
apad: x
|
||||
"""
|
||||
|
||||
print("size:", calcsize(fmt))
|
||||
|
||||
class foo(object):
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
@@ -1,248 +0,0 @@
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from functools import partial
|
||||
from itertools import count
|
||||
import sympy as sp
|
||||
import sys
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
BezierCurveC = tuple(
|
||||
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
|
||||
|
||||
|
||||
class _BezierFuncsLazy(dict):
|
||||
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)
|
||||
|
||||
|
||||
class GreenPen(BasePen):
|
||||
_BezierFuncs = {}
|
||||
|
||||
@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 _moveTo(self, p0):
|
||||
self.__startPoint = p0
|
||||
|
||||
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 _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 _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.
|
||||
# Use fontTools.pens.momentsPen.MomentsPen instead.
|
||||
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)
|
||||
|
||||
|
||||
def printGreenPen(penName, funcs, file=sys.stdout, docstring=None):
|
||||
if docstring is not None:
|
||||
print('"""%s"""' % docstring)
|
||||
|
||||
print(
|
||||
"""from fontTools.pens.basePen import BasePen, OpenContourError
|
||||
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
|
||||
|
||||
|
||||
__all__ = ["%s"]
|
||||
|
||||
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(
|
||||
"""
|
||||
def _moveTo(self, p0):
|
||||
self.__startPoint = p0
|
||||
|
||||
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 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(
|
||||
"""\
|
||||
@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(
|
||||
"""\
|
||||
@cython.locals(x0=cython.double, y0=cython.double)
|
||||
@cython.locals(x1=cython.double, y1=cython.double)
|
||||
@cython.locals(x2=cython.double, y2=cython.double)
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
x0,y0 = self._getCurrentPoint()
|
||||
x1,y1 = p1
|
||||
x2,y2 = p2
|
||||
""",
|
||||
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)
|
||||
@cython.locals(x3=cython.double, y3=cython.double)
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
x0,y0 = self._getCurrentPoint()
|
||||
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(
|
||||
"""
|
||||
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)
|
||||
@@ -1,229 +0,0 @@
|
||||
"""Helpers for writing unit tests."""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from io import BytesIO
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from unittest import TestCase as _TestCase
|
||||
from fontTools.config import Config
|
||||
from fontTools.misc.textTools import tobytes
|
||||
from fontTools.misc.xmlWriter import XMLWriter
|
||||
|
||||
|
||||
def parseXML(xmlSnippet):
|
||||
"""Parses a snippet of XML.
|
||||
|
||||
Input can be either a single string (unicode or UTF-8 bytes), or a
|
||||
a sequence of strings.
|
||||
|
||||
The result is in the same format that would be returned by
|
||||
XMLReader, but the parser imposes no constraints on the root
|
||||
element so it can be called on small snippets of TTX files.
|
||||
"""
|
||||
# To support snippets with multiple elements, we add a fake root.
|
||||
reader = TestXMLReader_()
|
||||
xml = b"<root>"
|
||||
if isinstance(xmlSnippet, bytes):
|
||||
xml += xmlSnippet
|
||||
elif isinstance(xmlSnippet, str):
|
||||
xml += tobytes(xmlSnippet, "utf-8")
|
||||
elif isinstance(xmlSnippet, Iterable):
|
||||
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__
|
||||
)
|
||||
xml += b"</root>"
|
||||
reader.parser.Parse(xml, 0)
|
||||
return reader.root[2]
|
||||
|
||||
|
||||
def parseXmlInto(font, parseInto, xmlSnippet):
|
||||
parsed_xml = [e for e in parseXML(xmlSnippet.strip()) if not isinstance(e, str)]
|
||||
for name, attrs, content in parsed_xml:
|
||||
parseInto.fromXML(name, attrs, content, font)
|
||||
parseInto.populateDefaults()
|
||||
return parseInto
|
||||
|
||||
|
||||
class FakeFont:
|
||||
def __init__(self, glyphs):
|
||||
self.glyphOrder_ = glyphs
|
||||
self.reverseGlyphOrderDict_ = {g: i for i, g in enumerate(glyphs)}
|
||||
self.lazy = False
|
||||
self.tables = {}
|
||||
self.cfg = Config()
|
||||
|
||||
def __getitem__(self, tag):
|
||||
return self.tables[tag]
|
||||
|
||||
def __setitem__(self, tag, table):
|
||||
self.tables[tag] = table
|
||||
|
||||
def get(self, tag, default=None):
|
||||
return self.tables.get(tag, default)
|
||||
|
||||
def getGlyphID(self, name):
|
||||
return self.reverseGlyphOrderDict_[name]
|
||||
|
||||
def getGlyphIDMany(self, lst):
|
||||
return [self.getGlyphID(gid) for gid in lst]
|
||||
|
||||
def getGlyphName(self, glyphID):
|
||||
if glyphID < len(self.glyphOrder_):
|
||||
return self.glyphOrder_[glyphID]
|
||||
else:
|
||||
return "glyph%.5d" % glyphID
|
||||
|
||||
def getGlyphNameMany(self, lst):
|
||||
return [self.getGlyphName(gid) for gid in lst]
|
||||
|
||||
def getGlyphOrder(self):
|
||||
return self.glyphOrder_
|
||||
|
||||
def getReverseGlyphMap(self):
|
||||
return self.reverseGlyphOrderDict_
|
||||
|
||||
def getGlyphNames(self):
|
||||
return sorted(self.getGlyphOrder())
|
||||
|
||||
|
||||
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_
|
||||
self.parser.CharacterDataHandler = self.addCharacterData_
|
||||
self.root = None
|
||||
self.stack = []
|
||||
|
||||
def startElement_(self, name, attrs):
|
||||
element = (name, attrs, [])
|
||||
if self.stack:
|
||||
self.stack[-1][2].append(element)
|
||||
else:
|
||||
self.root = element
|
||||
self.stack.append(element)
|
||||
|
||||
def endElement_(self, name):
|
||||
self.stack.pop()
|
||||
|
||||
def addCharacterData_(self, data):
|
||||
self.stack[-1][2].append(data)
|
||||
|
||||
|
||||
def makeXMLWriter(newlinestr="\n"):
|
||||
# don't write OS-specific new lines
|
||||
writer = XMLWriter(BytesIO(), newlinestr=newlinestr)
|
||||
# erase XML declaration
|
||||
writer.file.seek(0)
|
||||
writer.file.truncate()
|
||||
return writer
|
||||
|
||||
|
||||
def getXML(func, ttFont=None):
|
||||
"""Call the passed toXML function and return the written content as a
|
||||
list of lines (unicode strings).
|
||||
Result is stripped of XML declaration and OS-specific newline characters.
|
||||
"""
|
||||
writer = makeXMLWriter()
|
||||
func(writer, ttFont)
|
||||
xml = writer.file.getvalue().decode("utf-8")
|
||||
# toXML methods must always end with a writer.newline()
|
||||
assert xml.endswith("\n")
|
||||
return xml.splitlines()
|
||||
|
||||
|
||||
def stripVariableItemsFromTTX(
|
||||
string: str,
|
||||
ttLibVersion: bool = True,
|
||||
checkSumAdjustment: bool = True,
|
||||
modified: bool = True,
|
||||
created: bool = True,
|
||||
sfntVersion: bool = False, # opt-in only
|
||||
) -> str:
|
||||
"""Strip stuff like ttLibVersion, checksums, timestamps, etc. from TTX dumps."""
|
||||
# ttlib changes with the fontTools version
|
||||
if ttLibVersion:
|
||||
string = re.sub(' ttLibVersion="[^"]+"', "", string)
|
||||
# sometimes (e.g. some subsetter tests) we don't care whether it's OTF or TTF
|
||||
if sfntVersion:
|
||||
string = re.sub(' sfntVersion="[^"]+"', "", string)
|
||||
# head table checksum and creation and mod date changes with each save.
|
||||
if checkSumAdjustment:
|
||||
string = re.sub('<checkSumAdjustment value="[^"]+"/>', "", string)
|
||||
if modified:
|
||||
string = re.sub('<modified value="[^"]+"/>', "", string)
|
||||
if created:
|
||||
string = re.sub('<created value="[^"]+"/>', "", string)
|
||||
return string
|
||||
|
||||
|
||||
class MockFont(object):
|
||||
"""A font-like object that automatically adds any looked up glyphname
|
||||
to its glyphOrder."""
|
||||
|
||||
def __init__(self):
|
||||
self._glyphOrder = [".notdef"]
|
||||
|
||||
class AllocatingDict(dict):
|
||||
def __missing__(reverseDict, key):
|
||||
self._glyphOrder.append(key)
|
||||
gid = len(reverseDict)
|
||||
reverseDict[key] = gid
|
||||
return gid
|
||||
|
||||
self._reverseGlyphOrder = AllocatingDict({".notdef": 0})
|
||||
self.lazy = False
|
||||
|
||||
def getGlyphID(self, glyph):
|
||||
gid = self._reverseGlyphOrder[glyph]
|
||||
return gid
|
||||
|
||||
def getReverseGlyphMap(self):
|
||||
return self._reverseGlyphOrder
|
||||
|
||||
def getGlyphName(self, gid):
|
||||
return self._glyphOrder[gid]
|
||||
|
||||
def getGlyphOrder(self):
|
||||
return self._glyphOrder
|
||||
|
||||
|
||||
class TestCase(_TestCase):
|
||||
def __init__(self, methodName):
|
||||
_TestCase.__init__(self, methodName)
|
||||
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
|
||||
# and fires deprecation warnings if a program uses the old name.
|
||||
if not hasattr(self, "assertRaisesRegex"):
|
||||
self.assertRaisesRegex = self.assertRaisesRegexp
|
||||
|
||||
|
||||
class DataFilesHandler(TestCase):
|
||||
def setUp(self):
|
||||
self.tempdir = None
|
||||
self.num_tempfiles = 0
|
||||
|
||||
def tearDown(self):
|
||||
if self.tempdir:
|
||||
shutil.rmtree(self.tempdir)
|
||||
|
||||
def getpath(self, testfile):
|
||||
folder = os.path.dirname(sys.modules[self.__module__].__file__)
|
||||
return os.path.join(folder, "data", testfile)
|
||||
|
||||
def temp_dir(self):
|
||||
if not self.tempdir:
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
|
||||
def temp_font(self, font_path, file_name):
|
||||
self.temp_dir()
|
||||
temppath = os.path.join(self.tempdir, file_name)
|
||||
shutil.copy2(font_path, temppath)
|
||||
return temppath
|
||||
@@ -1,154 +0,0 @@
|
||||
"""fontTools.misc.textTools.py -- miscellaneous routines."""
|
||||
|
||||
import ast
|
||||
import string
|
||||
|
||||
|
||||
# alias kept for backward compatibility
|
||||
safeEval = ast.literal_eval
|
||||
|
||||
|
||||
class Tag(str):
|
||||
@staticmethod
|
||||
def transcode(blob):
|
||||
if isinstance(blob, bytes):
|
||||
blob = blob.decode("latin-1")
|
||||
return blob
|
||||
|
||||
def __new__(self, content):
|
||||
return str.__new__(self, self.transcode(content))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __eq__(self, other):
|
||||
return str.__eq__(self, self.transcode(other))
|
||||
|
||||
def __hash__(self):
|
||||
return str.__hash__(self)
|
||||
|
||||
def tobytes(self):
|
||||
return self.encode("latin-1")
|
||||
|
||||
|
||||
def readHex(content):
|
||||
"""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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def binary2num(bin):
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
def tostr(s, encoding="ascii", errors="strict"):
|
||||
if not isinstance(s, str):
|
||||
return s.decode(encoding, errors)
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
def tobytes(s, encoding="ascii", errors="strict"):
|
||||
if isinstance(s, str):
|
||||
return s.encode(encoding, errors)
|
||||
else:
|
||||
return bytes(s)
|
||||
|
||||
|
||||
def bytechr(n):
|
||||
return bytes([n])
|
||||
|
||||
|
||||
def byteord(c):
|
||||
return c if isinstance(c, int) else ord(c)
|
||||
|
||||
|
||||
def strjoin(iterable, joiner=""):
|
||||
return tostr(joiner).join(iterable)
|
||||
|
||||
|
||||
def bytesjoin(iterable, joiner=b""):
|
||||
return tobytes(joiner).join(tobytes(item) for item in iterable)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest, sys
|
||||
|
||||
sys.exit(doctest.testmod().failed)
|
||||
@@ -1,88 +0,0 @@
|
||||
"""fontTools.misc.timeTools.py -- tools for working with OpenType timestamps.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
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",
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def timestampToString(value):
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def timestampSinceEpoch(value):
|
||||
return int(value - epoch_diff)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import doctest
|
||||
|
||||
sys.exit(doctest.testmod().failed)
|
||||
@@ -1,494 +0,0 @@
|
||||
"""Affine 2D transformation matrix class.
|
||||
|
||||
The Transform class implements various transformation matrix operations,
|
||||
both on the matrix itself, as well as on 2D coordinates.
|
||||
|
||||
Transform instances are effectively immutable: all methods that operate on the
|
||||
transformation itself always return a new instance. This has as the
|
||||
interesting side effect that Transform instances are hashable, ie. they can be
|
||||
used as dictionary keys.
|
||||
|
||||
This module exports the following symbols:
|
||||
|
||||
Transform
|
||||
this is the main class
|
||||
Identity
|
||||
Transform instance set to the identity transformation
|
||||
Offset
|
||||
Convenience function that returns a translating transformation
|
||||
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)
|
||||
>>> t.transformPoint((100, 100))
|
||||
(200, 300)
|
||||
>>> t = Scale(2, 3)
|
||||
>>> t.transformPoint((100, 100))
|
||||
(200, 300)
|
||||
>>> t.transformPoint((0, 0))
|
||||
(0, 0)
|
||||
>>> t = Offset(2, 3)
|
||||
>>> t.transformPoint((100, 100))
|
||||
(102, 103)
|
||||
>>> t.transformPoint((0, 0))
|
||||
(2, 3)
|
||||
>>> t2 = t.scale(0.5)
|
||||
>>> t2.transformPoint((100, 100))
|
||||
(52.0, 53.0)
|
||||
>>> import math
|
||||
>>> t3 = t2.rotate(math.pi / 2)
|
||||
>>> t3.transformPoint((0, 0))
|
||||
(2.0, 3.0)
|
||||
>>> t3.transformPoint((100, 100))
|
||||
(-48.0, 53.0)
|
||||
>>> t = Identity.scale(0.5).translate(100, 200).skew(0.1, 0.2)
|
||||
>>> t.transformPoints([(0, 0), (1, 1), (100, 100)])
|
||||
[(50.0, 100.0), (50.550167336042726, 100.60135501775433), (105.01673360427253, 160.13550177543362)]
|
||||
>>>
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import NamedTuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
__all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"]
|
||||
|
||||
|
||||
_EPSILON = 1e-15
|
||||
_ONE_EPSILON = 1 - _EPSILON
|
||||
_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
|
||||
|
||||
|
||||
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 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.
|
||||
|
||||
: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)
|
||||
|
||||
|
||||
@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)
|
||||
@@ -1,45 +0,0 @@
|
||||
"""Generic tools for working with trees."""
|
||||
|
||||
from math import ceil, log
|
||||
|
||||
|
||||
def build_n_ary_tree(leaves, n):
|
||||
"""Build N-ary tree from sequence of leaf nodes.
|
||||
|
||||
Return a list of lists where each non-leaf node is a list containing
|
||||
max n nodes.
|
||||
"""
|
||||
if not leaves:
|
||||
return []
|
||||
|
||||
assert n > 1
|
||||
|
||||
depth = ceil(log(len(leaves), n))
|
||||
|
||||
if depth <= 1:
|
||||
return list(leaves)
|
||||
|
||||
# Fully populate complete subtrees of root until we have enough leaves left
|
||||
root = []
|
||||
unassigned = None
|
||||
full_step = n ** (depth - 1)
|
||||
for i in range(0, len(leaves), full_step):
|
||||
subtree = leaves[i : i + full_step]
|
||||
if len(subtree) < full_step:
|
||||
unassigned = subtree
|
||||
break
|
||||
while len(subtree) > n:
|
||||
subtree = [subtree[k : k + n] for k in range(0, len(subtree), n)]
|
||||
root.append(subtree)
|
||||
|
||||
if unassigned:
|
||||
# Recurse to fill the last subtree, which is the only partially populated one
|
||||
subtree = build_n_ary_tree(unassigned, n)
|
||||
if len(subtree) <= n - len(root):
|
||||
# replace last subtree with its children if they can still fit
|
||||
root.extend(subtree)
|
||||
else:
|
||||
root.append(subtree)
|
||||
assert len(root) <= n
|
||||
|
||||
return root
|
||||
@@ -1,147 +0,0 @@
|
||||
from numbers import Number
|
||||
import math
|
||||
import operator
|
||||
import warnings
|
||||
|
||||
|
||||
__all__ = ["Vector"]
|
||||
|
||||
|
||||
class Vector(tuple):
|
||||
"""A math-like vector.
|
||||
|
||||
Represents an n-dimensional numeric vector. ``Vector`` objects support
|
||||
vector addition and subtraction, scalar multiplication and division,
|
||||
negation, rounding, and comparison tests.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, values, keep=False):
|
||||
if keep is not False:
|
||||
warnings.warn(
|
||||
"the 'keep' argument has been deprecated",
|
||||
DeprecationWarning,
|
||||
)
|
||||
if type(values) == Vector:
|
||||
# No need to create a new object
|
||||
return values
|
||||
return super().__new__(cls, values)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({super().__repr__()})"
|
||||
|
||||
def _vectorOp(self, other, op):
|
||||
if isinstance(other, Vector):
|
||||
assert len(self) == len(other)
|
||||
return self.__class__(op(a, b) for a, b in zip(self, other))
|
||||
if isinstance(other, Number):
|
||||
return self.__class__(op(v, other) for v in self)
|
||||
raise NotImplementedError()
|
||||
|
||||
def _scalarOp(self, other, op):
|
||||
if isinstance(other, Number):
|
||||
return self.__class__(op(v, other) for v in self)
|
||||
raise NotImplementedError()
|
||||
|
||||
def _unaryOp(self, op):
|
||||
return self.__class__(op(v) for v in self)
|
||||
|
||||
def __add__(self, other):
|
||||
return self._vectorOp(other, operator.add)
|
||||
|
||||
__radd__ = __add__
|
||||
|
||||
def __sub__(self, other):
|
||||
return self._vectorOp(other, operator.sub)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self._vectorOp(other, _operator_rsub)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self._scalarOp(other, operator.mul)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self._scalarOp(other, operator.truediv)
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
return self._scalarOp(other, _operator_rtruediv)
|
||||
|
||||
def __pos__(self):
|
||||
return self._unaryOp(operator.pos)
|
||||
|
||||
def __neg__(self):
|
||||
return self._unaryOp(operator.neg)
|
||||
|
||||
def __round__(self, *, round=round):
|
||||
return self._unaryOp(round)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, list):
|
||||
# bw compat Vector([1, 2, 3]) == [1, 2, 3]
|
||||
other = tuple(other)
|
||||
return super().__eq__(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __bool__(self):
|
||||
return any(self)
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __abs__(self):
|
||||
return math.sqrt(sum(x * x for x in self))
|
||||
|
||||
def length(self):
|
||||
"""Return the length of the vector. Equivalent to abs(vector)."""
|
||||
return abs(self)
|
||||
|
||||
def normalized(self):
|
||||
"""Return the normalized vector of the vector."""
|
||||
return self / abs(self)
|
||||
|
||||
def dot(self, other):
|
||||
"""Performs vector dot product, returning the sum of
|
||||
``a[0] * b[0], a[1] * b[1], ...``"""
|
||||
assert len(self) == len(other)
|
||||
return sum(a * b for a, b in zip(self, other))
|
||||
|
||||
# Deprecated methods/properties
|
||||
|
||||
def toInt(self):
|
||||
warnings.warn(
|
||||
"the 'toInt' method has been deprecated, use round(vector) instead",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return self.__round__()
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
warnings.warn(
|
||||
"the 'values' attribute has been deprecated, use "
|
||||
"the vector object itself instead",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return list(self)
|
||||
|
||||
@values.setter
|
||||
def values(self, values):
|
||||
raise AttributeError(
|
||||
"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)
|
||||
|
||||
|
||||
def _operator_rtruediv(a, b):
|
||||
return operator.truediv(b, a)
|
||||
@@ -1,141 +0,0 @@
|
||||
"""Generic visitor pattern implementation for Python objects."""
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class Visitor(object):
|
||||
defaultStop = False
|
||||
|
||||
@classmethod
|
||||
def _register(celf, clazzes_attrs):
|
||||
assert celf != Visitor, "Subclass Visitor instead."
|
||||
if "_visitors" not in celf.__dict__:
|
||||
celf._visitors = {}
|
||||
|
||||
def wrapper(method):
|
||||
assert method.__name__ == "visit"
|
||||
for clazzes, attrs in clazzes_attrs:
|
||||
if type(clazzes) != tuple:
|
||||
clazzes = (clazzes,)
|
||||
if type(attrs) == str:
|
||||
attrs = (attrs,)
|
||||
for clazz in clazzes:
|
||||
_visitors = celf._visitors.setdefault(clazz, {})
|
||||
for attr in attrs:
|
||||
assert attr not in _visitors, (
|
||||
"Oops, class '%s' has visitor function for '%s' defined already."
|
||||
% (clazz.__name__, attr)
|
||||
)
|
||||
_visitors[attr] = method
|
||||
return None
|
||||
|
||||
return wrapper
|
||||
|
||||
@classmethod
|
||||
def register(celf, clazzes):
|
||||
if type(clazzes) != tuple:
|
||||
clazzes = (clazzes,)
|
||||
return celf._register([(clazzes, (None,))])
|
||||
|
||||
@classmethod
|
||||
def register_attr(celf, clazzes, attrs):
|
||||
clazzes_attrs = []
|
||||
if type(clazzes) != tuple:
|
||||
clazzes = (clazzes,)
|
||||
if type(attrs) == str:
|
||||
attrs = (attrs,)
|
||||
for clazz in clazzes:
|
||||
clazzes_attrs.append((clazz, attrs))
|
||||
return celf._register(clazzes_attrs)
|
||||
|
||||
@classmethod
|
||||
def register_attrs(celf, clazzes_attrs):
|
||||
return celf._register(clazzes_attrs)
|
||||
|
||||
@classmethod
|
||||
def _visitorsFor(celf, thing, _default={}):
|
||||
typ = type(thing)
|
||||
|
||||
for celf in celf.mro():
|
||||
_visitors = getattr(celf, "_visitors", None)
|
||||
if _visitors is None:
|
||||
break
|
||||
|
||||
m = celf._visitors.get(typ, None)
|
||||
if m is not None:
|
||||
return m
|
||||
|
||||
return _default
|
||||
|
||||
def visitObject(self, obj, *args, **kwargs):
|
||||
"""Called to visit an object. This function loops over all non-private
|
||||
attributes of the objects and calls any user-registered (via
|
||||
@register_attr() or @register_attrs()) visit() functions.
|
||||
|
||||
If there is no user-registered visit function, of if there is and it
|
||||
returns True, or it returns None (or doesn't return anything) and
|
||||
visitor.defaultStop is False (default), then the visitor will proceed
|
||||
to call self.visitAttr()"""
|
||||
|
||||
keys = sorted(vars(obj).keys())
|
||||
_visitors = self._visitorsFor(obj)
|
||||
defaultVisitor = _visitors.get("*", None)
|
||||
for key in keys:
|
||||
if key[0] == "_":
|
||||
continue
|
||||
value = getattr(obj, key)
|
||||
visitorFunc = _visitors.get(key, defaultVisitor)
|
||||
if visitorFunc is not None:
|
||||
ret = visitorFunc(self, obj, key, value, *args, **kwargs)
|
||||
if ret == False or (ret is None and self.defaultStop):
|
||||
continue
|
||||
self.visitAttr(obj, key, value, *args, **kwargs)
|
||||
|
||||
def visitAttr(self, obj, attr, value, *args, **kwargs):
|
||||
"""Called to visit an attribute of an object."""
|
||||
self.visit(value, *args, **kwargs)
|
||||
|
||||
def visitList(self, obj, *args, **kwargs):
|
||||
"""Called to visit any value that is a list."""
|
||||
for value in obj:
|
||||
self.visit(value, *args, **kwargs)
|
||||
|
||||
def visitDict(self, obj, *args, **kwargs):
|
||||
"""Called to visit any value that is a dictionary."""
|
||||
for value in obj.values():
|
||||
self.visit(value, *args, **kwargs)
|
||||
|
||||
def visitLeaf(self, obj, *args, **kwargs):
|
||||
"""Called to visit any value that is not an object, list,
|
||||
or dictionary."""
|
||||
pass
|
||||
|
||||
def visit(self, obj, *args, **kwargs):
|
||||
"""This is the main entry to the visitor. The visitor will visit object
|
||||
obj.
|
||||
|
||||
The visitor will first determine if there is a registered (via
|
||||
@register()) visit function for the type of object. If there is, it
|
||||
will be called, and (visitor, obj, *args, **kwargs) will be passed to
|
||||
the user visit function.
|
||||
|
||||
If there is no user-registered visit function, of if there is and it
|
||||
returns True, or it returns None (or doesn't return anything) and
|
||||
visitor.defaultStop is False (default), then the visitor will proceed
|
||||
to dispatch to one of self.visitObject(), self.visitList(),
|
||||
self.visitDict(), or self.visitLeaf() (any of which can be overriden in
|
||||
a subclass)."""
|
||||
|
||||
visitorFunc = self._visitorsFor(obj).get(None, None)
|
||||
if visitorFunc is not None:
|
||||
ret = visitorFunc(self, obj, *args, **kwargs)
|
||||
if ret == False or (ret is None and self.defaultStop):
|
||||
return
|
||||
if hasattr(obj, "__dict__") and not isinstance(obj, enum.Enum):
|
||||
self.visitObject(obj, *args, **kwargs)
|
||||
elif isinstance(obj, list):
|
||||
self.visitList(obj, *args, **kwargs)
|
||||
elif isinstance(obj, dict):
|
||||
self.visitDict(obj, *args, **kwargs)
|
||||
else:
|
||||
self.visitLeaf(obj, *args, **kwargs)
|
||||
@@ -1,188 +0,0 @@
|
||||
from fontTools import ttLib
|
||||
from fontTools.misc.textTools import safeEval
|
||||
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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:
|
||||
# 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 set(self, val, maxval=None):
|
||||
pass
|
||||
|
||||
def increment(self, val=1):
|
||||
pass
|
||||
|
||||
def setLabel(self, text):
|
||||
print(text)
|
||||
@@ -1,204 +0,0 @@
|
||||
"""xmlWriter.py -- Simple XML authoring class"""
|
||||
|
||||
from fontTools.misc.textTools import byteord, strjoin, tobytes, tostr
|
||||
import sys
|
||||
import os
|
||||
import string
|
||||
|
||||
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 escape(data):
|
||||
data = tostr(data, "utf_8")
|
||||
data = data.replace("&", "&")
|
||||
data = data.replace("<", "<")
|
||||
data = data.replace(">", ">")
|
||||
data = data.replace("\r", " ")
|
||||
return data
|
||||
|
||||
|
||||
def escapeattr(data):
|
||||
data = escape(data)
|
||||
data = data.replace('"', """)
|
||||
return data
|
||||
|
||||
|
||||
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")))
|
||||
|
||||
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
import sys
|
||||
from fontTools.mtiLib import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
"""OpenType Layout-related functionality."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
class OpenTypeLibError(Exception):
|
||||
def __init__(self, message, location):
|
||||
Exception.__init__(self, message)
|
||||
self.location = location
|
||||
|
||||
def __str__(self):
|
||||
message = Exception.__str__(self)
|
||||
if self.location:
|
||||
return f"{self.location}: {message}"
|
||||
else:
|
||||
return message
|
||||
@@ -1,96 +0,0 @@
|
||||
__all__ = ["maxCtxFont"]
|
||||
|
||||
|
||||
def maxCtxFont(font):
|
||||
"""Calculate the usMaxContext value for an entire font."""
|
||||
|
||||
maxCtx = 0
|
||||
for tag in ("GSUB", "GPOS"):
|
||||
if tag not in font:
|
||||
continue
|
||||
table = font[tag].table
|
||||
if not table.LookupList:
|
||||
continue
|
||||
for lookup in table.LookupList.Lookup:
|
||||
for st in lookup.SubTable:
|
||||
maxCtx = maxCtxSubtable(maxCtx, tag, lookup.LookupType, st)
|
||||
return maxCtx
|
||||
|
||||
|
||||
def maxCtxSubtable(maxCtx, tag, lookupType, st):
|
||||
"""Calculate usMaxContext based on a single lookup table (and an existing
|
||||
max value).
|
||||
"""
|
||||
|
||||
# single positioning, single / multiple substitution
|
||||
if (tag == "GPOS" and lookupType == 1) or (
|
||||
tag == "GSUB" and lookupType in (1, 2, 3)
|
||||
):
|
||||
maxCtx = max(maxCtx, 1)
|
||||
|
||||
# pair positioning
|
||||
elif tag == "GPOS" and lookupType == 2:
|
||||
maxCtx = max(maxCtx, 2)
|
||||
|
||||
# ligatures
|
||||
elif tag == "GSUB" and lookupType == 4:
|
||||
for ligatures in st.ligatures.values():
|
||||
for ligature in ligatures:
|
||||
maxCtx = max(maxCtx, ligature.CompCount)
|
||||
|
||||
# context
|
||||
elif (tag == "GPOS" and lookupType == 7) or (tag == "GSUB" and lookupType == 5):
|
||||
maxCtx = maxCtxContextualSubtable(maxCtx, st, "Pos" if tag == "GPOS" else "Sub")
|
||||
|
||||
# chained context
|
||||
elif (tag == "GPOS" and lookupType == 8) or (tag == "GSUB" and lookupType == 6):
|
||||
maxCtx = maxCtxContextualSubtable(
|
||||
maxCtx, st, "Pos" if tag == "GPOS" else "Sub", "Chain"
|
||||
)
|
||||
|
||||
# extensions
|
||||
elif (tag == "GPOS" and lookupType == 9) or (tag == "GSUB" and lookupType == 7):
|
||||
maxCtx = maxCtxSubtable(maxCtx, tag, st.ExtensionLookupType, st.ExtSubTable)
|
||||
|
||||
# reverse-chained context
|
||||
elif tag == "GSUB" and lookupType == 8:
|
||||
maxCtx = maxCtxContextualRule(maxCtx, st, "Reverse")
|
||||
|
||||
return maxCtx
|
||||
|
||||
|
||||
def maxCtxContextualSubtable(maxCtx, st, ruleType, chain=""):
|
||||
"""Calculate usMaxContext based on a contextual feature subtable."""
|
||||
|
||||
if st.Format == 1:
|
||||
for ruleset in getattr(st, "%s%sRuleSet" % (chain, ruleType)):
|
||||
if ruleset is None:
|
||||
continue
|
||||
for rule in getattr(ruleset, "%s%sRule" % (chain, ruleType)):
|
||||
if rule is None:
|
||||
continue
|
||||
maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
|
||||
|
||||
elif st.Format == 2:
|
||||
for ruleset in getattr(st, "%s%sClassSet" % (chain, ruleType)):
|
||||
if ruleset is None:
|
||||
continue
|
||||
for rule in getattr(ruleset, "%s%sClassRule" % (chain, ruleType)):
|
||||
if rule is None:
|
||||
continue
|
||||
maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
|
||||
|
||||
elif st.Format == 3:
|
||||
maxCtx = maxCtxContextualRule(maxCtx, st, chain)
|
||||
|
||||
return maxCtx
|
||||
|
||||
|
||||
def maxCtxContextualRule(maxCtx, st, chain):
|
||||
"""Calculate usMaxContext based on a contextual feature rule."""
|
||||
|
||||
if not chain:
|
||||
return max(maxCtx, st.GlyphCount)
|
||||
elif chain == "Reverse":
|
||||
return max(maxCtx, st.GlyphCount + st.LookAheadGlyphCount)
|
||||
return max(maxCtx, st.InputGlyphCount + st.LookAheadGlyphCount)
|
||||
@@ -1,53 +0,0 @@
|
||||
from argparse import RawTextHelpFormatter
|
||||
from fontTools.otlLib.optimize.gpos import COMPRESSION_LEVEL, compact
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Optimize the layout tables of an existing font"""
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from fontTools import configLogger
|
||||
|
||||
parser = ArgumentParser(
|
||||
prog="otlLib.optimize",
|
||||
description=main.__doc__,
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
)
|
||||
parser.add_argument("font")
|
||||
parser.add_argument(
|
||||
"-o", metavar="OUTPUTFILE", dest="outfile", default=None, help="output file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gpos-compression-level",
|
||||
help=COMPRESSION_LEVEL.help,
|
||||
default=COMPRESSION_LEVEL.default,
|
||||
choices=list(range(10)),
|
||||
type=int,
|
||||
)
|
||||
logging_group = parser.add_mutually_exclusive_group(required=False)
|
||||
logging_group.add_argument(
|
||||
"-v", "--verbose", action="store_true", help="Run more verbosely."
|
||||
)
|
||||
logging_group.add_argument(
|
||||
"-q", "--quiet", action="store_true", help="Turn verbosity off."
|
||||
)
|
||||
options = parser.parse_args(args)
|
||||
|
||||
configLogger(
|
||||
level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
|
||||
)
|
||||
|
||||
font = TTFont(options.font)
|
||||
compact(font, options.gpos_compression_level)
|
||||
font.save(options.outfile or options.font)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(main())
|
||||
import doctest
|
||||
|
||||
sys.exit(doctest.testmod().failed)
|
||||
@@ -1,6 +0,0 @@
|
||||
import sys
|
||||
from fontTools.otlLib.optimize import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -1,453 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict, namedtuple
|
||||
from functools import reduce
|
||||
from itertools import chain
|
||||
from math import log2
|
||||
from typing import DefaultDict, Dict, Iterable, List, Sequence, Tuple
|
||||
|
||||
from fontTools.config import OPTIONS
|
||||
from fontTools.misc.intTools import bit_count, bit_indices
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.ttLib.tables import otBase, otTables
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
COMPRESSION_LEVEL = OPTIONS[f"{__name__}:COMPRESSION_LEVEL"]
|
||||
|
||||
# Kept because ufo2ft depends on it, to be removed once ufo2ft uses the config instead
|
||||
# https://github.com/fonttools/fonttools/issues/2592
|
||||
GPOS_COMPACT_MODE_ENV_KEY = "FONTTOOLS_GPOS_COMPACT_MODE"
|
||||
GPOS_COMPACT_MODE_DEFAULT = str(COMPRESSION_LEVEL.default)
|
||||
|
||||
|
||||
def _compression_level_from_env() -> int:
|
||||
env_level = GPOS_COMPACT_MODE_DEFAULT
|
||||
if GPOS_COMPACT_MODE_ENV_KEY in os.environ:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
f"'{GPOS_COMPACT_MODE_ENV_KEY}' environment variable is deprecated. "
|
||||
"Please set the 'fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL' option "
|
||||
"in TTFont.cfg.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
env_level = os.environ[GPOS_COMPACT_MODE_ENV_KEY]
|
||||
if len(env_level) == 1 and env_level in "0123456789":
|
||||
return int(env_level)
|
||||
raise ValueError(f"Bad {GPOS_COMPACT_MODE_ENV_KEY}={env_level}")
|
||||
|
||||
|
||||
def compact(font: TTFont, level: int) -> TTFont:
|
||||
# Ideal plan:
|
||||
# 1. Find lookups of Lookup Type 2: Pair Adjustment Positioning Subtable
|
||||
# https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-2-pair-adjustment-positioning-subtable
|
||||
# 2. Extract glyph-glyph kerning and class-kerning from all present subtables
|
||||
# 3. Regroup into different subtable arrangements
|
||||
# 4. Put back into the lookup
|
||||
#
|
||||
# Actual implementation:
|
||||
# 2. Only class kerning is optimized currently
|
||||
# 3. If the input kerning is already in several subtables, the subtables
|
||||
# are not grouped together first; instead each subtable is treated
|
||||
# independently, so currently this step is:
|
||||
# Split existing subtables into more smaller subtables
|
||||
gpos = font["GPOS"]
|
||||
for lookup in gpos.table.LookupList.Lookup:
|
||||
if lookup.LookupType == 2:
|
||||
compact_lookup(font, level, lookup)
|
||||
elif lookup.LookupType == 9 and lookup.SubTable[0].ExtensionLookupType == 2:
|
||||
compact_ext_lookup(font, level, lookup)
|
||||
return font
|
||||
|
||||
|
||||
def compact_lookup(font: TTFont, level: int, lookup: otTables.Lookup) -> None:
|
||||
new_subtables = compact_pair_pos(font, level, lookup.SubTable)
|
||||
lookup.SubTable = new_subtables
|
||||
lookup.SubTableCount = len(new_subtables)
|
||||
|
||||
|
||||
def compact_ext_lookup(font: TTFont, level: int, lookup: otTables.Lookup) -> None:
|
||||
new_subtables = compact_pair_pos(
|
||||
font, level, [ext_subtable.ExtSubTable for ext_subtable in lookup.SubTable]
|
||||
)
|
||||
new_ext_subtables = []
|
||||
for subtable in new_subtables:
|
||||
ext_subtable = otTables.ExtensionPos()
|
||||
ext_subtable.Format = 1
|
||||
ext_subtable.ExtSubTable = subtable
|
||||
new_ext_subtables.append(ext_subtable)
|
||||
lookup.SubTable = new_ext_subtables
|
||||
lookup.SubTableCount = len(new_ext_subtables)
|
||||
|
||||
|
||||
def compact_pair_pos(
|
||||
font: TTFont, level: int, subtables: Sequence[otTables.PairPos]
|
||||
) -> Sequence[otTables.PairPos]:
|
||||
new_subtables = []
|
||||
for subtable in subtables:
|
||||
if subtable.Format == 1:
|
||||
# Not doing anything to Format 1 (yet?)
|
||||
new_subtables.append(subtable)
|
||||
elif subtable.Format == 2:
|
||||
new_subtables.extend(compact_class_pairs(font, level, subtable))
|
||||
return new_subtables
|
||||
|
||||
|
||||
def compact_class_pairs(
|
||||
font: TTFont, level: int, subtable: otTables.PairPos
|
||||
) -> List[otTables.PairPos]:
|
||||
from fontTools.otlLib.builder import buildPairPosClassesSubtable
|
||||
|
||||
subtables = []
|
||||
classes1: DefaultDict[int, List[str]] = defaultdict(list)
|
||||
for g in subtable.Coverage.glyphs:
|
||||
classes1[subtable.ClassDef1.classDefs.get(g, 0)].append(g)
|
||||
classes2: DefaultDict[int, List[str]] = defaultdict(list)
|
||||
for g, i in subtable.ClassDef2.classDefs.items():
|
||||
classes2[i].append(g)
|
||||
all_pairs = {}
|
||||
for i, class1 in enumerate(subtable.Class1Record):
|
||||
for j, class2 in enumerate(class1.Class2Record):
|
||||
if is_really_zero(class2):
|
||||
continue
|
||||
all_pairs[(tuple(sorted(classes1[i])), tuple(sorted(classes2[j])))] = (
|
||||
getattr(class2, "Value1", None),
|
||||
getattr(class2, "Value2", None),
|
||||
)
|
||||
grouped_pairs = cluster_pairs_by_class2_coverage_custom_cost(font, all_pairs, level)
|
||||
for pairs in grouped_pairs:
|
||||
subtables.append(buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap()))
|
||||
return subtables
|
||||
|
||||
|
||||
def is_really_zero(class2: otTables.Class2Record) -> bool:
|
||||
v1 = getattr(class2, "Value1", None)
|
||||
v2 = getattr(class2, "Value2", None)
|
||||
return (v1 is None or v1.getEffectiveFormat() == 0) and (
|
||||
v2 is None or v2.getEffectiveFormat() == 0
|
||||
)
|
||||
|
||||
|
||||
Pairs = Dict[
|
||||
Tuple[Tuple[str, ...], Tuple[str, ...]],
|
||||
Tuple[otBase.ValueRecord, otBase.ValueRecord],
|
||||
]
|
||||
|
||||
|
||||
# Adapted from https://github.com/fonttools/fonttools/blob/f64f0b42f2d1163b2d85194e0979def539f5dca3/Lib/fontTools/ttLib/tables/otTables.py#L935-L958
|
||||
def _getClassRanges(glyphIDs: Iterable[int]):
|
||||
glyphIDs = sorted(glyphIDs)
|
||||
last = glyphIDs[0]
|
||||
ranges = [[last]]
|
||||
for glyphID in glyphIDs[1:]:
|
||||
if glyphID != last + 1:
|
||||
ranges[-1].append(last)
|
||||
ranges.append([glyphID])
|
||||
last = glyphID
|
||||
ranges[-1].append(last)
|
||||
return ranges, glyphIDs[0], glyphIDs[-1]
|
||||
|
||||
|
||||
# Adapted from https://github.com/fonttools/fonttools/blob/f64f0b42f2d1163b2d85194e0979def539f5dca3/Lib/fontTools/ttLib/tables/otTables.py#L960-L989
|
||||
def _classDef_bytes(
|
||||
class_data: List[Tuple[List[Tuple[int, int]], int, int]],
|
||||
class_ids: List[int],
|
||||
coverage=False,
|
||||
):
|
||||
if not class_ids:
|
||||
return 0
|
||||
first_ranges, min_glyph_id, max_glyph_id = class_data[class_ids[0]]
|
||||
range_count = len(first_ranges)
|
||||
for i in class_ids[1:]:
|
||||
data = class_data[i]
|
||||
range_count += len(data[0])
|
||||
min_glyph_id = min(min_glyph_id, data[1])
|
||||
max_glyph_id = max(max_glyph_id, data[2])
|
||||
glyphCount = max_glyph_id - min_glyph_id + 1
|
||||
# https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-1
|
||||
format1_bytes = 6 + glyphCount * 2
|
||||
# https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-2
|
||||
format2_bytes = 4 + range_count * 6
|
||||
return min(format1_bytes, format2_bytes)
|
||||
|
||||
|
||||
ClusteringContext = namedtuple(
|
||||
"ClusteringContext",
|
||||
[
|
||||
"lines",
|
||||
"all_class1",
|
||||
"all_class1_data",
|
||||
"all_class2_data",
|
||||
"valueFormat1_bytes",
|
||||
"valueFormat2_bytes",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class Cluster:
|
||||
# TODO(Python 3.7): Turn this into a dataclass
|
||||
# ctx: ClusteringContext
|
||||
# indices: int
|
||||
# Caches
|
||||
# TODO(Python 3.8): use functools.cached_property instead of the
|
||||
# manually cached properties, and remove the cache fields listed below.
|
||||
# _indices: Optional[List[int]] = None
|
||||
# _column_indices: Optional[List[int]] = None
|
||||
# _cost: Optional[int] = None
|
||||
|
||||
__slots__ = "ctx", "indices_bitmask", "_indices", "_column_indices", "_cost"
|
||||
|
||||
def __init__(self, ctx: ClusteringContext, indices_bitmask: int):
|
||||
self.ctx = ctx
|
||||
self.indices_bitmask = indices_bitmask
|
||||
self._indices = None
|
||||
self._column_indices = None
|
||||
self._cost = None
|
||||
|
||||
@property
|
||||
def indices(self):
|
||||
if self._indices is None:
|
||||
self._indices = bit_indices(self.indices_bitmask)
|
||||
return self._indices
|
||||
|
||||
@property
|
||||
def column_indices(self):
|
||||
if self._column_indices is None:
|
||||
# Indices of columns that have a 1 in at least 1 line
|
||||
# => binary OR all the lines
|
||||
bitmask = reduce(int.__or__, (self.ctx.lines[i] for i in self.indices))
|
||||
self._column_indices = bit_indices(bitmask)
|
||||
return self._column_indices
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
# Add 1 because Class2=0 cannot be used but needs to be encoded.
|
||||
return len(self.column_indices) + 1
|
||||
|
||||
@property
|
||||
def cost(self):
|
||||
if self._cost is None:
|
||||
self._cost = (
|
||||
# 2 bytes to store the offset to this subtable in the Lookup table above
|
||||
2
|
||||
# Contents of the subtable
|
||||
# From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment
|
||||
# uint16 posFormat Format identifier: format = 2
|
||||
+ 2
|
||||
# Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable.
|
||||
+ 2
|
||||
+ self.coverage_bytes
|
||||
# uint16 valueFormat1 ValueRecord definition — for the first glyph of the pair (may be zero).
|
||||
+ 2
|
||||
# uint16 valueFormat2 ValueRecord definition — for the second glyph of the pair (may be zero).
|
||||
+ 2
|
||||
# Offset16 classDef1Offset Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair.
|
||||
+ 2
|
||||
+ self.classDef1_bytes
|
||||
# Offset16 classDef2Offset Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair.
|
||||
+ 2
|
||||
+ self.classDef2_bytes
|
||||
# uint16 class1Count Number of classes in classDef1 table — includes Class 0.
|
||||
+ 2
|
||||
# uint16 class2Count Number of classes in classDef2 table — includes Class 0.
|
||||
+ 2
|
||||
# Class1Record class1Records[class1Count] Array of Class1 records, ordered by classes in classDef1.
|
||||
+ (self.ctx.valueFormat1_bytes + self.ctx.valueFormat2_bytes)
|
||||
* len(self.indices)
|
||||
* self.width
|
||||
)
|
||||
return self._cost
|
||||
|
||||
@property
|
||||
def coverage_bytes(self):
|
||||
format1_bytes = (
|
||||
# From https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-1
|
||||
# uint16 coverageFormat Format identifier — format = 1
|
||||
# uint16 glyphCount Number of glyphs in the glyph array
|
||||
4
|
||||
# uint16 glyphArray[glyphCount] Array of glyph IDs — in numerical order
|
||||
+ sum(len(self.ctx.all_class1[i]) for i in self.indices) * 2
|
||||
)
|
||||
ranges = sorted(
|
||||
chain.from_iterable(self.ctx.all_class1_data[i][0] for i in self.indices)
|
||||
)
|
||||
merged_range_count = 0
|
||||
last = None
|
||||
for start, end in ranges:
|
||||
if last is not None and start != last + 1:
|
||||
merged_range_count += 1
|
||||
last = end
|
||||
format2_bytes = (
|
||||
# From https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-2
|
||||
# uint16 coverageFormat Format identifier — format = 2
|
||||
# uint16 rangeCount Number of RangeRecords
|
||||
4
|
||||
# RangeRecord rangeRecords[rangeCount] Array of glyph ranges — ordered by startGlyphID.
|
||||
# uint16 startGlyphID First glyph ID in the range
|
||||
# uint16 endGlyphID Last glyph ID in the range
|
||||
# uint16 startCoverageIndex Coverage Index of first glyph ID in range
|
||||
+ merged_range_count * 6
|
||||
)
|
||||
return min(format1_bytes, format2_bytes)
|
||||
|
||||
@property
|
||||
def classDef1_bytes(self):
|
||||
# We can skip encoding one of the Class1 definitions, and use
|
||||
# Class1=0 to represent it instead, because Class1 is gated by the
|
||||
# Coverage definition. Use Class1=0 for the highest byte savings.
|
||||
# Going through all options takes too long, pick the biggest class
|
||||
# = what happens in otlLib.builder.ClassDefBuilder.classes()
|
||||
biggest_index = max(self.indices, key=lambda i: len(self.ctx.all_class1[i]))
|
||||
return _classDef_bytes(
|
||||
self.ctx.all_class1_data, [i for i in self.indices if i != biggest_index]
|
||||
)
|
||||
|
||||
@property
|
||||
def classDef2_bytes(self):
|
||||
# All Class2 need to be encoded because we can't use Class2=0
|
||||
return _classDef_bytes(self.ctx.all_class2_data, self.column_indices)
|
||||
|
||||
|
||||
def cluster_pairs_by_class2_coverage_custom_cost(
|
||||
font: TTFont,
|
||||
pairs: Pairs,
|
||||
compression: int = 5,
|
||||
) -> List[Pairs]:
|
||||
if not pairs:
|
||||
# The subtable was actually empty?
|
||||
return [pairs]
|
||||
|
||||
# Sorted for reproducibility/determinism
|
||||
all_class1 = sorted(set(pair[0] for pair in pairs))
|
||||
all_class2 = sorted(set(pair[1] for pair in pairs))
|
||||
|
||||
# Use Python's big ints for binary vectors representing each line
|
||||
lines = [
|
||||
sum(
|
||||
1 << i if (class1, class2) in pairs else 0
|
||||
for i, class2 in enumerate(all_class2)
|
||||
)
|
||||
for class1 in all_class1
|
||||
]
|
||||
|
||||
# Map glyph names to ids and work with ints throughout for ClassDef formats
|
||||
name_to_id = font.getReverseGlyphMap()
|
||||
# Each entry in the arrays below is (range_count, min_glyph_id, max_glyph_id)
|
||||
all_class1_data = [
|
||||
_getClassRanges(name_to_id[name] for name in cls) for cls in all_class1
|
||||
]
|
||||
all_class2_data = [
|
||||
_getClassRanges(name_to_id[name] for name in cls) for cls in all_class2
|
||||
]
|
||||
|
||||
format1 = 0
|
||||
format2 = 0
|
||||
for pair, value in pairs.items():
|
||||
format1 |= value[0].getEffectiveFormat() if value[0] else 0
|
||||
format2 |= value[1].getEffectiveFormat() if value[1] else 0
|
||||
valueFormat1_bytes = bit_count(format1) * 2
|
||||
valueFormat2_bytes = bit_count(format2) * 2
|
||||
|
||||
ctx = ClusteringContext(
|
||||
lines,
|
||||
all_class1,
|
||||
all_class1_data,
|
||||
all_class2_data,
|
||||
valueFormat1_bytes,
|
||||
valueFormat2_bytes,
|
||||
)
|
||||
|
||||
cluster_cache: Dict[int, Cluster] = {}
|
||||
|
||||
def make_cluster(indices: int) -> Cluster:
|
||||
cluster = cluster_cache.get(indices, None)
|
||||
if cluster is not None:
|
||||
return cluster
|
||||
cluster = Cluster(ctx, indices)
|
||||
cluster_cache[indices] = cluster
|
||||
return cluster
|
||||
|
||||
def merge(cluster: Cluster, other: Cluster) -> Cluster:
|
||||
return make_cluster(cluster.indices_bitmask | other.indices_bitmask)
|
||||
|
||||
# Agglomerative clustering by hand, checking the cost gain of the new
|
||||
# cluster against the previously separate clusters
|
||||
# Start with 1 cluster per line
|
||||
# cluster = set of lines = new subtable
|
||||
clusters = [make_cluster(1 << i) for i in range(len(lines))]
|
||||
|
||||
# Cost of 1 cluster with everything
|
||||
# `(1 << len) - 1` gives a bitmask full of 1's of length `len`
|
||||
cost_before_splitting = make_cluster((1 << len(lines)) - 1).cost
|
||||
log.debug(f" len(clusters) = {len(clusters)}")
|
||||
|
||||
while len(clusters) > 1:
|
||||
lowest_cost_change = None
|
||||
best_cluster_index = None
|
||||
best_other_index = None
|
||||
best_merged = None
|
||||
for i, cluster in enumerate(clusters):
|
||||
for j, other in enumerate(clusters[i + 1 :]):
|
||||
merged = merge(cluster, other)
|
||||
cost_change = merged.cost - cluster.cost - other.cost
|
||||
if lowest_cost_change is None or cost_change < lowest_cost_change:
|
||||
lowest_cost_change = cost_change
|
||||
best_cluster_index = i
|
||||
best_other_index = i + 1 + j
|
||||
best_merged = merged
|
||||
assert lowest_cost_change is not None
|
||||
assert best_cluster_index is not None
|
||||
assert best_other_index is not None
|
||||
assert best_merged is not None
|
||||
|
||||
# If the best merge we found is still taking down the file size, then
|
||||
# there's no question: we must do it, because it's beneficial in both
|
||||
# ways (lower file size and lower number of subtables). However, if the
|
||||
# best merge we found is not reducing file size anymore, then we need to
|
||||
# look at the other stop criteria = the compression factor.
|
||||
if lowest_cost_change > 0:
|
||||
# Stop critera: check whether we should keep merging.
|
||||
# Compute size reduction brought by splitting
|
||||
cost_after_splitting = sum(c.cost for c in clusters)
|
||||
# size_reduction so that after = before * (1 - size_reduction)
|
||||
# E.g. before = 1000, after = 800, 1 - 800/1000 = 0.2
|
||||
size_reduction = 1 - cost_after_splitting / cost_before_splitting
|
||||
|
||||
# Force more merging by taking into account the compression number.
|
||||
# Target behaviour: compression number = 1 to 9, default 5 like gzip
|
||||
# - 1 = accept to add 1 subtable to reduce size by 50%
|
||||
# - 5 = accept to add 5 subtables to reduce size by 50%
|
||||
# See https://github.com/harfbuzz/packtab/blob/master/Lib/packTab/__init__.py#L690-L691
|
||||
# Given the size reduction we have achieved so far, compute how many
|
||||
# new subtables are acceptable.
|
||||
max_new_subtables = -log2(1 - size_reduction) * compression
|
||||
log.debug(
|
||||
f" len(clusters) = {len(clusters):3d} size_reduction={size_reduction:5.2f} max_new_subtables={max_new_subtables}",
|
||||
)
|
||||
if compression == 9:
|
||||
# Override level 9 to mean: create any number of subtables
|
||||
max_new_subtables = len(clusters)
|
||||
|
||||
# If we have managed to take the number of new subtables below the
|
||||
# threshold, then we can stop.
|
||||
if len(clusters) <= max_new_subtables + 1:
|
||||
break
|
||||
|
||||
# No reason to stop yet, do the merge and move on to the next.
|
||||
del clusters[best_other_index]
|
||||
clusters[best_cluster_index] = best_merged
|
||||
|
||||
# All clusters are final; turn bitmasks back into the "Pairs" format
|
||||
pairs_by_class1: Dict[Tuple[str, ...], Pairs] = defaultdict(dict)
|
||||
for pair, values in pairs.items():
|
||||
pairs_by_class1[pair[0]][pair] = values
|
||||
pairs_groups: List[Pairs] = []
|
||||
for cluster in clusters:
|
||||
pairs_group: Pairs = dict()
|
||||
for i in cluster.indices:
|
||||
class1 = all_class1[i]
|
||||
pairs_group.update(pairs_by_class1[class1])
|
||||
pairs_groups.append(pairs_group)
|
||||
return pairs_groups
|
||||
@@ -1 +0,0 @@
|
||||
"""Empty __init__.py file to signal Python this directory is a package."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,52 +0,0 @@
|
||||
"""Calculate the area of a glyph."""
|
||||
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["AreaPen"]
|
||||
|
||||
|
||||
class AreaPen(BasePen):
|
||||
def __init__(self, glyphset=None):
|
||||
BasePen.__init__(self, glyphset)
|
||||
self.value = 0
|
||||
|
||||
def _moveTo(self, p0):
|
||||
self._p0 = self._startPoint = p0
|
||||
|
||||
def _lineTo(self, p1):
|
||||
x0, y0 = self._p0
|
||||
x1, y1 = p1
|
||||
self.value -= (x1 - x0) * (y1 + y0) * 0.5
|
||||
self._p0 = p1
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
# https://github.com/Pomax/bezierinfo/issues/44
|
||||
p0 = self._p0
|
||||
x0, y0 = p0[0], p0[1]
|
||||
x1, y1 = p1[0] - x0, p1[1] - y0
|
||||
x2, y2 = p2[0] - x0, p2[1] - y0
|
||||
self.value -= (x2 * y1 - x1 * y2) / 3
|
||||
self._lineTo(p2)
|
||||
self._p0 = p2
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
# https://github.com/Pomax/bezierinfo/issues/44
|
||||
p0 = self._p0
|
||||
x0, y0 = p0[0], p0[1]
|
||||
x1, y1 = p1[0] - x0, p1[1] - y0
|
||||
x2, y2 = p2[0] - x0, p2[1] - y0
|
||||
x3, y3 = p3[0] - x0, p3[1] - y0
|
||||
self.value -= (x1 * (-y2 - y3) + x2 * (y1 - 2 * y3) + x3 * (y1 + 2 * y2)) * 0.15
|
||||
self._lineTo(p3)
|
||||
self._p0 = p3
|
||||
|
||||
def _closePath(self):
|
||||
self._lineTo(self._startPoint)
|
||||
del self._p0, self._startPoint
|
||||
|
||||
def _endPath(self):
|
||||
if self._p0 != self._startPoint:
|
||||
# Area is not defined for open contours.
|
||||
raise NotImplementedError
|
||||
del self._p0, self._startPoint
|
||||
@@ -1,475 +0,0 @@
|
||||
"""fontTools.pens.basePen.py -- Tools and base classes to build pen objects.
|
||||
|
||||
The Pen Protocol
|
||||
|
||||
A Pen is a kind of object that standardizes the way how to "draw" outlines:
|
||||
it is a middle man between an outline and a drawing. In other words:
|
||||
it is an abstraction for drawing outlines, making sure that outline objects
|
||||
don't need to know the details about how and where they're being drawn, and
|
||||
that drawings don't need to know the details of how outlines are stored.
|
||||
|
||||
The most basic pattern is this::
|
||||
|
||||
outline.draw(pen) # 'outline' draws itself onto 'pen'
|
||||
|
||||
Pens can be used to render outlines to the screen, but also to construct
|
||||
new outlines. Eg. an outline object can be both a drawable object (it has a
|
||||
draw() method) as well as a pen itself: you *build* an outline using pen
|
||||
methods.
|
||||
|
||||
The AbstractPen class defines the Pen protocol. It implements almost
|
||||
nothing (only no-op closePath() and endPath() methods), but is useful
|
||||
for documentation purposes. Subclassing it basically tells the reader:
|
||||
"this class implements the Pen protocol.". An examples of an AbstractPen
|
||||
subclass is :py:class:`fontTools.pens.transformPen.TransformPen`.
|
||||
|
||||
The BasePen class is a base implementation useful for pens that actually
|
||||
draw (for example a pen renders outlines using a native graphics engine).
|
||||
BasePen contains a lot of base functionality, making it very easy to build
|
||||
a pen that fully conforms to the pen protocol. Note that if you subclass
|
||||
BasePen, you *don't* override moveTo(), lineTo(), etc., but _moveTo(),
|
||||
_lineTo(), etc. See the BasePen doc string for details. Examples of
|
||||
BasePen subclasses are fontTools.pens.boundsPen.BoundsPen and
|
||||
fontTools.pens.cocoaPen.CocoaPen.
|
||||
|
||||
Coordinates are usually expressed as (x, y) tuples, but generally any
|
||||
sequence of length 2 will do.
|
||||
"""
|
||||
|
||||
from typing import Tuple, Dict
|
||||
|
||||
from fontTools.misc.loggingTools import LogMixin
|
||||
from fontTools.misc.transform import DecomposedTransform, Identity
|
||||
|
||||
__all__ = [
|
||||
"AbstractPen",
|
||||
"NullPen",
|
||||
"BasePen",
|
||||
"PenError",
|
||||
"decomposeSuperBezierSegment",
|
||||
"decomposeQuadraticSegment",
|
||||
]
|
||||
|
||||
|
||||
class PenError(Exception):
|
||||
"""Represents an error during penning."""
|
||||
|
||||
|
||||
class OpenContourError(PenError):
|
||||
pass
|
||||
|
||||
|
||||
class AbstractPen:
|
||||
def moveTo(self, pt: Tuple[float, float]) -> None:
|
||||
"""Begin a new sub path, set the current point to 'pt'. You must
|
||||
end each sub path with a call to pen.closePath() or pen.endPath().
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lineTo(self, pt: Tuple[float, float]) -> None:
|
||||
"""Draw a straight line from the current point to 'pt'."""
|
||||
raise NotImplementedError
|
||||
|
||||
def curveTo(self, *points: Tuple[float, float]) -> None:
|
||||
"""Draw a cubic bezier with an arbitrary number of control points.
|
||||
|
||||
The last point specified is on-curve, all others are off-curve
|
||||
(control) points. If the number of control points is > 2, the
|
||||
segment is split into multiple bezier segments. This works
|
||||
like this:
|
||||
|
||||
Let n be the number of control points (which is the number of
|
||||
arguments to this call minus 1). If n==2, a plain vanilla cubic
|
||||
bezier is drawn. If n==1, we fall back to a quadratic segment and
|
||||
if n==0 we draw a straight line. It gets interesting when n>2:
|
||||
n-1 PostScript-style cubic segments will be drawn as if it were
|
||||
one curve. See decomposeSuperBezierSegment().
|
||||
|
||||
The conversion algorithm used for n>2 is inspired by NURB
|
||||
splines, and is conceptually equivalent to the TrueType "implied
|
||||
points" principle. See also decomposeQuadraticSegment().
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def qCurveTo(self, *points: Tuple[float, float]) -> None:
|
||||
"""Draw a whole string of quadratic curve segments.
|
||||
|
||||
The last point specified is on-curve, all others are off-curve
|
||||
points.
|
||||
|
||||
This method implements TrueType-style curves, breaking up curves
|
||||
using 'implied points': between each two consequtive off-curve points,
|
||||
there is one implied point exactly in the middle between them. See
|
||||
also decomposeQuadraticSegment().
|
||||
|
||||
The last argument (normally the on-curve point) may be None.
|
||||
This is to support contours that have NO on-curve points (a rarely
|
||||
seen feature of TrueType outlines).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def closePath(self) -> None:
|
||||
"""Close the current sub path. You must call either pen.closePath()
|
||||
or pen.endPath() after each sub path.
|
||||
"""
|
||||
pass
|
||||
|
||||
def endPath(self) -> None:
|
||||
"""End the current sub path, but don't close it. You must call
|
||||
either pen.closePath() or pen.endPath() after each sub path.
|
||||
"""
|
||||
pass
|
||||
|
||||
def addComponent(
|
||||
self,
|
||||
glyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float],
|
||||
) -> None:
|
||||
"""Add a sub glyph. The 'transformation' argument must be a 6-tuple
|
||||
containing an affine transformation, or a Transform object from the
|
||||
fontTools.misc.transform module. More precisely: it should be a
|
||||
sequence containing 6 numbers.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def addVarComponent(
|
||||
self,
|
||||
glyphName: str,
|
||||
transformation: DecomposedTransform,
|
||||
location: Dict[str, float],
|
||||
) -> None:
|
||||
"""Add a VarComponent sub glyph. The 'transformation' argument
|
||||
must be a DecomposedTransform from the fontTools.misc.transform module,
|
||||
and the 'location' argument must be a dictionary mapping axis tags
|
||||
to their locations.
|
||||
"""
|
||||
# GlyphSet decomposes for us
|
||||
raise AttributeError
|
||||
|
||||
|
||||
class NullPen(AbstractPen):
|
||||
"""A pen that does nothing."""
|
||||
|
||||
def moveTo(self, pt):
|
||||
pass
|
||||
|
||||
def lineTo(self, pt):
|
||||
pass
|
||||
|
||||
def curveTo(self, *points):
|
||||
pass
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
pass
|
||||
|
||||
def closePath(self):
|
||||
pass
|
||||
|
||||
def endPath(self):
|
||||
pass
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
pass
|
||||
|
||||
def addVarComponent(self, glyphName, transformation, location):
|
||||
pass
|
||||
|
||||
|
||||
class LoggingPen(LogMixin, AbstractPen):
|
||||
"""A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MissingComponentError(KeyError):
|
||||
"""Indicates a component pointing to a non-existent glyph in the glyphset."""
|
||||
|
||||
|
||||
class DecomposingPen(LoggingPen):
|
||||
"""Implements a 'addComponent' method that decomposes components
|
||||
(i.e. draws them onto self as simple contours).
|
||||
It can also be used as a mixin class (e.g. see ContourRecordingPen).
|
||||
|
||||
You must override moveTo, lineTo, curveTo and qCurveTo. You may
|
||||
additionally override closePath, endPath and addComponent.
|
||||
|
||||
By default a warning message is logged when a base glyph is missing;
|
||||
set the class variable ``skipMissingComponents`` to False if you want
|
||||
all instances of a sub-class to raise a :class:`MissingComponentError`
|
||||
exception by default.
|
||||
"""
|
||||
|
||||
skipMissingComponents = True
|
||||
# alias error for convenience
|
||||
MissingComponentError = MissingComponentError
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
glyphSet,
|
||||
*args,
|
||||
skipMissingComponents=None,
|
||||
reverseFlipped=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
|
||||
as components are looked up by their name.
|
||||
|
||||
If the optional 'reverseFlipped' argument is True, components whose transformation
|
||||
matrix has a negative determinant will be decomposed with a reversed path direction
|
||||
to compensate for the flip.
|
||||
|
||||
The optional 'skipMissingComponents' argument can be set to True/False to
|
||||
override the homonymous class attribute for a given pen instance.
|
||||
"""
|
||||
super(DecomposingPen, self).__init__(*args, **kwargs)
|
||||
self.glyphSet = glyphSet
|
||||
self.skipMissingComponents = (
|
||||
self.__class__.skipMissingComponents
|
||||
if skipMissingComponents is None
|
||||
else skipMissingComponents
|
||||
)
|
||||
self.reverseFlipped = reverseFlipped
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
"""Transform the points of the base glyph and draw it onto self."""
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
|
||||
try:
|
||||
glyph = self.glyphSet[glyphName]
|
||||
except KeyError:
|
||||
if not self.skipMissingComponents:
|
||||
raise MissingComponentError(glyphName)
|
||||
self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName)
|
||||
else:
|
||||
pen = self
|
||||
if transformation != Identity:
|
||||
pen = TransformPen(pen, transformation)
|
||||
if self.reverseFlipped:
|
||||
# if the transformation has a negative determinant, it will
|
||||
# reverse the contour direction of the component
|
||||
a, b, c, d = transformation[:4]
|
||||
det = a * d - b * c
|
||||
if det < 0:
|
||||
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||
|
||||
pen = ReverseContourPen(pen)
|
||||
glyph.draw(pen)
|
||||
|
||||
def addVarComponent(self, glyphName, transformation, location):
|
||||
# GlyphSet decomposes for us
|
||||
raise AttributeError
|
||||
|
||||
|
||||
class BasePen(DecomposingPen):
|
||||
"""Base class for drawing pens. You must override _moveTo, _lineTo and
|
||||
_curveToOne. You may additionally override _closePath, _endPath,
|
||||
addComponent, addVarComponent, and/or _qCurveToOne. You should not
|
||||
override any other methods.
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet=None):
|
||||
super(BasePen, self).__init__(glyphSet)
|
||||
self.__currentPoint = None
|
||||
|
||||
# must override
|
||||
|
||||
def _moveTo(self, pt):
|
||||
raise NotImplementedError
|
||||
|
||||
def _lineTo(self, pt):
|
||||
raise NotImplementedError
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
raise NotImplementedError
|
||||
|
||||
# may override
|
||||
|
||||
def _closePath(self):
|
||||
pass
|
||||
|
||||
def _endPath(self):
|
||||
pass
|
||||
|
||||
def _qCurveToOne(self, pt1, pt2):
|
||||
"""This method implements the basic quadratic curve type. The
|
||||
default implementation delegates the work to the cubic curve
|
||||
function. Optionally override with a native implementation.
|
||||
"""
|
||||
pt0x, pt0y = self.__currentPoint
|
||||
pt1x, pt1y = pt1
|
||||
pt2x, pt2y = pt2
|
||||
mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
|
||||
mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
|
||||
mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
|
||||
mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
|
||||
self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
|
||||
|
||||
# don't override
|
||||
|
||||
def _getCurrentPoint(self):
|
||||
"""Return the current point. This is not part of the public
|
||||
interface, yet is useful for subclasses.
|
||||
"""
|
||||
return self.__currentPoint
|
||||
|
||||
def closePath(self):
|
||||
self._closePath()
|
||||
self.__currentPoint = None
|
||||
|
||||
def endPath(self):
|
||||
self._endPath()
|
||||
self.__currentPoint = None
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._moveTo(pt)
|
||||
self.__currentPoint = pt
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._lineTo(pt)
|
||||
self.__currentPoint = pt
|
||||
|
||||
def curveTo(self, *points):
|
||||
n = len(points) - 1 # 'n' is the number of control points
|
||||
assert n >= 0
|
||||
if n == 2:
|
||||
# The common case, we have exactly two BCP's, so this is a standard
|
||||
# cubic bezier. Even though decomposeSuperBezierSegment() handles
|
||||
# this case just fine, we special-case it anyway since it's so
|
||||
# common.
|
||||
self._curveToOne(*points)
|
||||
self.__currentPoint = points[-1]
|
||||
elif n > 2:
|
||||
# n is the number of control points; split curve into n-1 cubic
|
||||
# bezier segments. The algorithm used here is inspired by NURB
|
||||
# splines and the TrueType "implied point" principle, and ensures
|
||||
# the smoothest possible connection between two curve segments,
|
||||
# with no disruption in the curvature. It is practical since it
|
||||
# allows one to construct multiple bezier segments with a much
|
||||
# smaller amount of points.
|
||||
_curveToOne = self._curveToOne
|
||||
for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
|
||||
_curveToOne(pt1, pt2, pt3)
|
||||
self.__currentPoint = pt3
|
||||
elif n == 1:
|
||||
self.qCurveTo(*points)
|
||||
elif n == 0:
|
||||
self.lineTo(points[0])
|
||||
else:
|
||||
raise AssertionError("can't get there from here")
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
n = len(points) - 1 # 'n' is the number of control points
|
||||
assert n >= 0
|
||||
if points[-1] is None:
|
||||
# Special case for TrueType quadratics: it is possible to
|
||||
# define a contour with NO on-curve points. BasePen supports
|
||||
# this by allowing the final argument (the expected on-curve
|
||||
# point) to be None. We simulate the feature by making the implied
|
||||
# on-curve point between the last and the first off-curve points
|
||||
# explicit.
|
||||
x, y = points[-2] # last off-curve point
|
||||
nx, ny = points[0] # first off-curve point
|
||||
impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
|
||||
self.__currentPoint = impliedStartPoint
|
||||
self._moveTo(impliedStartPoint)
|
||||
points = points[:-1] + (impliedStartPoint,)
|
||||
if n > 0:
|
||||
# Split the string of points into discrete quadratic curve
|
||||
# segments. Between any two consecutive off-curve points
|
||||
# there's an implied on-curve point exactly in the middle.
|
||||
# This is where the segment splits.
|
||||
_qCurveToOne = self._qCurveToOne
|
||||
for pt1, pt2 in decomposeQuadraticSegment(points):
|
||||
_qCurveToOne(pt1, pt2)
|
||||
self.__currentPoint = pt2
|
||||
else:
|
||||
self.lineTo(points[0])
|
||||
|
||||
|
||||
def decomposeSuperBezierSegment(points):
|
||||
"""Split the SuperBezier described by 'points' into a list of regular
|
||||
bezier segments. The 'points' argument must be a sequence with length
|
||||
3 or greater, containing (x, y) coordinates. The last point is the
|
||||
destination on-curve point, the rest of the points are off-curve points.
|
||||
The start point should not be supplied.
|
||||
|
||||
This function returns a list of (pt1, pt2, pt3) tuples, which each
|
||||
specify a regular curveto-style bezier segment.
|
||||
"""
|
||||
n = len(points) - 1
|
||||
assert n > 1
|
||||
bezierSegments = []
|
||||
pt1, pt2, pt3 = points[0], None, None
|
||||
for i in range(2, n + 1):
|
||||
# calculate points in between control points.
|
||||
nDivisions = min(i, 3, n - i + 2)
|
||||
for j in range(1, nDivisions):
|
||||
factor = j / nDivisions
|
||||
temp1 = points[i - 1]
|
||||
temp2 = points[i - 2]
|
||||
temp = (
|
||||
temp2[0] + factor * (temp1[0] - temp2[0]),
|
||||
temp2[1] + factor * (temp1[1] - temp2[1]),
|
||||
)
|
||||
if pt2 is None:
|
||||
pt2 = temp
|
||||
else:
|
||||
pt3 = (0.5 * (pt2[0] + temp[0]), 0.5 * (pt2[1] + temp[1]))
|
||||
bezierSegments.append((pt1, pt2, pt3))
|
||||
pt1, pt2, pt3 = temp, None, None
|
||||
bezierSegments.append((pt1, points[-2], points[-1]))
|
||||
return bezierSegments
|
||||
|
||||
|
||||
def decomposeQuadraticSegment(points):
|
||||
"""Split the quadratic curve segment described by 'points' into a list
|
||||
of "atomic" quadratic segments. The 'points' argument must be a sequence
|
||||
with length 2 or greater, containing (x, y) coordinates. The last point
|
||||
is the destination on-curve point, the rest of the points are off-curve
|
||||
points. The start point should not be supplied.
|
||||
|
||||
This function returns a list of (pt1, pt2) tuples, which each specify a
|
||||
plain quadratic bezier segment.
|
||||
"""
|
||||
n = len(points) - 1
|
||||
assert n > 0
|
||||
quadSegments = []
|
||||
for i in range(n - 1):
|
||||
x, y = points[i]
|
||||
nx, ny = points[i + 1]
|
||||
impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
|
||||
quadSegments.append((points[i], impliedPt))
|
||||
quadSegments.append((points[-2], points[-1]))
|
||||
return quadSegments
|
||||
|
||||
|
||||
class _TestPen(BasePen):
|
||||
"""Test class that prints PostScript to stdout."""
|
||||
|
||||
def _moveTo(self, pt):
|
||||
print("%s %s moveto" % (pt[0], pt[1]))
|
||||
|
||||
def _lineTo(self, pt):
|
||||
print("%s %s lineto" % (pt[0], pt[1]))
|
||||
|
||||
def _curveToOne(self, bcp1, bcp2, pt):
|
||||
print(
|
||||
"%s %s %s %s %s %s curveto"
|
||||
% (bcp1[0], bcp1[1], bcp2[0], bcp2[1], pt[0], pt[1])
|
||||
)
|
||||
|
||||
def _closePath(self):
|
||||
print("closepath")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pen = _TestPen(None)
|
||||
pen.moveTo((0, 0))
|
||||
pen.lineTo((0, 100))
|
||||
pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
|
||||
pen.closePath()
|
||||
|
||||
pen = _TestPen(None)
|
||||
# testing the "no on-curve point" scenario
|
||||
pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
|
||||
pen.closePath()
|
||||
@@ -1,98 +0,0 @@
|
||||
from fontTools.misc.arrayTools import updateBounds, pointInRect, unionRect
|
||||
from fontTools.misc.bezierTools import calcCubicBounds, calcQuadraticBounds
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["BoundsPen", "ControlBoundsPen"]
|
||||
|
||||
|
||||
class ControlBoundsPen(BasePen):
|
||||
"""Pen to calculate the "control bounds" of a shape. This is the
|
||||
bounding box of all control points, so may be larger than the
|
||||
actual bounding box if there are curves that don't have points
|
||||
on their extremes.
|
||||
|
||||
When the shape has been drawn, the bounds are available as the
|
||||
``bounds`` attribute of the pen object. It's a 4-tuple::
|
||||
|
||||
(xMin, yMin, xMax, yMax).
|
||||
|
||||
If ``ignoreSinglePoints`` is True, single points are ignored.
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet, ignoreSinglePoints=False):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self.ignoreSinglePoints = ignoreSinglePoints
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
self.bounds = None
|
||||
self._start = None
|
||||
|
||||
def _moveTo(self, pt):
|
||||
self._start = pt
|
||||
if not self.ignoreSinglePoints:
|
||||
self._addMoveTo()
|
||||
|
||||
def _addMoveTo(self):
|
||||
if self._start is None:
|
||||
return
|
||||
bounds = self.bounds
|
||||
if bounds:
|
||||
self.bounds = updateBounds(bounds, self._start)
|
||||
else:
|
||||
x, y = self._start
|
||||
self.bounds = (x, y, x, y)
|
||||
self._start = None
|
||||
|
||||
def _lineTo(self, pt):
|
||||
self._addMoveTo()
|
||||
self.bounds = updateBounds(self.bounds, pt)
|
||||
|
||||
def _curveToOne(self, bcp1, bcp2, pt):
|
||||
self._addMoveTo()
|
||||
bounds = self.bounds
|
||||
bounds = updateBounds(bounds, bcp1)
|
||||
bounds = updateBounds(bounds, bcp2)
|
||||
bounds = updateBounds(bounds, pt)
|
||||
self.bounds = bounds
|
||||
|
||||
def _qCurveToOne(self, bcp, pt):
|
||||
self._addMoveTo()
|
||||
bounds = self.bounds
|
||||
bounds = updateBounds(bounds, bcp)
|
||||
bounds = updateBounds(bounds, pt)
|
||||
self.bounds = bounds
|
||||
|
||||
|
||||
class BoundsPen(ControlBoundsPen):
|
||||
"""Pen to calculate the bounds of a shape. It calculates the
|
||||
correct bounds even when the shape contains curves that don't
|
||||
have points on their extremes. This is somewhat slower to compute
|
||||
than the "control bounds".
|
||||
|
||||
When the shape has been drawn, the bounds are available as the
|
||||
``bounds`` attribute of the pen object. It's a 4-tuple::
|
||||
|
||||
(xMin, yMin, xMax, yMax)
|
||||
"""
|
||||
|
||||
def _curveToOne(self, bcp1, bcp2, pt):
|
||||
self._addMoveTo()
|
||||
bounds = self.bounds
|
||||
bounds = updateBounds(bounds, pt)
|
||||
if not pointInRect(bcp1, bounds) or not pointInRect(bcp2, bounds):
|
||||
bounds = unionRect(
|
||||
bounds, calcCubicBounds(self._getCurrentPoint(), bcp1, bcp2, pt)
|
||||
)
|
||||
self.bounds = bounds
|
||||
|
||||
def _qCurveToOne(self, bcp, pt):
|
||||
self._addMoveTo()
|
||||
bounds = self.bounds
|
||||
bounds = updateBounds(bounds, pt)
|
||||
if not pointInRect(bcp, bounds):
|
||||
bounds = unionRect(
|
||||
bounds, calcQuadraticBounds(self._getCurrentPoint(), bcp, pt)
|
||||
)
|
||||
self.bounds = bounds
|
||||
@@ -1,26 +0,0 @@
|
||||
"""Pen to draw to a Cairo graphics library context."""
|
||||
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["CairoPen"]
|
||||
|
||||
|
||||
class CairoPen(BasePen):
|
||||
"""Pen to draw to a Cairo graphics library context."""
|
||||
|
||||
def __init__(self, glyphSet, context):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self.context = context
|
||||
|
||||
def _moveTo(self, p):
|
||||
self.context.move_to(*p)
|
||||
|
||||
def _lineTo(self, p):
|
||||
self.context.line_to(*p)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
self.context.curve_to(*p1, *p2, *p3)
|
||||
|
||||
def _closePath(self):
|
||||
self.context.close_path()
|
||||
@@ -1,26 +0,0 @@
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["CocoaPen"]
|
||||
|
||||
|
||||
class CocoaPen(BasePen):
|
||||
def __init__(self, glyphSet, path=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
from AppKit import NSBezierPath
|
||||
|
||||
path = NSBezierPath.bezierPath()
|
||||
self.path = path
|
||||
|
||||
def _moveTo(self, p):
|
||||
self.path.moveToPoint_(p)
|
||||
|
||||
def _lineTo(self, p):
|
||||
self.path.lineToPoint_(p)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
self.path.curveToPoint_controlPoint1_controlPoint2_(p3, p1, p2)
|
||||
|
||||
def _closePath(self):
|
||||
self.path.closePath()
|
||||
@@ -1,325 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import operator
|
||||
from fontTools.cu2qu import curve_to_quadratic, curves_to_quadratic
|
||||
from fontTools.pens.basePen import decomposeSuperBezierSegment
|
||||
from fontTools.pens.filterPen import FilterPen
|
||||
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||
from fontTools.pens.pointPen import BasePointToSegmentPen
|
||||
from fontTools.pens.pointPen import ReverseContourPointPen
|
||||
|
||||
|
||||
class Cu2QuPen(FilterPen):
|
||||
"""A filter pen to convert cubic bezier curves to quadratic b-splines
|
||||
using the FontTools SegmentPen protocol.
|
||||
|
||||
Args:
|
||||
|
||||
other_pen: another SegmentPen used to draw the transformed outline.
|
||||
max_err: maximum approximation error in font units. For optimal results,
|
||||
if you know the UPEM of the font, we recommend setting this to a
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: flip the contours' direction but keep starting point.
|
||||
stats: a dictionary counting the point numbers of quadratic segments.
|
||||
all_quadratic: if True (default), only quadratic b-splines are generated.
|
||||
if False, quadratic curves or cubic curves are generated depending
|
||||
on which one is more economical.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
other_pen,
|
||||
max_err,
|
||||
reverse_direction=False,
|
||||
stats=None,
|
||||
all_quadratic=True,
|
||||
):
|
||||
if reverse_direction:
|
||||
other_pen = ReverseContourPen(other_pen)
|
||||
super().__init__(other_pen)
|
||||
self.max_err = max_err
|
||||
self.stats = stats
|
||||
self.all_quadratic = all_quadratic
|
||||
|
||||
def _convert_curve(self, pt1, pt2, pt3):
|
||||
curve = (self.current_pt, pt1, pt2, pt3)
|
||||
result = curve_to_quadratic(curve, self.max_err, self.all_quadratic)
|
||||
if self.stats is not None:
|
||||
n = str(len(result) - 2)
|
||||
self.stats[n] = self.stats.get(n, 0) + 1
|
||||
if self.all_quadratic:
|
||||
self.qCurveTo(*result[1:])
|
||||
else:
|
||||
if len(result) == 3:
|
||||
self.qCurveTo(*result[1:])
|
||||
else:
|
||||
assert len(result) == 4
|
||||
super().curveTo(*result[1:])
|
||||
|
||||
def curveTo(self, *points):
|
||||
n = len(points)
|
||||
if n == 3:
|
||||
# this is the most common case, so we special-case it
|
||||
self._convert_curve(*points)
|
||||
elif n > 3:
|
||||
for segment in decomposeSuperBezierSegment(points):
|
||||
self._convert_curve(*segment)
|
||||
else:
|
||||
self.qCurveTo(*points)
|
||||
|
||||
|
||||
class Cu2QuPointPen(BasePointToSegmentPen):
|
||||
"""A filter pen to convert cubic bezier curves to quadratic b-splines
|
||||
using the FontTools PointPen protocol.
|
||||
|
||||
Args:
|
||||
other_point_pen: another PointPen used to draw the transformed outline.
|
||||
max_err: maximum approximation error in font units. For optimal results,
|
||||
if you know the UPEM of the font, we recommend setting this to a
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: reverse the winding direction of all contours.
|
||||
stats: a dictionary counting the point numbers of quadratic segments.
|
||||
all_quadratic: if True (default), only quadratic b-splines are generated.
|
||||
if False, quadratic curves or cubic curves are generated depending
|
||||
on which one is more economical.
|
||||
"""
|
||||
|
||||
__points_required = {
|
||||
"move": (1, operator.eq),
|
||||
"line": (1, operator.eq),
|
||||
"qcurve": (2, operator.ge),
|
||||
"curve": (3, operator.eq),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
other_point_pen,
|
||||
max_err,
|
||||
reverse_direction=False,
|
||||
stats=None,
|
||||
all_quadratic=True,
|
||||
):
|
||||
BasePointToSegmentPen.__init__(self)
|
||||
if reverse_direction:
|
||||
self.pen = ReverseContourPointPen(other_point_pen)
|
||||
else:
|
||||
self.pen = other_point_pen
|
||||
self.max_err = max_err
|
||||
self.stats = stats
|
||||
self.all_quadratic = all_quadratic
|
||||
|
||||
def _flushContour(self, segments):
|
||||
assert len(segments) >= 1
|
||||
closed = segments[0][0] != "move"
|
||||
new_segments = []
|
||||
prev_points = segments[-1][1]
|
||||
prev_on_curve = prev_points[-1][0]
|
||||
for segment_type, points in segments:
|
||||
if segment_type == "curve":
|
||||
for sub_points in self._split_super_bezier_segments(points):
|
||||
on_curve, smooth, name, kwargs = sub_points[-1]
|
||||
bcp1, bcp2 = sub_points[0][0], sub_points[1][0]
|
||||
cubic = [prev_on_curve, bcp1, bcp2, on_curve]
|
||||
quad = curve_to_quadratic(cubic, self.max_err, self.all_quadratic)
|
||||
if self.stats is not None:
|
||||
n = str(len(quad) - 2)
|
||||
self.stats[n] = self.stats.get(n, 0) + 1
|
||||
new_points = [(pt, False, None, {}) for pt in quad[1:-1]]
|
||||
new_points.append((on_curve, smooth, name, kwargs))
|
||||
if self.all_quadratic or len(new_points) == 2:
|
||||
new_segments.append(["qcurve", new_points])
|
||||
else:
|
||||
new_segments.append(["curve", new_points])
|
||||
prev_on_curve = sub_points[-1][0]
|
||||
else:
|
||||
new_segments.append([segment_type, points])
|
||||
prev_on_curve = points[-1][0]
|
||||
if closed:
|
||||
# the BasePointToSegmentPen.endPath method that calls _flushContour
|
||||
# rotates the point list of closed contours so that they end with
|
||||
# the first on-curve point. We restore the original starting point.
|
||||
new_segments = new_segments[-1:] + new_segments[:-1]
|
||||
self._drawPoints(new_segments)
|
||||
|
||||
def _split_super_bezier_segments(self, points):
|
||||
sub_segments = []
|
||||
# n is the number of control points
|
||||
n = len(points) - 1
|
||||
if n == 2:
|
||||
# a simple bezier curve segment
|
||||
sub_segments.append(points)
|
||||
elif n > 2:
|
||||
# a "super" bezier; decompose it
|
||||
on_curve, smooth, name, kwargs = points[-1]
|
||||
num_sub_segments = n - 1
|
||||
for i, sub_points in enumerate(
|
||||
decomposeSuperBezierSegment([pt for pt, _, _, _ in points])
|
||||
):
|
||||
new_segment = []
|
||||
for point in sub_points[:-1]:
|
||||
new_segment.append((point, False, None, {}))
|
||||
if i == (num_sub_segments - 1):
|
||||
# the last on-curve keeps its original attributes
|
||||
new_segment.append((on_curve, smooth, name, kwargs))
|
||||
else:
|
||||
# on-curves of sub-segments are always "smooth"
|
||||
new_segment.append((sub_points[-1], True, None, {}))
|
||||
sub_segments.append(new_segment)
|
||||
else:
|
||||
raise AssertionError("expected 2 control points, found: %d" % n)
|
||||
return sub_segments
|
||||
|
||||
def _drawPoints(self, segments):
|
||||
pen = self.pen
|
||||
pen.beginPath()
|
||||
last_offcurves = []
|
||||
points_required = self.__points_required
|
||||
for i, (segment_type, points) in enumerate(segments):
|
||||
if segment_type in points_required:
|
||||
n, op = points_required[segment_type]
|
||||
assert op(len(points), n), (
|
||||
f"illegal {segment_type!r} segment point count: "
|
||||
f"expected {n}, got {len(points)}"
|
||||
)
|
||||
offcurves = points[:-1]
|
||||
if i == 0:
|
||||
# any off-curve points preceding the first on-curve
|
||||
# will be appended at the end of the contour
|
||||
last_offcurves = offcurves
|
||||
else:
|
||||
for pt, smooth, name, kwargs in offcurves:
|
||||
pen.addPoint(pt, None, smooth, name, **kwargs)
|
||||
pt, smooth, name, kwargs = points[-1]
|
||||
if pt is None:
|
||||
assert segment_type == "qcurve"
|
||||
# special quadratic contour with no on-curve points:
|
||||
# we need to skip the "None" point. See also the Pen
|
||||
# protocol's qCurveTo() method and fontTools.pens.basePen
|
||||
pass
|
||||
else:
|
||||
pen.addPoint(pt, segment_type, smooth, name, **kwargs)
|
||||
else:
|
||||
raise AssertionError("unexpected segment type: %r" % segment_type)
|
||||
for pt, smooth, name, kwargs in last_offcurves:
|
||||
pen.addPoint(pt, None, smooth, name, **kwargs)
|
||||
pen.endPath()
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation):
|
||||
assert self.currentPath is None
|
||||
self.pen.addComponent(baseGlyphName, transformation)
|
||||
|
||||
|
||||
class Cu2QuMultiPen:
|
||||
"""A filter multi-pen to convert cubic bezier curves to quadratic b-splines
|
||||
in a interpolation-compatible manner, using the FontTools SegmentPen protocol.
|
||||
|
||||
Args:
|
||||
|
||||
other_pens: list of SegmentPens used to draw the transformed outlines.
|
||||
max_err: maximum approximation error in font units. For optimal results,
|
||||
if you know the UPEM of the font, we recommend setting this to a
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: flip the contours' direction but keep starting point.
|
||||
|
||||
This pen does not follow the normal SegmentPen protocol. Instead, its
|
||||
moveTo/lineTo/qCurveTo/curveTo methods take a list of tuples that are
|
||||
arguments that would normally be passed to a SegmentPen, one item for
|
||||
each of the pens in other_pens.
|
||||
"""
|
||||
|
||||
# TODO Simplify like 3e8ebcdce592fe8a59ca4c3a294cc9724351e1ce
|
||||
# Remove start_pts and _add_moveTO
|
||||
|
||||
def __init__(self, other_pens, max_err, reverse_direction=False):
|
||||
if reverse_direction:
|
||||
other_pens = [
|
||||
ReverseContourPen(pen, outputImpliedClosingLine=True)
|
||||
for pen in other_pens
|
||||
]
|
||||
self.pens = other_pens
|
||||
self.max_err = max_err
|
||||
self.start_pts = None
|
||||
self.current_pts = None
|
||||
|
||||
def _check_contour_is_open(self):
|
||||
if self.current_pts is None:
|
||||
raise AssertionError("moveTo is required")
|
||||
|
||||
def _check_contour_is_closed(self):
|
||||
if self.current_pts is not None:
|
||||
raise AssertionError("closePath or endPath is required")
|
||||
|
||||
def _add_moveTo(self):
|
||||
if self.start_pts is not None:
|
||||
for pt, pen in zip(self.start_pts, self.pens):
|
||||
pen.moveTo(*pt)
|
||||
self.start_pts = None
|
||||
|
||||
def moveTo(self, pts):
|
||||
self._check_contour_is_closed()
|
||||
self.start_pts = self.current_pts = pts
|
||||
self._add_moveTo()
|
||||
|
||||
def lineTo(self, pts):
|
||||
self._check_contour_is_open()
|
||||
self._add_moveTo()
|
||||
for pt, pen in zip(pts, self.pens):
|
||||
pen.lineTo(*pt)
|
||||
self.current_pts = pts
|
||||
|
||||
def qCurveTo(self, pointsList):
|
||||
self._check_contour_is_open()
|
||||
if len(pointsList[0]) == 1:
|
||||
self.lineTo([(points[0],) for points in pointsList])
|
||||
return
|
||||
self._add_moveTo()
|
||||
current_pts = []
|
||||
for points, pen in zip(pointsList, self.pens):
|
||||
pen.qCurveTo(*points)
|
||||
current_pts.append((points[-1],))
|
||||
self.current_pts = current_pts
|
||||
|
||||
def _curves_to_quadratic(self, pointsList):
|
||||
curves = []
|
||||
for current_pt, points in zip(self.current_pts, pointsList):
|
||||
curves.append(current_pt + points)
|
||||
quadratics = curves_to_quadratic(curves, [self.max_err] * len(curves))
|
||||
pointsList = []
|
||||
for quadratic in quadratics:
|
||||
pointsList.append(quadratic[1:])
|
||||
self.qCurveTo(pointsList)
|
||||
|
||||
def curveTo(self, pointsList):
|
||||
self._check_contour_is_open()
|
||||
self._curves_to_quadratic(pointsList)
|
||||
|
||||
def closePath(self):
|
||||
self._check_contour_is_open()
|
||||
if self.start_pts is None:
|
||||
for pen in self.pens:
|
||||
pen.closePath()
|
||||
self.current_pts = self.start_pts = None
|
||||
|
||||
def endPath(self):
|
||||
self._check_contour_is_open()
|
||||
if self.start_pts is None:
|
||||
for pen in self.pens:
|
||||
pen.endPath()
|
||||
self.current_pts = self.start_pts = None
|
||||
|
||||
def addComponent(self, glyphName, transformations):
|
||||
self._check_contour_is_closed()
|
||||
for trans, pen in zip(transformations, self.pens):
|
||||
pen.addComponent(glyphName, trans)
|
||||
@@ -1,101 +0,0 @@
|
||||
from fontTools.pens.filterPen import ContourFilterPen
|
||||
|
||||
|
||||
class ExplicitClosingLinePen(ContourFilterPen):
|
||||
"""A filter pen that adds an explicit lineTo to the first point of each closed
|
||||
contour if the end point of the last segment is not already the same as the first point.
|
||||
Otherwise, it passes the contour through unchanged.
|
||||
|
||||
>>> from pprint import pprint
|
||||
>>> from fontTools.pens.recordingPen import RecordingPen
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((100, 0))
|
||||
>>> pen.lineTo((100, 100))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('lineTo', ((100, 0),)),
|
||||
('lineTo', ((100, 100),)),
|
||||
('lineTo', ((0, 0),)),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((100, 0))
|
||||
>>> pen.lineTo((100, 100))
|
||||
>>> pen.lineTo((0, 0))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('lineTo', ((100, 0),)),
|
||||
('lineTo', ((100, 100),)),
|
||||
('lineTo', ((0, 0),)),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.curveTo((100, 0), (0, 100), (100, 100))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('curveTo', ((100, 0), (0, 100), (100, 100))),
|
||||
('lineTo', ((0, 0),)),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.curveTo((100, 0), (0, 100), (100, 100))
|
||||
>>> pen.lineTo((0, 0))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('curveTo', ((100, 0), (0, 100), (100, 100))),
|
||||
('lineTo', ((0, 0),)),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.curveTo((100, 0), (0, 100), (0, 0))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('curveTo', ((100, 0), (0, 100), (0, 0))),
|
||||
('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)), ('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.closePath()
|
||||
>>> pprint(rec.value)
|
||||
[('closePath', ())]
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = ExplicitClosingLinePen(rec)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((100, 0))
|
||||
>>> pen.lineTo((100, 100))
|
||||
>>> pen.endPath()
|
||||
>>> pprint(rec.value)
|
||||
[('moveTo', ((0, 0),)),
|
||||
('lineTo', ((100, 0),)),
|
||||
('lineTo', ((100, 100),)),
|
||||
('endPath', ())]
|
||||
"""
|
||||
|
||||
def filterContour(self, contour):
|
||||
if (
|
||||
not contour
|
||||
or contour[0][0] != "moveTo"
|
||||
or contour[-1][0] != "closePath"
|
||||
or len(contour) < 3
|
||||
):
|
||||
return
|
||||
movePt = contour[0][1][0]
|
||||
lastSeg = contour[-2][1]
|
||||
if lastSeg and movePt != lastSeg[-1]:
|
||||
contour[-1:] = [("lineTo", (movePt,)), ("closePath", ())]
|
||||
@@ -1,241 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fontTools.pens.basePen import AbstractPen, DecomposingPen
|
||||
from fontTools.pens.pointPen import AbstractPointPen, DecomposingPointPen
|
||||
from fontTools.pens.recordingPen import RecordingPen
|
||||
|
||||
|
||||
class _PassThruComponentsMixin(object):
|
||||
def addComponent(self, glyphName, transformation, **kwargs):
|
||||
self._outPen.addComponent(glyphName, transformation, **kwargs)
|
||||
|
||||
|
||||
class FilterPen(_PassThruComponentsMixin, AbstractPen):
|
||||
"""Base class for pens that apply some transformation to the coordinates
|
||||
they receive and pass them to another pen.
|
||||
|
||||
You can override any of its methods. The default implementation does
|
||||
nothing, but passes the commands unmodified to the other pen.
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPen
|
||||
>>> rec = RecordingPen()
|
||||
>>> pen = FilterPen(rec)
|
||||
>>> v = iter(rec.value)
|
||||
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> next(v)
|
||||
('moveTo', ((0, 0),))
|
||||
|
||||
>>> pen.lineTo((1, 1))
|
||||
>>> next(v)
|
||||
('lineTo', ((1, 1),))
|
||||
|
||||
>>> pen.curveTo((2, 2), (3, 3), (4, 4))
|
||||
>>> next(v)
|
||||
('curveTo', ((2, 2), (3, 3), (4, 4)))
|
||||
|
||||
>>> pen.qCurveTo((5, 5), (6, 6), (7, 7), (8, 8))
|
||||
>>> next(v)
|
||||
('qCurveTo', ((5, 5), (6, 6), (7, 7), (8, 8)))
|
||||
|
||||
>>> pen.closePath()
|
||||
>>> next(v)
|
||||
('closePath', ())
|
||||
|
||||
>>> pen.moveTo((9, 9))
|
||||
>>> next(v)
|
||||
('moveTo', ((9, 9),))
|
||||
|
||||
>>> pen.endPath()
|
||||
>>> next(v)
|
||||
('endPath', ())
|
||||
|
||||
>>> pen.addComponent('foo', (1, 0, 0, 1, 0, 0))
|
||||
>>> next(v)
|
||||
('addComponent', ('foo', (1, 0, 0, 1, 0, 0)))
|
||||
"""
|
||||
|
||||
def __init__(self, outPen):
|
||||
self._outPen = outPen
|
||||
self.current_pt = None
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._outPen.moveTo(pt)
|
||||
self.current_pt = pt
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._outPen.lineTo(pt)
|
||||
self.current_pt = pt
|
||||
|
||||
def curveTo(self, *points):
|
||||
self._outPen.curveTo(*points)
|
||||
self.current_pt = points[-1]
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
self._outPen.qCurveTo(*points)
|
||||
self.current_pt = points[-1]
|
||||
|
||||
def closePath(self):
|
||||
self._outPen.closePath()
|
||||
self.current_pt = None
|
||||
|
||||
def endPath(self):
|
||||
self._outPen.endPath()
|
||||
self.current_pt = None
|
||||
|
||||
|
||||
class ContourFilterPen(_PassThruComponentsMixin, RecordingPen):
|
||||
"""A "buffered" filter pen that accumulates contour data, passes
|
||||
it through a ``filterContour`` method when the contour is closed or ended,
|
||||
and finally draws the result with the output pen.
|
||||
|
||||
Components are passed through unchanged.
|
||||
"""
|
||||
|
||||
def __init__(self, outPen):
|
||||
super(ContourFilterPen, self).__init__()
|
||||
self._outPen = outPen
|
||||
|
||||
def closePath(self):
|
||||
super(ContourFilterPen, self).closePath()
|
||||
self._flushContour()
|
||||
|
||||
def endPath(self):
|
||||
super(ContourFilterPen, self).endPath()
|
||||
self._flushContour()
|
||||
|
||||
def _flushContour(self):
|
||||
result = self.filterContour(self.value)
|
||||
if result is not None:
|
||||
self.value = result
|
||||
self.replay(self._outPen)
|
||||
self.value = []
|
||||
|
||||
def filterContour(self, contour):
|
||||
"""Subclasses must override this to perform the filtering.
|
||||
|
||||
The contour is a list of pen (operator, operands) tuples.
|
||||
Operators are strings corresponding to the AbstractPen methods:
|
||||
"moveTo", "lineTo", "curveTo", "qCurveTo", "closePath" and
|
||||
"endPath". The operands are the positional arguments that are
|
||||
passed to each method.
|
||||
|
||||
If the method doesn't return a value (i.e. returns None), it's
|
||||
assumed that the argument was modified in-place.
|
||||
Otherwise, the return value is drawn with the output pen.
|
||||
"""
|
||||
return # or return contour
|
||||
|
||||
|
||||
class FilterPointPen(_PassThruComponentsMixin, AbstractPointPen):
|
||||
"""Baseclass for point pens that apply some transformation to the
|
||||
coordinates they receive and pass them to another point pen.
|
||||
|
||||
You can override any of its methods. The default implementation does
|
||||
nothing, but passes the commands unmodified to the other pen.
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPointPen
|
||||
>>> rec = RecordingPointPen()
|
||||
>>> pen = FilterPointPen(rec)
|
||||
>>> v = iter(rec.value)
|
||||
>>> pen.beginPath(identifier="abc")
|
||||
>>> next(v)
|
||||
('beginPath', (), {'identifier': 'abc'})
|
||||
>>> pen.addPoint((1, 2), "line", False)
|
||||
>>> next(v)
|
||||
('addPoint', ((1, 2), 'line', False, None), {})
|
||||
>>> pen.addComponent("a", (2, 0, 0, 2, 10, -10), identifier="0001")
|
||||
>>> next(v)
|
||||
('addComponent', ('a', (2, 0, 0, 2, 10, -10)), {'identifier': '0001'})
|
||||
>>> pen.endPath()
|
||||
>>> next(v)
|
||||
('endPath', (), {})
|
||||
"""
|
||||
|
||||
def __init__(self, outPen):
|
||||
self._outPen = outPen
|
||||
|
||||
def beginPath(self, **kwargs):
|
||||
self._outPen.beginPath(**kwargs)
|
||||
|
||||
def endPath(self):
|
||||
self._outPen.endPath()
|
||||
|
||||
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
|
||||
self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
|
||||
|
||||
|
||||
class _DecomposingFilterPenMixin:
|
||||
"""Mixin class that decomposes components as regular contours.
|
||||
|
||||
Shared by both DecomposingFilterPen and DecomposingFilterPointPen.
|
||||
|
||||
Takes two required parameters, another (segment or point) pen 'outPen' to draw
|
||||
with, and a 'glyphSet' dict of drawable glyph objects to draw components from.
|
||||
|
||||
The 'skipMissingComponents' and 'reverseFlipped' optional arguments work the
|
||||
same as in the DecomposingPen/DecomposingPointPen. Both are False by default.
|
||||
|
||||
In addition, the decomposing filter pens also take the following two options:
|
||||
|
||||
'include' is an optional set of component base glyph names to consider for
|
||||
decomposition; the default include=None means decompose all components no matter
|
||||
the base glyph name).
|
||||
|
||||
'decomposeNested' (bool) controls whether to recurse decomposition into nested
|
||||
components of components (this only matters when 'include' was also provided);
|
||||
if False, only decompose top-level components included in the set, but not
|
||||
also their children.
|
||||
"""
|
||||
|
||||
# raises MissingComponentError if base glyph is not found in glyphSet
|
||||
skipMissingComponents = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
outPen,
|
||||
glyphSet,
|
||||
skipMissingComponents=None,
|
||||
reverseFlipped=False,
|
||||
include: set[str] | None = None,
|
||||
decomposeNested: bool = True,
|
||||
):
|
||||
super().__init__(
|
||||
outPen=outPen,
|
||||
glyphSet=glyphSet,
|
||||
skipMissingComponents=skipMissingComponents,
|
||||
reverseFlipped=reverseFlipped,
|
||||
)
|
||||
self.include = include
|
||||
self.decomposeNested = decomposeNested
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, **kwargs):
|
||||
# only decompose the component if it's included in the set
|
||||
if self.include is None or baseGlyphName in self.include:
|
||||
# if we're decomposing nested components, temporarily set include to None
|
||||
include_bak = self.include
|
||||
if self.decomposeNested and self.include:
|
||||
self.include = None
|
||||
try:
|
||||
super().addComponent(baseGlyphName, transformation, **kwargs)
|
||||
finally:
|
||||
if self.include != include_bak:
|
||||
self.include = include_bak
|
||||
else:
|
||||
_PassThruComponentsMixin.addComponent(
|
||||
self, baseGlyphName, transformation, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class DecomposingFilterPen(_DecomposingFilterPenMixin, DecomposingPen, FilterPen):
|
||||
"""Filter pen that draws components as regular contours."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DecomposingFilterPointPen(
|
||||
_DecomposingFilterPenMixin, DecomposingPointPen, FilterPointPen
|
||||
):
|
||||
"""Filter point pen that draws components as regular contours."""
|
||||
|
||||
pass
|
||||
@@ -1,458 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Pen to rasterize paths with FreeType."""
|
||||
|
||||
__all__ = ["FreeTypePen"]
|
||||
|
||||
import os
|
||||
import ctypes
|
||||
import platform
|
||||
import subprocess
|
||||
import collections
|
||||
import math
|
||||
|
||||
import freetype
|
||||
from freetype.raw import FT_Outline_Get_Bitmap, FT_Outline_Get_BBox, FT_Outline_Get_CBox
|
||||
from freetype.ft_types import FT_Pos
|
||||
from freetype.ft_structs import FT_Vector, FT_BBox, FT_Bitmap, FT_Outline
|
||||
from freetype.ft_enums import (
|
||||
FT_OUTLINE_NONE,
|
||||
FT_OUTLINE_EVEN_ODD_FILL,
|
||||
FT_PIXEL_MODE_GRAY,
|
||||
FT_CURVE_TAG_ON,
|
||||
FT_CURVE_TAG_CONIC,
|
||||
FT_CURVE_TAG_CUBIC,
|
||||
)
|
||||
from freetype.ft_errors import FT_Exception
|
||||
|
||||
from fontTools.pens.basePen import BasePen, PenError
|
||||
from fontTools.misc.roundTools import otRound
|
||||
from fontTools.misc.transform import Transform
|
||||
|
||||
Contour = collections.namedtuple("Contour", ("points", "tags"))
|
||||
|
||||
|
||||
class FreeTypePen(BasePen):
|
||||
"""Pen to rasterize paths with FreeType. Requires `freetype-py` module.
|
||||
|
||||
Constructs ``FT_Outline`` from the paths, and renders it within a bitmap
|
||||
buffer.
|
||||
|
||||
For ``array()`` and ``show()``, `numpy` and `matplotlib` must be installed.
|
||||
For ``image()``, `Pillow` is required. Each module is lazily loaded when the
|
||||
corresponding method is called.
|
||||
|
||||
Args:
|
||||
glyphSet: a dictionary of drawable glyph objects keyed by name
|
||||
used to resolve component references in composite glyphs.
|
||||
|
||||
:Examples:
|
||||
If `numpy` and `matplotlib` is available, the following code will
|
||||
show the glyph image of `fi` in a new window::
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.freetypePen import FreeTypePen
|
||||
from fontTools.misc.transform import Offset
|
||||
pen = FreeTypePen(None)
|
||||
font = TTFont('SourceSansPro-Regular.otf')
|
||||
glyph = font.getGlyphSet()['fi']
|
||||
glyph.draw(pen)
|
||||
width, ascender, descender = glyph.width, font['OS/2'].usWinAscent, -font['OS/2'].usWinDescent
|
||||
height = ascender - descender
|
||||
pen.show(width=width, height=height, transform=Offset(0, -descender))
|
||||
|
||||
Combining with `uharfbuzz`, you can typeset a chunk of glyphs in a pen::
|
||||
|
||||
import uharfbuzz as hb
|
||||
from fontTools.pens.freetypePen import FreeTypePen
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.misc.transform import Offset
|
||||
|
||||
en1, en2, ar, ja = 'Typesetting', 'Jeff', 'صف الحروف', 'たいぷせっと'
|
||||
for text, font_path, direction, typo_ascender, typo_descender, vhea_ascender, vhea_descender, contain, features in (
|
||||
(en1, 'NotoSans-Regular.ttf', 'ltr', 2189, -600, None, None, False, {"kern": True, "liga": True}),
|
||||
(en2, 'NotoSans-Regular.ttf', 'ltr', 2189, -600, None, None, True, {"kern": True, "liga": True}),
|
||||
(ar, 'NotoSansArabic-Regular.ttf', 'rtl', 1374, -738, None, None, False, {"kern": True, "liga": True}),
|
||||
(ja, 'NotoSansJP-Regular.otf', 'ltr', 880, -120, 500, -500, False, {"palt": True, "kern": True}),
|
||||
(ja, 'NotoSansJP-Regular.otf', 'ttb', 880, -120, 500, -500, False, {"vert": True, "vpal": True, "vkrn": True})
|
||||
):
|
||||
blob = hb.Blob.from_file_path(font_path)
|
||||
face = hb.Face(blob)
|
||||
font = hb.Font(face)
|
||||
buf = hb.Buffer()
|
||||
buf.direction = direction
|
||||
buf.add_str(text)
|
||||
buf.guess_segment_properties()
|
||||
hb.shape(font, buf, features)
|
||||
|
||||
x, y = 0, 0
|
||||
pen = FreeTypePen(None)
|
||||
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
|
||||
gid = info.codepoint
|
||||
transformed = TransformPen(pen, Offset(x + pos.x_offset, y + pos.y_offset))
|
||||
font.draw_glyph_with_pen(gid, transformed)
|
||||
x += pos.x_advance
|
||||
y += pos.y_advance
|
||||
|
||||
offset, width, height = None, None, None
|
||||
if direction in ('ltr', 'rtl'):
|
||||
offset = (0, -typo_descender)
|
||||
width = x
|
||||
height = typo_ascender - typo_descender
|
||||
else:
|
||||
offset = (-vhea_descender, -y)
|
||||
width = vhea_ascender - vhea_descender
|
||||
height = -y
|
||||
pen.show(width=width, height=height, transform=Offset(*offset), contain=contain)
|
||||
|
||||
For Jupyter Notebook, the rendered image will be displayed in a cell if
|
||||
you replace ``show()`` with ``image()`` in the examples.
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self.contours = []
|
||||
|
||||
def outline(self, transform=None, evenOdd=False):
|
||||
"""Converts the current contours to ``FT_Outline``.
|
||||
|
||||
Args:
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
"""
|
||||
transform = transform or Transform()
|
||||
if not hasattr(transform, "transformPoint"):
|
||||
transform = Transform(*transform)
|
||||
n_contours = len(self.contours)
|
||||
n_points = sum((len(contour.points) for contour in self.contours))
|
||||
points = []
|
||||
for contour in self.contours:
|
||||
for point in contour.points:
|
||||
point = transform.transformPoint(point)
|
||||
points.append(
|
||||
FT_Vector(
|
||||
FT_Pos(otRound(point[0] * 64)), FT_Pos(otRound(point[1] * 64))
|
||||
)
|
||||
)
|
||||
tags = []
|
||||
for contour in self.contours:
|
||||
for tag in contour.tags:
|
||||
tags.append(tag)
|
||||
contours = []
|
||||
contours_sum = 0
|
||||
for contour in self.contours:
|
||||
contours_sum += len(contour.points)
|
||||
contours.append(contours_sum - 1)
|
||||
flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE
|
||||
return FT_Outline(
|
||||
(ctypes.c_short)(n_contours),
|
||||
(ctypes.c_short)(n_points),
|
||||
(FT_Vector * n_points)(*points),
|
||||
(ctypes.c_ubyte * n_points)(*tags),
|
||||
(ctypes.c_short * n_contours)(*contours),
|
||||
(ctypes.c_int)(flags),
|
||||
)
|
||||
|
||||
def buffer(
|
||||
self, width=None, height=None, transform=None, contain=False, evenOdd=False
|
||||
):
|
||||
"""Renders the current contours within a bitmap buffer.
|
||||
|
||||
Args:
|
||||
width: Image width of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
height: Image height of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module. The bitmap size is not affected by this matrix.
|
||||
contain: If ``True``, the image size will be automatically expanded
|
||||
so that it fits to the bounding box of the paths. Useful for
|
||||
rendering glyphs with negative sidebearings without clipping.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
|
||||
Returns:
|
||||
A tuple of ``(buffer, size)``, where ``buffer`` is a ``bytes``
|
||||
object of the resulted bitmap and ``size`` is a 2-tuple of its
|
||||
dimension.
|
||||
|
||||
:Notes:
|
||||
The image size should always be given explicitly if you need to get
|
||||
a proper glyph image. When ``width`` and ``height`` are omitted, it
|
||||
forcifully fits to the bounding box and the side bearings get
|
||||
cropped. If you pass ``0`` to both ``width`` and ``height`` and set
|
||||
``contain`` to ``True``, it expands to the bounding box while
|
||||
maintaining the origin of the contours, meaning that LSB will be
|
||||
maintained but RSB won’t. The difference between the two becomes
|
||||
more obvious when rotate or skew transformation is applied.
|
||||
|
||||
:Example:
|
||||
.. code-block::
|
||||
|
||||
>> pen = FreeTypePen(None)
|
||||
>> glyph.draw(pen)
|
||||
>> buf, size = pen.buffer(width=500, height=1000)
|
||||
>> type(buf), len(buf), size
|
||||
(<class 'bytes'>, 500000, (500, 1000))
|
||||
|
||||
"""
|
||||
transform = transform or Transform()
|
||||
if not hasattr(transform, "transformPoint"):
|
||||
transform = Transform(*transform)
|
||||
contain_x, contain_y = contain or width is None, contain or height is None
|
||||
if contain_x or contain_y:
|
||||
dx, dy = transform.dx, transform.dy
|
||||
bbox = self.bbox
|
||||
p1, p2, p3, p4 = (
|
||||
transform.transformPoint((bbox[0], bbox[1])),
|
||||
transform.transformPoint((bbox[2], bbox[1])),
|
||||
transform.transformPoint((bbox[0], bbox[3])),
|
||||
transform.transformPoint((bbox[2], bbox[3])),
|
||||
)
|
||||
px, py = (p1[0], p2[0], p3[0], p4[0]), (p1[1], p2[1], p3[1], p4[1])
|
||||
if contain_x:
|
||||
if width is None:
|
||||
dx = dx - min(*px)
|
||||
width = max(*px) - min(*px)
|
||||
else:
|
||||
dx = dx - min(min(*px), 0.0)
|
||||
width = max(width, max(*px) - min(min(*px), 0.0))
|
||||
if contain_y:
|
||||
if height is None:
|
||||
dy = dy - min(*py)
|
||||
height = max(*py) - min(*py)
|
||||
else:
|
||||
dy = dy - min(min(*py), 0.0)
|
||||
height = max(height, max(*py) - min(min(*py), 0.0))
|
||||
transform = Transform(*transform[:4], dx, dy)
|
||||
width, height = math.ceil(width), math.ceil(height)
|
||||
buf = ctypes.create_string_buffer(width * height)
|
||||
bitmap = FT_Bitmap(
|
||||
(ctypes.c_int)(height),
|
||||
(ctypes.c_int)(width),
|
||||
(ctypes.c_int)(width),
|
||||
(ctypes.POINTER(ctypes.c_ubyte))(buf),
|
||||
(ctypes.c_short)(256),
|
||||
(ctypes.c_ubyte)(FT_PIXEL_MODE_GRAY),
|
||||
(ctypes.c_char)(0),
|
||||
(ctypes.c_void_p)(None),
|
||||
)
|
||||
outline = self.outline(transform=transform, evenOdd=evenOdd)
|
||||
err = FT_Outline_Get_Bitmap(
|
||||
freetype.get_handle(), ctypes.byref(outline), ctypes.byref(bitmap)
|
||||
)
|
||||
if err != 0:
|
||||
raise FT_Exception(err)
|
||||
return buf.raw, (width, height)
|
||||
|
||||
def array(
|
||||
self, width=None, height=None, transform=None, contain=False, evenOdd=False
|
||||
):
|
||||
"""Returns the rendered contours as a numpy array. Requires `numpy`.
|
||||
|
||||
Args:
|
||||
width: Image width of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
height: Image height of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module. The bitmap size is not affected by this matrix.
|
||||
contain: If ``True``, the image size will be automatically expanded
|
||||
so that it fits to the bounding box of the paths. Useful for
|
||||
rendering glyphs with negative sidebearings without clipping.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
|
||||
Returns:
|
||||
A ``numpy.ndarray`` object with a shape of ``(height, width)``.
|
||||
Each element takes a value in the range of ``[0.0, 1.0]``.
|
||||
|
||||
:Notes:
|
||||
The image size should always be given explicitly if you need to get
|
||||
a proper glyph image. When ``width`` and ``height`` are omitted, it
|
||||
forcifully fits to the bounding box and the side bearings get
|
||||
cropped. If you pass ``0`` to both ``width`` and ``height`` and set
|
||||
``contain`` to ``True``, it expands to the bounding box while
|
||||
maintaining the origin of the contours, meaning that LSB will be
|
||||
maintained but RSB won’t. The difference between the two becomes
|
||||
more obvious when rotate or skew transformation is applied.
|
||||
|
||||
:Example:
|
||||
.. code-block::
|
||||
|
||||
>> pen = FreeTypePen(None)
|
||||
>> glyph.draw(pen)
|
||||
>> arr = pen.array(width=500, height=1000)
|
||||
>> type(a), a.shape
|
||||
(<class 'numpy.ndarray'>, (1000, 500))
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
buf, size = self.buffer(
|
||||
width=width,
|
||||
height=height,
|
||||
transform=transform,
|
||||
contain=contain,
|
||||
evenOdd=evenOdd,
|
||||
)
|
||||
return np.frombuffer(buf, "B").reshape((size[1], size[0])) / 255.0
|
||||
|
||||
def show(
|
||||
self, width=None, height=None, transform=None, contain=False, evenOdd=False
|
||||
):
|
||||
"""Plots the rendered contours with `pyplot`. Requires `numpy` and
|
||||
`matplotlib`.
|
||||
|
||||
Args:
|
||||
width: Image width of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
height: Image height of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module. The bitmap size is not affected by this matrix.
|
||||
contain: If ``True``, the image size will be automatically expanded
|
||||
so that it fits to the bounding box of the paths. Useful for
|
||||
rendering glyphs with negative sidebearings without clipping.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
|
||||
:Notes:
|
||||
The image size should always be given explicitly if you need to get
|
||||
a proper glyph image. When ``width`` and ``height`` are omitted, it
|
||||
forcifully fits to the bounding box and the side bearings get
|
||||
cropped. If you pass ``0`` to both ``width`` and ``height`` and set
|
||||
``contain`` to ``True``, it expands to the bounding box while
|
||||
maintaining the origin of the contours, meaning that LSB will be
|
||||
maintained but RSB won’t. The difference between the two becomes
|
||||
more obvious when rotate or skew transformation is applied.
|
||||
|
||||
:Example:
|
||||
.. code-block::
|
||||
|
||||
>> pen = FreeTypePen(None)
|
||||
>> glyph.draw(pen)
|
||||
>> pen.show(width=500, height=1000)
|
||||
"""
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
a = self.array(
|
||||
width=width,
|
||||
height=height,
|
||||
transform=transform,
|
||||
contain=contain,
|
||||
evenOdd=evenOdd,
|
||||
)
|
||||
plt.imshow(a, cmap="gray_r", vmin=0, vmax=1)
|
||||
plt.show()
|
||||
|
||||
def image(
|
||||
self, width=None, height=None, transform=None, contain=False, evenOdd=False
|
||||
):
|
||||
"""Returns the rendered contours as a PIL image. Requires `Pillow`.
|
||||
Can be used to display a glyph image in Jupyter Notebook.
|
||||
|
||||
Args:
|
||||
width: Image width of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
height: Image height of the bitmap in pixels. If omitted, it
|
||||
automatically fits to the bounding box of the contours.
|
||||
transform: An optional 6-tuple containing an affine transformation,
|
||||
or a ``Transform`` object from the ``fontTools.misc.transform``
|
||||
module. The bitmap size is not affected by this matrix.
|
||||
contain: If ``True``, the image size will be automatically expanded
|
||||
so that it fits to the bounding box of the paths. Useful for
|
||||
rendering glyphs with negative sidebearings without clipping.
|
||||
evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
|
||||
|
||||
Returns:
|
||||
A ``PIL.image`` object. The image is filled in black with alpha
|
||||
channel obtained from the rendered bitmap.
|
||||
|
||||
:Notes:
|
||||
The image size should always be given explicitly if you need to get
|
||||
a proper glyph image. When ``width`` and ``height`` are omitted, it
|
||||
forcifully fits to the bounding box and the side bearings get
|
||||
cropped. If you pass ``0`` to both ``width`` and ``height`` and set
|
||||
``contain`` to ``True``, it expands to the bounding box while
|
||||
maintaining the origin of the contours, meaning that LSB will be
|
||||
maintained but RSB won’t. The difference between the two becomes
|
||||
more obvious when rotate or skew transformation is applied.
|
||||
|
||||
:Example:
|
||||
.. code-block::
|
||||
|
||||
>> pen = FreeTypePen(None)
|
||||
>> glyph.draw(pen)
|
||||
>> img = pen.image(width=500, height=1000)
|
||||
>> type(img), img.size
|
||||
(<class 'PIL.Image.Image'>, (500, 1000))
|
||||
"""
|
||||
from PIL import Image
|
||||
|
||||
buf, size = self.buffer(
|
||||
width=width,
|
||||
height=height,
|
||||
transform=transform,
|
||||
contain=contain,
|
||||
evenOdd=evenOdd,
|
||||
)
|
||||
img = Image.new("L", size, 0)
|
||||
img.putalpha(Image.frombuffer("L", size, buf))
|
||||
return img
|
||||
|
||||
@property
|
||||
def bbox(self):
|
||||
"""Computes the exact bounding box of an outline.
|
||||
|
||||
Returns:
|
||||
A tuple of ``(xMin, yMin, xMax, yMax)``.
|
||||
"""
|
||||
bbox = FT_BBox()
|
||||
outline = self.outline()
|
||||
FT_Outline_Get_BBox(ctypes.byref(outline), ctypes.byref(bbox))
|
||||
return (bbox.xMin / 64.0, bbox.yMin / 64.0, bbox.xMax / 64.0, bbox.yMax / 64.0)
|
||||
|
||||
@property
|
||||
def cbox(self):
|
||||
"""Returns an outline's ‘control box’.
|
||||
|
||||
Returns:
|
||||
A tuple of ``(xMin, yMin, xMax, yMax)``.
|
||||
"""
|
||||
cbox = FT_BBox()
|
||||
outline = self.outline()
|
||||
FT_Outline_Get_CBox(ctypes.byref(outline), ctypes.byref(cbox))
|
||||
return (cbox.xMin / 64.0, cbox.yMin / 64.0, cbox.xMax / 64.0, cbox.yMax / 64.0)
|
||||
|
||||
def _moveTo(self, pt):
|
||||
contour = Contour([], [])
|
||||
self.contours.append(contour)
|
||||
contour.points.append(pt)
|
||||
contour.tags.append(FT_CURVE_TAG_ON)
|
||||
|
||||
def _lineTo(self, pt):
|
||||
if not (self.contours and len(self.contours[-1].points) > 0):
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
contour = self.contours[-1]
|
||||
contour.points.append(pt)
|
||||
contour.tags.append(FT_CURVE_TAG_ON)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
if not (self.contours and len(self.contours[-1].points) > 0):
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
t1, t2, t3 = FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_ON
|
||||
contour = self.contours[-1]
|
||||
for p, t in ((p1, t1), (p2, t2), (p3, t3)):
|
||||
contour.points.append(p)
|
||||
contour.tags.append(t)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
if not (self.contours and len(self.contours[-1].points) > 0):
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
t1, t2 = FT_CURVE_TAG_CONIC, FT_CURVE_TAG_ON
|
||||
contour = self.contours[-1]
|
||||
for p, t in ((p1, t1), (p2, t2)):
|
||||
contour.points.append(p)
|
||||
contour.tags.append(t)
|
||||
@@ -1,89 +0,0 @@
|
||||
# Modified from https://github.com/adobe-type-tools/psautohint/blob/08b346865710ed3c172f1eb581d6ef243b203f99/python/psautohint/ufoFont.py#L800-L838
|
||||
import hashlib
|
||||
|
||||
from fontTools.pens.basePen import MissingComponentError
|
||||
from fontTools.pens.pointPen import AbstractPointPen
|
||||
|
||||
|
||||
class HashPointPen(AbstractPointPen):
|
||||
"""
|
||||
This pen can be used to check if a glyph's contents (outlines plus
|
||||
components) have changed.
|
||||
|
||||
Components are added as the original outline plus each composite's
|
||||
transformation.
|
||||
|
||||
Example: You have some TrueType hinting code for a glyph which you want to
|
||||
compile. The hinting code specifies a hash value computed with HashPointPen
|
||||
that was valid for the glyph's outlines at the time the hinting code was
|
||||
written. Now you can calculate the hash for the glyph's current outlines to
|
||||
check if the outlines have changed, which would probably make the hinting
|
||||
code invalid.
|
||||
|
||||
> glyph = ufo[name]
|
||||
> hash_pen = HashPointPen(glyph.width, ufo)
|
||||
> glyph.drawPoints(hash_pen)
|
||||
> ttdata = glyph.lib.get("public.truetype.instructions", None)
|
||||
> stored_hash = ttdata.get("id", None) # The hash is stored in the "id" key
|
||||
> if stored_hash is None or stored_hash != hash_pen.hash:
|
||||
> logger.error(f"Glyph hash mismatch, glyph '{name}' will have no instructions in font.")
|
||||
> else:
|
||||
> # The hash values are identical, the outline has not changed.
|
||||
> # Compile the hinting code ...
|
||||
> pass
|
||||
|
||||
If you want to compare a glyph from a source format which supports floating point
|
||||
coordinates and transformations against a glyph from a format which has restrictions
|
||||
on the precision of floats, e.g. UFO vs. TTF, you must use an appropriate rounding
|
||||
function to make the values comparable. For TTF fonts with composites, this
|
||||
construct can be used to make the transform values conform to F2Dot14:
|
||||
|
||||
> ttf_hash_pen = HashPointPen(ttf_glyph_width, ttFont.getGlyphSet())
|
||||
> ttf_round_pen = RoundingPointPen(ttf_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14))
|
||||
> ufo_hash_pen = HashPointPen(ufo_glyph.width, ufo)
|
||||
> ttf_glyph.drawPoints(ttf_round_pen, ttFont["glyf"])
|
||||
> ufo_round_pen = RoundingPointPen(ufo_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14))
|
||||
> ufo_glyph.drawPoints(ufo_round_pen)
|
||||
> assert ttf_hash_pen.hash == ufo_hash_pen.hash
|
||||
"""
|
||||
|
||||
def __init__(self, glyphWidth=0, glyphSet=None):
|
||||
self.glyphset = glyphSet
|
||||
self.data = ["w%s" % round(glyphWidth, 9)]
|
||||
|
||||
@property
|
||||
def hash(self):
|
||||
data = "".join(self.data)
|
||||
if len(data) >= 128:
|
||||
data = hashlib.sha512(data.encode("ascii")).hexdigest()
|
||||
return data
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
pass
|
||||
|
||||
def endPath(self):
|
||||
self.data.append("|")
|
||||
|
||||
def addPoint(
|
||||
self,
|
||||
pt,
|
||||
segmentType=None,
|
||||
smooth=False,
|
||||
name=None,
|
||||
identifier=None,
|
||||
**kwargs,
|
||||
):
|
||||
if segmentType is None:
|
||||
pt_type = "o" # offcurve
|
||||
else:
|
||||
pt_type = segmentType[0]
|
||||
self.data.append(f"{pt_type}{pt[0]:g}{pt[1]:+g}")
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
|
||||
tr = "".join([f"{t:+}" for t in transformation])
|
||||
self.data.append("[")
|
||||
try:
|
||||
self.glyphset[baseGlyphName].drawPoints(self)
|
||||
except KeyError:
|
||||
raise MissingComponentError(baseGlyphName)
|
||||
self.data.append(f"({tr})]")
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,881 +0,0 @@
|
||||
from fontTools.pens.basePen import BasePen, OpenContourError
|
||||
|
||||
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
|
||||
|
||||
|
||||
__all__ = ["MomentsPen"]
|
||||
|
||||
|
||||
class MomentsPen(BasePen):
|
||||
def __init__(self, glyphset=None):
|
||||
BasePen.__init__(self, glyphset)
|
||||
|
||||
self.area = 0
|
||||
self.momentX = 0
|
||||
self.momentY = 0
|
||||
self.momentXX = 0
|
||||
self.momentXY = 0
|
||||
self.momentYY = 0
|
||||
|
||||
def _moveTo(self, p0):
|
||||
self.__startPoint = p0
|
||||
|
||||
def _closePath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self.__startPoint:
|
||||
self._lineTo(self.__startPoint)
|
||||
|
||||
def _endPath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self.__startPoint:
|
||||
raise OpenContourError("Glyph statistics not defined on open contours.")
|
||||
|
||||
@cython.locals(r0=cython.double)
|
||||
@cython.locals(r1=cython.double)
|
||||
@cython.locals(r2=cython.double)
|
||||
@cython.locals(r3=cython.double)
|
||||
@cython.locals(r4=cython.double)
|
||||
@cython.locals(r5=cython.double)
|
||||
@cython.locals(r6=cython.double)
|
||||
@cython.locals(r7=cython.double)
|
||||
@cython.locals(r8=cython.double)
|
||||
@cython.locals(r9=cython.double)
|
||||
@cython.locals(r10=cython.double)
|
||||
@cython.locals(r11=cython.double)
|
||||
@cython.locals(r12=cython.double)
|
||||
@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
|
||||
|
||||
r0 = x1 * y0
|
||||
r1 = x1 * y1
|
||||
r2 = x1**2
|
||||
r3 = r2 * y1
|
||||
r4 = y0 - y1
|
||||
r5 = r4 * x0
|
||||
r6 = x0**2
|
||||
r7 = 2 * y0
|
||||
r8 = y0**2
|
||||
r9 = y1**2
|
||||
r10 = x1**3
|
||||
r11 = y0**3
|
||||
r12 = y1**3
|
||||
|
||||
self.area += -r0 / 2 - r1 / 2 + x0 * (y0 + y1) / 2
|
||||
self.momentX += -r2 * y0 / 6 - r3 / 3 - r5 * x1 / 6 + r6 * (r7 + y1) / 6
|
||||
self.momentY += (
|
||||
-r0 * y1 / 6 - r8 * x1 / 6 - r9 * x1 / 6 + x0 * (r8 + r9 + y0 * y1) / 6
|
||||
)
|
||||
self.momentXX += (
|
||||
-r10 * y0 / 12
|
||||
- r10 * y1 / 4
|
||||
- r2 * r5 / 12
|
||||
- r4 * r6 * x1 / 12
|
||||
+ x0**3 * (3 * y0 + y1) / 12
|
||||
)
|
||||
self.momentXY += (
|
||||
-r2 * r8 / 24
|
||||
- r2 * r9 / 8
|
||||
- r3 * r7 / 24
|
||||
+ r6 * (r7 * y1 + 3 * r8 + r9) / 24
|
||||
- x0 * x1 * (r8 - r9) / 12
|
||||
)
|
||||
self.momentYY += (
|
||||
-r0 * r9 / 12
|
||||
- r1 * r8 / 12
|
||||
- r11 * x1 / 12
|
||||
- r12 * x1 / 12
|
||||
+ x0 * (r11 + r12 + r8 * y1 + r9 * y0) / 12
|
||||
)
|
||||
|
||||
@cython.locals(r0=cython.double)
|
||||
@cython.locals(r1=cython.double)
|
||||
@cython.locals(r2=cython.double)
|
||||
@cython.locals(r3=cython.double)
|
||||
@cython.locals(r4=cython.double)
|
||||
@cython.locals(r5=cython.double)
|
||||
@cython.locals(r6=cython.double)
|
||||
@cython.locals(r7=cython.double)
|
||||
@cython.locals(r8=cython.double)
|
||||
@cython.locals(r9=cython.double)
|
||||
@cython.locals(r10=cython.double)
|
||||
@cython.locals(r11=cython.double)
|
||||
@cython.locals(r12=cython.double)
|
||||
@cython.locals(r13=cython.double)
|
||||
@cython.locals(r14=cython.double)
|
||||
@cython.locals(r15=cython.double)
|
||||
@cython.locals(r16=cython.double)
|
||||
@cython.locals(r17=cython.double)
|
||||
@cython.locals(r18=cython.double)
|
||||
@cython.locals(r19=cython.double)
|
||||
@cython.locals(r20=cython.double)
|
||||
@cython.locals(r21=cython.double)
|
||||
@cython.locals(r22=cython.double)
|
||||
@cython.locals(r23=cython.double)
|
||||
@cython.locals(r24=cython.double)
|
||||
@cython.locals(r25=cython.double)
|
||||
@cython.locals(r26=cython.double)
|
||||
@cython.locals(r27=cython.double)
|
||||
@cython.locals(r28=cython.double)
|
||||
@cython.locals(r29=cython.double)
|
||||
@cython.locals(r30=cython.double)
|
||||
@cython.locals(r31=cython.double)
|
||||
@cython.locals(r32=cython.double)
|
||||
@cython.locals(r33=cython.double)
|
||||
@cython.locals(r34=cython.double)
|
||||
@cython.locals(r35=cython.double)
|
||||
@cython.locals(r36=cython.double)
|
||||
@cython.locals(r37=cython.double)
|
||||
@cython.locals(r38=cython.double)
|
||||
@cython.locals(r39=cython.double)
|
||||
@cython.locals(r40=cython.double)
|
||||
@cython.locals(r41=cython.double)
|
||||
@cython.locals(r42=cython.double)
|
||||
@cython.locals(r43=cython.double)
|
||||
@cython.locals(r44=cython.double)
|
||||
@cython.locals(r45=cython.double)
|
||||
@cython.locals(r46=cython.double)
|
||||
@cython.locals(r47=cython.double)
|
||||
@cython.locals(r48=cython.double)
|
||||
@cython.locals(r49=cython.double)
|
||||
@cython.locals(r50=cython.double)
|
||||
@cython.locals(r51=cython.double)
|
||||
@cython.locals(r52=cython.double)
|
||||
@cython.locals(r53=cython.double)
|
||||
@cython.locals(x0=cython.double, y0=cython.double)
|
||||
@cython.locals(x1=cython.double, y1=cython.double)
|
||||
@cython.locals(x2=cython.double, y2=cython.double)
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
x0, y0 = self._getCurrentPoint()
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
|
||||
r0 = 2 * y1
|
||||
r1 = r0 * x2
|
||||
r2 = x2 * y2
|
||||
r3 = 3 * r2
|
||||
r4 = 2 * x1
|
||||
r5 = 3 * y0
|
||||
r6 = x1**2
|
||||
r7 = x2**2
|
||||
r8 = 4 * y1
|
||||
r9 = 10 * y2
|
||||
r10 = 2 * y2
|
||||
r11 = r4 * x2
|
||||
r12 = x0**2
|
||||
r13 = 10 * y0
|
||||
r14 = r4 * y2
|
||||
r15 = x2 * y0
|
||||
r16 = 4 * x1
|
||||
r17 = r0 * x1 + r2
|
||||
r18 = r2 * r8
|
||||
r19 = y1**2
|
||||
r20 = 2 * r19
|
||||
r21 = y2**2
|
||||
r22 = r21 * x2
|
||||
r23 = 5 * r22
|
||||
r24 = y0**2
|
||||
r25 = y0 * y2
|
||||
r26 = 5 * r24
|
||||
r27 = x1**3
|
||||
r28 = x2**3
|
||||
r29 = 30 * y1
|
||||
r30 = 6 * y1
|
||||
r31 = 10 * r7 * x1
|
||||
r32 = 5 * y2
|
||||
r33 = 12 * r6
|
||||
r34 = 30 * x1
|
||||
r35 = x1 * y1
|
||||
r36 = r3 + 20 * r35
|
||||
r37 = 12 * x1
|
||||
r38 = 20 * r6
|
||||
r39 = 8 * r6 * y1
|
||||
r40 = r32 * r7
|
||||
r41 = 60 * y1
|
||||
r42 = 20 * r19
|
||||
r43 = 4 * r19
|
||||
r44 = 15 * r21
|
||||
r45 = 12 * x2
|
||||
r46 = 12 * y2
|
||||
r47 = 6 * x1
|
||||
r48 = 8 * r19 * x1 + r23
|
||||
r49 = 8 * y1**3
|
||||
r50 = y2**3
|
||||
r51 = y0**3
|
||||
r52 = 10 * y1
|
||||
r53 = 12 * y1
|
||||
|
||||
self.area += (
|
||||
-r1 / 6
|
||||
- r3 / 6
|
||||
+ x0 * (r0 + r5 + y2) / 6
|
||||
+ x1 * y2 / 3
|
||||
- y0 * (r4 + x2) / 6
|
||||
)
|
||||
self.momentX += (
|
||||
-r11 * (-r10 + y1) / 30
|
||||
+ r12 * (r13 + r8 + y2) / 30
|
||||
+ r6 * y2 / 15
|
||||
- r7 * r8 / 30
|
||||
- r7 * r9 / 30
|
||||
+ x0 * (r14 - r15 - r16 * y0 + r17) / 30
|
||||
- y0 * (r11 + 2 * r6 + r7) / 30
|
||||
)
|
||||
self.momentY += (
|
||||
-r18 / 30
|
||||
- r20 * x2 / 30
|
||||
- r23 / 30
|
||||
- r24 * (r16 + x2) / 30
|
||||
+ x0 * (r0 * y2 + r20 + r21 + r25 + r26 + r8 * y0) / 30
|
||||
+ x1 * y2 * (r10 + y1) / 15
|
||||
- y0 * (r1 + r17) / 30
|
||||
)
|
||||
self.momentXX += (
|
||||
r12 * (r1 - 5 * r15 - r34 * y0 + r36 + r9 * x1) / 420
|
||||
+ 2 * r27 * y2 / 105
|
||||
- r28 * r29 / 420
|
||||
- r28 * y2 / 4
|
||||
- r31 * (r0 - 3 * y2) / 420
|
||||
- r6 * x2 * (r0 - r32) / 105
|
||||
+ x0**3 * (r30 + 21 * y0 + y2) / 84
|
||||
- x0
|
||||
* (
|
||||
r0 * r7
|
||||
+ r15 * r37
|
||||
- r2 * r37
|
||||
- r33 * y2
|
||||
+ r38 * y0
|
||||
- r39
|
||||
- r40
|
||||
+ r5 * r7
|
||||
)
|
||||
/ 420
|
||||
- y0 * (8 * r27 + 5 * r28 + r31 + r33 * x2) / 420
|
||||
)
|
||||
self.momentXY += (
|
||||
r12 * (r13 * y2 + 3 * r21 + 105 * r24 + r41 * y0 + r42 + r46 * y1) / 840
|
||||
- r16 * x2 * (r43 - r44) / 840
|
||||
- r21 * r7 / 8
|
||||
- r24 * (r38 + r45 * x1 + 3 * r7) / 840
|
||||
- r41 * r7 * y2 / 840
|
||||
- r42 * r7 / 840
|
||||
+ r6 * y2 * (r32 + r8) / 210
|
||||
+ x0
|
||||
* (
|
||||
-r15 * r8
|
||||
+ r16 * r25
|
||||
+ r18
|
||||
+ r21 * r47
|
||||
- r24 * r34
|
||||
- r26 * x2
|
||||
+ r35 * r46
|
||||
+ r48
|
||||
)
|
||||
/ 420
|
||||
- y0 * (r16 * r2 + r30 * r7 + r35 * r45 + r39 + r40) / 420
|
||||
)
|
||||
self.momentYY += (
|
||||
-r2 * r42 / 420
|
||||
- r22 * r29 / 420
|
||||
- r24 * (r14 + r36 + r52 * x2) / 420
|
||||
- r49 * x2 / 420
|
||||
- r50 * x2 / 12
|
||||
- r51 * (r47 + x2) / 84
|
||||
+ x0
|
||||
* (
|
||||
r19 * r46
|
||||
+ r21 * r5
|
||||
+ r21 * r52
|
||||
+ r24 * r29
|
||||
+ r25 * r53
|
||||
+ r26 * y2
|
||||
+ r42 * y0
|
||||
+ r49
|
||||
+ 5 * r50
|
||||
+ 35 * r51
|
||||
)
|
||||
/ 420
|
||||
+ x1 * y2 * (r43 + r44 + r9 * y1) / 210
|
||||
- y0 * (r19 * r45 + r2 * r53 - r21 * r4 + r48) / 420
|
||||
)
|
||||
|
||||
@cython.locals(r0=cython.double)
|
||||
@cython.locals(r1=cython.double)
|
||||
@cython.locals(r2=cython.double)
|
||||
@cython.locals(r3=cython.double)
|
||||
@cython.locals(r4=cython.double)
|
||||
@cython.locals(r5=cython.double)
|
||||
@cython.locals(r6=cython.double)
|
||||
@cython.locals(r7=cython.double)
|
||||
@cython.locals(r8=cython.double)
|
||||
@cython.locals(r9=cython.double)
|
||||
@cython.locals(r10=cython.double)
|
||||
@cython.locals(r11=cython.double)
|
||||
@cython.locals(r12=cython.double)
|
||||
@cython.locals(r13=cython.double)
|
||||
@cython.locals(r14=cython.double)
|
||||
@cython.locals(r15=cython.double)
|
||||
@cython.locals(r16=cython.double)
|
||||
@cython.locals(r17=cython.double)
|
||||
@cython.locals(r18=cython.double)
|
||||
@cython.locals(r19=cython.double)
|
||||
@cython.locals(r20=cython.double)
|
||||
@cython.locals(r21=cython.double)
|
||||
@cython.locals(r22=cython.double)
|
||||
@cython.locals(r23=cython.double)
|
||||
@cython.locals(r24=cython.double)
|
||||
@cython.locals(r25=cython.double)
|
||||
@cython.locals(r26=cython.double)
|
||||
@cython.locals(r27=cython.double)
|
||||
@cython.locals(r28=cython.double)
|
||||
@cython.locals(r29=cython.double)
|
||||
@cython.locals(r30=cython.double)
|
||||
@cython.locals(r31=cython.double)
|
||||
@cython.locals(r32=cython.double)
|
||||
@cython.locals(r33=cython.double)
|
||||
@cython.locals(r34=cython.double)
|
||||
@cython.locals(r35=cython.double)
|
||||
@cython.locals(r36=cython.double)
|
||||
@cython.locals(r37=cython.double)
|
||||
@cython.locals(r38=cython.double)
|
||||
@cython.locals(r39=cython.double)
|
||||
@cython.locals(r40=cython.double)
|
||||
@cython.locals(r41=cython.double)
|
||||
@cython.locals(r42=cython.double)
|
||||
@cython.locals(r43=cython.double)
|
||||
@cython.locals(r44=cython.double)
|
||||
@cython.locals(r45=cython.double)
|
||||
@cython.locals(r46=cython.double)
|
||||
@cython.locals(r47=cython.double)
|
||||
@cython.locals(r48=cython.double)
|
||||
@cython.locals(r49=cython.double)
|
||||
@cython.locals(r50=cython.double)
|
||||
@cython.locals(r51=cython.double)
|
||||
@cython.locals(r52=cython.double)
|
||||
@cython.locals(r53=cython.double)
|
||||
@cython.locals(r54=cython.double)
|
||||
@cython.locals(r55=cython.double)
|
||||
@cython.locals(r56=cython.double)
|
||||
@cython.locals(r57=cython.double)
|
||||
@cython.locals(r58=cython.double)
|
||||
@cython.locals(r59=cython.double)
|
||||
@cython.locals(r60=cython.double)
|
||||
@cython.locals(r61=cython.double)
|
||||
@cython.locals(r62=cython.double)
|
||||
@cython.locals(r63=cython.double)
|
||||
@cython.locals(r64=cython.double)
|
||||
@cython.locals(r65=cython.double)
|
||||
@cython.locals(r66=cython.double)
|
||||
@cython.locals(r67=cython.double)
|
||||
@cython.locals(r68=cython.double)
|
||||
@cython.locals(r69=cython.double)
|
||||
@cython.locals(r70=cython.double)
|
||||
@cython.locals(r71=cython.double)
|
||||
@cython.locals(r72=cython.double)
|
||||
@cython.locals(r73=cython.double)
|
||||
@cython.locals(r74=cython.double)
|
||||
@cython.locals(r75=cython.double)
|
||||
@cython.locals(r76=cython.double)
|
||||
@cython.locals(r77=cython.double)
|
||||
@cython.locals(r78=cython.double)
|
||||
@cython.locals(r79=cython.double)
|
||||
@cython.locals(r80=cython.double)
|
||||
@cython.locals(r81=cython.double)
|
||||
@cython.locals(r82=cython.double)
|
||||
@cython.locals(r83=cython.double)
|
||||
@cython.locals(r84=cython.double)
|
||||
@cython.locals(r85=cython.double)
|
||||
@cython.locals(r86=cython.double)
|
||||
@cython.locals(r87=cython.double)
|
||||
@cython.locals(r88=cython.double)
|
||||
@cython.locals(r89=cython.double)
|
||||
@cython.locals(r90=cython.double)
|
||||
@cython.locals(r91=cython.double)
|
||||
@cython.locals(r92=cython.double)
|
||||
@cython.locals(r93=cython.double)
|
||||
@cython.locals(r94=cython.double)
|
||||
@cython.locals(r95=cython.double)
|
||||
@cython.locals(r96=cython.double)
|
||||
@cython.locals(r97=cython.double)
|
||||
@cython.locals(r98=cython.double)
|
||||
@cython.locals(r99=cython.double)
|
||||
@cython.locals(r100=cython.double)
|
||||
@cython.locals(r101=cython.double)
|
||||
@cython.locals(r102=cython.double)
|
||||
@cython.locals(r103=cython.double)
|
||||
@cython.locals(r104=cython.double)
|
||||
@cython.locals(r105=cython.double)
|
||||
@cython.locals(r106=cython.double)
|
||||
@cython.locals(r107=cython.double)
|
||||
@cython.locals(r108=cython.double)
|
||||
@cython.locals(r109=cython.double)
|
||||
@cython.locals(r110=cython.double)
|
||||
@cython.locals(r111=cython.double)
|
||||
@cython.locals(r112=cython.double)
|
||||
@cython.locals(r113=cython.double)
|
||||
@cython.locals(r114=cython.double)
|
||||
@cython.locals(r115=cython.double)
|
||||
@cython.locals(r116=cython.double)
|
||||
@cython.locals(r117=cython.double)
|
||||
@cython.locals(r118=cython.double)
|
||||
@cython.locals(r119=cython.double)
|
||||
@cython.locals(r120=cython.double)
|
||||
@cython.locals(r121=cython.double)
|
||||
@cython.locals(r122=cython.double)
|
||||
@cython.locals(r123=cython.double)
|
||||
@cython.locals(r124=cython.double)
|
||||
@cython.locals(r125=cython.double)
|
||||
@cython.locals(r126=cython.double)
|
||||
@cython.locals(r127=cython.double)
|
||||
@cython.locals(r128=cython.double)
|
||||
@cython.locals(r129=cython.double)
|
||||
@cython.locals(r130=cython.double)
|
||||
@cython.locals(r131=cython.double)
|
||||
@cython.locals(r132=cython.double)
|
||||
@cython.locals(x0=cython.double, y0=cython.double)
|
||||
@cython.locals(x1=cython.double, y1=cython.double)
|
||||
@cython.locals(x2=cython.double, y2=cython.double)
|
||||
@cython.locals(x3=cython.double, y3=cython.double)
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
x0, y0 = self._getCurrentPoint()
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
x3, y3 = p3
|
||||
|
||||
r0 = 6 * y2
|
||||
r1 = r0 * x3
|
||||
r2 = 10 * y3
|
||||
r3 = r2 * x3
|
||||
r4 = 3 * y1
|
||||
r5 = 6 * x1
|
||||
r6 = 3 * x2
|
||||
r7 = 6 * y1
|
||||
r8 = 3 * y2
|
||||
r9 = x2**2
|
||||
r10 = 45 * r9
|
||||
r11 = r10 * y3
|
||||
r12 = x3**2
|
||||
r13 = r12 * y2
|
||||
r14 = r12 * y3
|
||||
r15 = 7 * y3
|
||||
r16 = 15 * x3
|
||||
r17 = r16 * x2
|
||||
r18 = x1**2
|
||||
r19 = 9 * r18
|
||||
r20 = x0**2
|
||||
r21 = 21 * y1
|
||||
r22 = 9 * r9
|
||||
r23 = r7 * x3
|
||||
r24 = 9 * y2
|
||||
r25 = r24 * x2 + r3
|
||||
r26 = 9 * x2
|
||||
r27 = x2 * y3
|
||||
r28 = -r26 * y1 + 15 * r27
|
||||
r29 = 3 * x1
|
||||
r30 = 45 * x1
|
||||
r31 = 12 * x3
|
||||
r32 = 45 * r18
|
||||
r33 = 5 * r12
|
||||
r34 = r8 * x3
|
||||
r35 = 105 * y0
|
||||
r36 = 30 * y0
|
||||
r37 = r36 * x2
|
||||
r38 = 5 * x3
|
||||
r39 = 15 * y3
|
||||
r40 = 5 * y3
|
||||
r41 = r40 * x3
|
||||
r42 = x2 * y2
|
||||
r43 = 18 * r42
|
||||
r44 = 45 * y1
|
||||
r45 = r41 + r43 + r44 * x1
|
||||
r46 = y2 * y3
|
||||
r47 = r46 * x3
|
||||
r48 = y2**2
|
||||
r49 = 45 * r48
|
||||
r50 = r49 * x3
|
||||
r51 = y3**2
|
||||
r52 = r51 * x3
|
||||
r53 = y1**2
|
||||
r54 = 9 * r53
|
||||
r55 = y0**2
|
||||
r56 = 21 * x1
|
||||
r57 = 6 * x2
|
||||
r58 = r16 * y2
|
||||
r59 = r39 * y2
|
||||
r60 = 9 * r48
|
||||
r61 = r6 * y3
|
||||
r62 = 3 * y3
|
||||
r63 = r36 * y2
|
||||
r64 = y1 * y3
|
||||
r65 = 45 * r53
|
||||
r66 = 5 * r51
|
||||
r67 = x2**3
|
||||
r68 = x3**3
|
||||
r69 = 630 * y2
|
||||
r70 = 126 * x3
|
||||
r71 = x1**3
|
||||
r72 = 126 * x2
|
||||
r73 = 63 * r9
|
||||
r74 = r73 * x3
|
||||
r75 = r15 * x3 + 15 * r42
|
||||
r76 = 630 * x1
|
||||
r77 = 14 * x3
|
||||
r78 = 21 * r27
|
||||
r79 = 42 * x1
|
||||
r80 = 42 * x2
|
||||
r81 = x1 * y2
|
||||
r82 = 63 * r42
|
||||
r83 = x1 * y1
|
||||
r84 = r41 + r82 + 378 * r83
|
||||
r85 = x2 * x3
|
||||
r86 = r85 * y1
|
||||
r87 = r27 * x3
|
||||
r88 = 27 * r9
|
||||
r89 = r88 * y2
|
||||
r90 = 42 * r14
|
||||
r91 = 90 * x1
|
||||
r92 = 189 * r18
|
||||
r93 = 378 * r18
|
||||
r94 = r12 * y1
|
||||
r95 = 252 * x1 * x2
|
||||
r96 = r79 * x3
|
||||
r97 = 30 * r85
|
||||
r98 = r83 * x3
|
||||
r99 = 30 * x3
|
||||
r100 = 42 * x3
|
||||
r101 = r42 * x1
|
||||
r102 = r10 * y2 + 14 * r14 + 126 * r18 * y1 + r81 * r99
|
||||
r103 = 378 * r48
|
||||
r104 = 18 * y1
|
||||
r105 = r104 * y2
|
||||
r106 = y0 * y1
|
||||
r107 = 252 * y2
|
||||
r108 = r107 * y0
|
||||
r109 = y0 * y3
|
||||
r110 = 42 * r64
|
||||
r111 = 378 * r53
|
||||
r112 = 63 * r48
|
||||
r113 = 27 * x2
|
||||
r114 = r27 * y2
|
||||
r115 = r113 * r48 + 42 * r52
|
||||
r116 = x3 * y3
|
||||
r117 = 54 * r42
|
||||
r118 = r51 * x1
|
||||
r119 = r51 * x2
|
||||
r120 = r48 * x1
|
||||
r121 = 21 * x3
|
||||
r122 = r64 * x1
|
||||
r123 = r81 * y3
|
||||
r124 = 30 * r27 * y1 + r49 * x2 + 14 * r52 + 126 * r53 * x1
|
||||
r125 = y2**3
|
||||
r126 = y3**3
|
||||
r127 = y1**3
|
||||
r128 = y0**3
|
||||
r129 = r51 * y2
|
||||
r130 = r112 * y3 + r21 * r51
|
||||
r131 = 189 * r53
|
||||
r132 = 90 * y2
|
||||
|
||||
self.area += (
|
||||
-r1 / 20
|
||||
- r3 / 20
|
||||
- r4 * (x2 + x3) / 20
|
||||
+ x0 * (r7 + r8 + 10 * y0 + y3) / 20
|
||||
+ 3 * x1 * (y2 + y3) / 20
|
||||
+ 3 * x2 * y3 / 10
|
||||
- y0 * (r5 + r6 + x3) / 20
|
||||
)
|
||||
self.momentX += (
|
||||
r11 / 840
|
||||
- r13 / 8
|
||||
- r14 / 3
|
||||
- r17 * (-r15 + r8) / 840
|
||||
+ r19 * (r8 + 2 * y3) / 840
|
||||
+ r20 * (r0 + r21 + 56 * y0 + y3) / 168
|
||||
+ r29 * (-r23 + r25 + r28) / 840
|
||||
- r4 * (10 * r12 + r17 + r22) / 840
|
||||
+ x0
|
||||
* (
|
||||
12 * r27
|
||||
+ r30 * y2
|
||||
+ r34
|
||||
- r35 * x1
|
||||
- r37
|
||||
- r38 * y0
|
||||
+ r39 * x1
|
||||
- r4 * x3
|
||||
+ r45
|
||||
)
|
||||
/ 840
|
||||
- y0 * (r17 + r30 * x2 + r31 * x1 + r32 + r33 + 18 * r9) / 840
|
||||
)
|
||||
self.momentY += (
|
||||
-r4 * (r25 + r58) / 840
|
||||
- r47 / 8
|
||||
- r50 / 840
|
||||
- r52 / 6
|
||||
- r54 * (r6 + 2 * x3) / 840
|
||||
- r55 * (r56 + r57 + x3) / 168
|
||||
+ x0
|
||||
* (
|
||||
r35 * y1
|
||||
+ r40 * y0
|
||||
+ r44 * y2
|
||||
+ 18 * r48
|
||||
+ 140 * r55
|
||||
+ r59
|
||||
+ r63
|
||||
+ 12 * r64
|
||||
+ r65
|
||||
+ r66
|
||||
)
|
||||
/ 840
|
||||
+ x1 * (r24 * y1 + 10 * r51 + r59 + r60 + r7 * y3) / 280
|
||||
+ x2 * y3 * (r15 + r8) / 56
|
||||
- y0 * (r16 * y1 + r31 * y2 + r44 * x2 + r45 + r61 - r62 * x1) / 840
|
||||
)
|
||||
self.momentXX += (
|
||||
-r12 * r72 * (-r40 + r8) / 9240
|
||||
+ 3 * r18 * (r28 + r34 - r38 * y1 + r75) / 3080
|
||||
+ r20
|
||||
* (
|
||||
r24 * x3
|
||||
- r72 * y0
|
||||
- r76 * y0
|
||||
- r77 * y0
|
||||
+ r78
|
||||
+ r79 * y3
|
||||
+ r80 * y1
|
||||
+ 210 * r81
|
||||
+ r84
|
||||
)
|
||||
/ 9240
|
||||
- r29
|
||||
* (
|
||||
r12 * r21
|
||||
+ 14 * r13
|
||||
+ r44 * r9
|
||||
- r73 * y3
|
||||
+ 54 * r86
|
||||
- 84 * r87
|
||||
- r89
|
||||
- r90
|
||||
)
|
||||
/ 9240
|
||||
- r4 * (70 * r12 * x2 + 27 * r67 + 42 * r68 + r74) / 9240
|
||||
+ 3 * r67 * y3 / 220
|
||||
- r68 * r69 / 9240
|
||||
- r68 * y3 / 4
|
||||
- r70 * r9 * (-r62 + y2) / 9240
|
||||
+ 3 * r71 * (r24 + r40) / 3080
|
||||
+ x0**3 * (r24 + r44 + 165 * y0 + y3) / 660
|
||||
+ x0
|
||||
* (
|
||||
r100 * r27
|
||||
+ 162 * r101
|
||||
+ r102
|
||||
+ r11
|
||||
+ 63 * r18 * y3
|
||||
+ r27 * r91
|
||||
- r33 * y0
|
||||
- r37 * x3
|
||||
+ r43 * x3
|
||||
- r73 * y0
|
||||
- r88 * y1
|
||||
+ r92 * y2
|
||||
- r93 * y0
|
||||
- 9 * r94
|
||||
- r95 * y0
|
||||
- r96 * y0
|
||||
- r97 * y1
|
||||
- 18 * r98
|
||||
+ r99 * x1 * y3
|
||||
)
|
||||
/ 9240
|
||||
- y0
|
||||
* (
|
||||
r12 * r56
|
||||
+ r12 * r80
|
||||
+ r32 * x3
|
||||
+ 45 * r67
|
||||
+ 14 * r68
|
||||
+ 126 * r71
|
||||
+ r74
|
||||
+ r85 * r91
|
||||
+ 135 * r9 * x1
|
||||
+ r92 * x2
|
||||
)
|
||||
/ 9240
|
||||
)
|
||||
self.momentXY += (
|
||||
-r103 * r12 / 18480
|
||||
- r12 * r51 / 8
|
||||
- 3 * r14 * y2 / 44
|
||||
+ 3 * r18 * (r105 + r2 * y1 + 18 * r46 + 15 * r48 + 7 * r51) / 6160
|
||||
+ r20
|
||||
* (
|
||||
1260 * r106
|
||||
+ r107 * y1
|
||||
+ r108
|
||||
+ 28 * r109
|
||||
+ r110
|
||||
+ r111
|
||||
+ r112
|
||||
+ 30 * r46
|
||||
+ 2310 * r55
|
||||
+ r66
|
||||
)
|
||||
/ 18480
|
||||
- r54 * (7 * r12 + 18 * r85 + 15 * r9) / 18480
|
||||
- r55 * (r33 + r73 + r93 + r95 + r96 + r97) / 18480
|
||||
- r7 * (42 * r13 + r82 * x3 + 28 * r87 + r89 + r90) / 18480
|
||||
- 3 * r85 * (r48 - r66) / 220
|
||||
+ 3 * r9 * y3 * (r62 + 2 * y2) / 440
|
||||
+ x0
|
||||
* (
|
||||
-r1 * y0
|
||||
- 84 * r106 * x2
|
||||
+ r109 * r56
|
||||
+ 54 * r114
|
||||
+ r117 * y1
|
||||
+ 15 * r118
|
||||
+ 21 * r119
|
||||
+ 81 * r120
|
||||
+ r121 * r46
|
||||
+ 54 * r122
|
||||
+ 60 * r123
|
||||
+ r124
|
||||
- r21 * x3 * y0
|
||||
+ r23 * y3
|
||||
- r54 * x3
|
||||
- r55 * r72
|
||||
- r55 * r76
|
||||
- r55 * r77
|
||||
+ r57 * y0 * y3
|
||||
+ r60 * x3
|
||||
+ 84 * r81 * y0
|
||||
+ 189 * r81 * y1
|
||||
)
|
||||
/ 9240
|
||||
+ x1
|
||||
* (
|
||||
r104 * r27
|
||||
- r105 * x3
|
||||
- r113 * r53
|
||||
+ 63 * r114
|
||||
+ r115
|
||||
- r16 * r53
|
||||
+ 28 * r47
|
||||
+ r51 * r80
|
||||
)
|
||||
/ 3080
|
||||
- y0
|
||||
* (
|
||||
54 * r101
|
||||
+ r102
|
||||
+ r116 * r5
|
||||
+ r117 * x3
|
||||
+ 21 * r13
|
||||
- r19 * y3
|
||||
+ r22 * y3
|
||||
+ r78 * x3
|
||||
+ 189 * r83 * x2
|
||||
+ 60 * r86
|
||||
+ 81 * r9 * y1
|
||||
+ 15 * r94
|
||||
+ 54 * r98
|
||||
)
|
||||
/ 9240
|
||||
)
|
||||
self.momentYY += (
|
||||
-r103 * r116 / 9240
|
||||
- r125 * r70 / 9240
|
||||
- r126 * x3 / 12
|
||||
- 3 * r127 * (r26 + r38) / 3080
|
||||
- r128 * (r26 + r30 + x3) / 660
|
||||
- r4 * (r112 * x3 + r115 - 14 * r119 + 84 * r47) / 9240
|
||||
- r52 * r69 / 9240
|
||||
- r54 * (r58 + r61 + r75) / 9240
|
||||
- r55
|
||||
* (r100 * y1 + r121 * y2 + r26 * y3 + r79 * y2 + r84 + 210 * x2 * y1)
|
||||
/ 9240
|
||||
+ x0
|
||||
* (
|
||||
r108 * y1
|
||||
+ r110 * y0
|
||||
+ r111 * y0
|
||||
+ r112 * y0
|
||||
+ 45 * r125
|
||||
+ 14 * r126
|
||||
+ 126 * r127
|
||||
+ 770 * r128
|
||||
+ 42 * r129
|
||||
+ r130
|
||||
+ r131 * y2
|
||||
+ r132 * r64
|
||||
+ 135 * r48 * y1
|
||||
+ 630 * r55 * y1
|
||||
+ 126 * r55 * y2
|
||||
+ 14 * r55 * y3
|
||||
+ r63 * y3
|
||||
+ r65 * y3
|
||||
+ r66 * y0
|
||||
)
|
||||
/ 9240
|
||||
+ x1
|
||||
* (
|
||||
27 * r125
|
||||
+ 42 * r126
|
||||
+ 70 * r129
|
||||
+ r130
|
||||
+ r39 * r53
|
||||
+ r44 * r48
|
||||
+ 27 * r53 * y2
|
||||
+ 54 * r64 * y2
|
||||
)
|
||||
/ 3080
|
||||
+ 3 * x2 * y3 * (r48 + r66 + r8 * y3) / 220
|
||||
- y0
|
||||
* (
|
||||
r100 * r46
|
||||
+ 18 * r114
|
||||
- 9 * r118
|
||||
- 27 * r120
|
||||
- 18 * r122
|
||||
- 30 * r123
|
||||
+ r124
|
||||
+ r131 * x2
|
||||
+ r132 * x3 * y1
|
||||
+ 162 * r42 * y1
|
||||
+ r50
|
||||
+ 63 * r53 * x3
|
||||
+ r64 * r99
|
||||
)
|
||||
/ 9240
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from fontTools.misc.symfont import x, y, printGreenPen
|
||||
|
||||
printGreenPen(
|
||||
"MomentsPen",
|
||||
[
|
||||
("area", 1),
|
||||
("momentX", x),
|
||||
("momentY", y),
|
||||
("momentXX", x**2),
|
||||
("momentXY", x * y),
|
||||
("momentYY", y**2),
|
||||
],
|
||||
)
|
||||
@@ -1,69 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Calculate the perimeter of a glyph."""
|
||||
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from fontTools.misc.bezierTools import (
|
||||
approximateQuadraticArcLengthC,
|
||||
calcQuadraticArcLengthC,
|
||||
approximateCubicArcLengthC,
|
||||
calcCubicArcLengthC,
|
||||
)
|
||||
import math
|
||||
|
||||
|
||||
__all__ = ["PerimeterPen"]
|
||||
|
||||
|
||||
def _distance(p0, p1):
|
||||
return math.hypot(p0[0] - p1[0], p0[1] - p1[1])
|
||||
|
||||
|
||||
class PerimeterPen(BasePen):
|
||||
def __init__(self, glyphset=None, tolerance=0.005):
|
||||
BasePen.__init__(self, glyphset)
|
||||
self.value = 0
|
||||
self.tolerance = tolerance
|
||||
|
||||
# Choose which algorithm to use for quadratic and for cubic.
|
||||
# Quadrature is faster but has fixed error characteristic with no strong
|
||||
# error bound. The cutoff points are derived empirically.
|
||||
self._addCubic = (
|
||||
self._addCubicQuadrature if tolerance >= 0.0015 else self._addCubicRecursive
|
||||
)
|
||||
self._addQuadratic = (
|
||||
self._addQuadraticQuadrature
|
||||
if tolerance >= 0.00075
|
||||
else self._addQuadraticExact
|
||||
)
|
||||
|
||||
def _moveTo(self, p0):
|
||||
self.__startPoint = p0
|
||||
|
||||
def _closePath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self.__startPoint:
|
||||
self._lineTo(self.__startPoint)
|
||||
|
||||
def _lineTo(self, p1):
|
||||
p0 = self._getCurrentPoint()
|
||||
self.value += _distance(p0, p1)
|
||||
|
||||
def _addQuadraticExact(self, c0, c1, c2):
|
||||
self.value += calcQuadraticArcLengthC(c0, c1, c2)
|
||||
|
||||
def _addQuadraticQuadrature(self, c0, c1, c2):
|
||||
self.value += approximateQuadraticArcLengthC(c0, c1, c2)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
p0 = self._getCurrentPoint()
|
||||
self._addQuadratic(complex(*p0), complex(*p1), complex(*p2))
|
||||
|
||||
def _addCubicRecursive(self, c0, c1, c2, c3):
|
||||
self.value += calcCubicArcLengthC(c0, c1, c2, c3, self.tolerance)
|
||||
|
||||
def _addCubicQuadrature(self, c0, c1, c2, c3):
|
||||
self.value += approximateCubicArcLengthC(c0, c1, c2, c3)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
p0 = self._getCurrentPoint()
|
||||
self._addCubic(complex(*p0), complex(*p1), complex(*p2), complex(*p3))
|
||||
@@ -1,191 +0,0 @@
|
||||
"""fontTools.pens.pointInsidePen -- Pen implementing "point inside" testing
|
||||
for shapes.
|
||||
"""
|
||||
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from fontTools.misc.bezierTools import solveQuadratic, solveCubic
|
||||
|
||||
|
||||
__all__ = ["PointInsidePen"]
|
||||
|
||||
|
||||
class PointInsidePen(BasePen):
|
||||
"""This pen implements "point inside" testing: to test whether
|
||||
a given point lies inside the shape (black) or outside (white).
|
||||
Instances of this class can be recycled, as long as the
|
||||
setTestPoint() method is used to set the new point to test.
|
||||
|
||||
Typical usage:
|
||||
|
||||
pen = PointInsidePen(glyphSet, (100, 200))
|
||||
outline.draw(pen)
|
||||
isInside = pen.getResult()
|
||||
|
||||
Both the even-odd algorithm and the non-zero-winding-rule
|
||||
algorithm are implemented. The latter is the default, specify
|
||||
True for the evenOdd argument of __init__ or setTestPoint
|
||||
to use the even-odd algorithm.
|
||||
"""
|
||||
|
||||
# This class implements the classical "shoot a ray from the test point
|
||||
# to infinity and count how many times it intersects the outline" (as well
|
||||
# as the non-zero variant, where the counter is incremented if the outline
|
||||
# intersects the ray in one direction and decremented if it intersects in
|
||||
# the other direction).
|
||||
# I found an amazingly clear explanation of the subtleties involved in
|
||||
# implementing this correctly for polygons here:
|
||||
# http://graphics.cs.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html
|
||||
# I extended the principles outlined on that page to curves.
|
||||
|
||||
def __init__(self, glyphSet, testPoint, evenOdd=False):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self.setTestPoint(testPoint, evenOdd)
|
||||
|
||||
def setTestPoint(self, testPoint, evenOdd=False):
|
||||
"""Set the point to test. Call this _before_ the outline gets drawn."""
|
||||
self.testPoint = testPoint
|
||||
self.evenOdd = evenOdd
|
||||
self.firstPoint = None
|
||||
self.intersectionCount = 0
|
||||
|
||||
def getWinding(self):
|
||||
if self.firstPoint is not None:
|
||||
# always make sure the sub paths are closed; the algorithm only works
|
||||
# for closed paths.
|
||||
self.closePath()
|
||||
return self.intersectionCount
|
||||
|
||||
def getResult(self):
|
||||
"""After the shape has been drawn, getResult() returns True if the test
|
||||
point lies within the (black) shape, and False if it doesn't.
|
||||
"""
|
||||
winding = self.getWinding()
|
||||
if self.evenOdd:
|
||||
result = winding % 2
|
||||
else: # non-zero
|
||||
result = self.intersectionCount != 0
|
||||
return not not result
|
||||
|
||||
def _addIntersection(self, goingUp):
|
||||
if self.evenOdd or goingUp:
|
||||
self.intersectionCount += 1
|
||||
else:
|
||||
self.intersectionCount -= 1
|
||||
|
||||
def _moveTo(self, point):
|
||||
if self.firstPoint is not None:
|
||||
# always make sure the sub paths are closed; the algorithm only works
|
||||
# for closed paths.
|
||||
self.closePath()
|
||||
self.firstPoint = point
|
||||
|
||||
def _lineTo(self, point):
|
||||
x, y = self.testPoint
|
||||
x1, y1 = self._getCurrentPoint()
|
||||
x2, y2 = point
|
||||
|
||||
if x1 < x and x2 < x:
|
||||
return
|
||||
if y1 < y and y2 < y:
|
||||
return
|
||||
if y1 >= y and y2 >= y:
|
||||
return
|
||||
|
||||
dx = x2 - x1
|
||||
dy = y2 - y1
|
||||
t = (y - y1) / dy
|
||||
ix = dx * t + x1
|
||||
if ix < x:
|
||||
return
|
||||
self._addIntersection(y2 > y1)
|
||||
|
||||
def _curveToOne(self, bcp1, bcp2, point):
|
||||
x, y = self.testPoint
|
||||
x1, y1 = self._getCurrentPoint()
|
||||
x2, y2 = bcp1
|
||||
x3, y3 = bcp2
|
||||
x4, y4 = point
|
||||
|
||||
if x1 < x and x2 < x and x3 < x and x4 < x:
|
||||
return
|
||||
if y1 < y and y2 < y and y3 < y and y4 < y:
|
||||
return
|
||||
if y1 >= y and y2 >= y and y3 >= y and y4 >= y:
|
||||
return
|
||||
|
||||
dy = y1
|
||||
cy = (y2 - dy) * 3.0
|
||||
by = (y3 - y2) * 3.0 - cy
|
||||
ay = y4 - dy - cy - by
|
||||
solutions = sorted(solveCubic(ay, by, cy, dy - y))
|
||||
solutions = [t for t in solutions if -0.0 <= t <= 1.0]
|
||||
if not solutions:
|
||||
return
|
||||
|
||||
dx = x1
|
||||
cx = (x2 - dx) * 3.0
|
||||
bx = (x3 - x2) * 3.0 - cx
|
||||
ax = x4 - dx - cx - bx
|
||||
|
||||
above = y1 >= y
|
||||
lastT = None
|
||||
for t in solutions:
|
||||
if t == lastT:
|
||||
continue
|
||||
lastT = t
|
||||
t2 = t * t
|
||||
t3 = t2 * t
|
||||
|
||||
direction = 3 * ay * t2 + 2 * by * t + cy
|
||||
incomingGoingUp = outgoingGoingUp = direction > 0.0
|
||||
if direction == 0.0:
|
||||
direction = 6 * ay * t + 2 * by
|
||||
outgoingGoingUp = direction > 0.0
|
||||
incomingGoingUp = not outgoingGoingUp
|
||||
if direction == 0.0:
|
||||
direction = ay
|
||||
incomingGoingUp = outgoingGoingUp = direction > 0.0
|
||||
|
||||
xt = ax * t3 + bx * t2 + cx * t + dx
|
||||
if xt < x:
|
||||
continue
|
||||
|
||||
if t in (0.0, -0.0):
|
||||
if not outgoingGoingUp:
|
||||
self._addIntersection(outgoingGoingUp)
|
||||
elif t == 1.0:
|
||||
if incomingGoingUp:
|
||||
self._addIntersection(incomingGoingUp)
|
||||
else:
|
||||
if incomingGoingUp == outgoingGoingUp:
|
||||
self._addIntersection(outgoingGoingUp)
|
||||
# else:
|
||||
# we're not really intersecting, merely touching
|
||||
|
||||
def _qCurveToOne_unfinished(self, bcp, point):
|
||||
# XXX need to finish this, for now doing it through a cubic
|
||||
# (BasePen implements _qCurveTo in terms of a cubic) will
|
||||
# have to do.
|
||||
x, y = self.testPoint
|
||||
x1, y1 = self._getCurrentPoint()
|
||||
x2, y2 = bcp
|
||||
x3, y3 = point
|
||||
c = y1
|
||||
b = (y2 - c) * 2.0
|
||||
a = y3 - c - b
|
||||
solutions = sorted(solveQuadratic(a, b, c - y))
|
||||
solutions = [
|
||||
t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON
|
||||
]
|
||||
if not solutions:
|
||||
return
|
||||
# XXX
|
||||
|
||||
def _closePath(self):
|
||||
if self._getCurrentPoint() != self.firstPoint:
|
||||
self.lineTo(self.firstPoint)
|
||||
self.firstPoint = None
|
||||
|
||||
def _endPath(self):
|
||||
"""Insideness is not defined for open contours."""
|
||||
raise NotImplementedError
|
||||
@@ -1,600 +0,0 @@
|
||||
"""
|
||||
=========
|
||||
PointPens
|
||||
=========
|
||||
|
||||
Where **SegmentPens** have an intuitive approach to drawing
|
||||
(if you're familiar with postscript anyway), the **PointPen**
|
||||
is geared towards accessing all the data in the contours of
|
||||
the glyph. A PointPen has a very simple interface, it just
|
||||
steps through all the points in a call from glyph.drawPoints().
|
||||
This allows the caller to provide more data for each point.
|
||||
For instance, whether or not a point is smooth, and its name.
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import Any, Optional, Tuple, Dict
|
||||
|
||||
from fontTools.misc.loggingTools import LogMixin
|
||||
from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError
|
||||
from fontTools.misc.transform import DecomposedTransform, Identity
|
||||
|
||||
__all__ = [
|
||||
"AbstractPointPen",
|
||||
"BasePointToSegmentPen",
|
||||
"PointToSegmentPen",
|
||||
"SegmentToPointPen",
|
||||
"GuessSmoothPointPen",
|
||||
"ReverseContourPointPen",
|
||||
]
|
||||
|
||||
|
||||
class AbstractPointPen:
|
||||
"""Baseclass for all PointPens."""
|
||||
|
||||
def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
|
||||
"""Start a new sub path."""
|
||||
raise NotImplementedError
|
||||
|
||||
def endPath(self) -> None:
|
||||
"""End the current sub path."""
|
||||
raise NotImplementedError
|
||||
|
||||
def addPoint(
|
||||
self,
|
||||
pt: Tuple[float, float],
|
||||
segmentType: Optional[str] = None,
|
||||
smooth: bool = False,
|
||||
name: Optional[str] = None,
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Add a point to the current sub path."""
|
||||
raise NotImplementedError
|
||||
|
||||
def addComponent(
|
||||
self,
|
||||
baseGlyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float],
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Add a sub glyph."""
|
||||
raise NotImplementedError
|
||||
|
||||
def addVarComponent(
|
||||
self,
|
||||
glyphName: str,
|
||||
transformation: DecomposedTransform,
|
||||
location: Dict[str, float],
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Add a VarComponent sub glyph. The 'transformation' argument
|
||||
must be a DecomposedTransform from the fontTools.misc.transform module,
|
||||
and the 'location' argument must be a dictionary mapping axis tags
|
||||
to their locations.
|
||||
"""
|
||||
# ttGlyphSet decomposes for us
|
||||
raise AttributeError
|
||||
|
||||
|
||||
class BasePointToSegmentPen(AbstractPointPen):
|
||||
"""
|
||||
Base class for retrieving the outline in a segment-oriented
|
||||
way. The PointPen protocol is simple yet also a little tricky,
|
||||
so when you need an outline presented as segments but you have
|
||||
as points, do use this base implementation as it properly takes
|
||||
care of all the edge cases.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.currentPath = None
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
if self.currentPath is not None:
|
||||
raise PenError("Path already begun.")
|
||||
self.currentPath = []
|
||||
|
||||
def _flushContour(self, segments):
|
||||
"""Override this method.
|
||||
|
||||
It will be called for each non-empty sub path with a list
|
||||
of segments: the 'segments' argument.
|
||||
|
||||
The segments list contains tuples of length 2:
|
||||
(segmentType, points)
|
||||
|
||||
segmentType is one of "move", "line", "curve" or "qcurve".
|
||||
"move" may only occur as the first segment, and it signifies
|
||||
an OPEN path. A CLOSED path does NOT start with a "move", in
|
||||
fact it will not contain a "move" at ALL.
|
||||
|
||||
The 'points' field in the 2-tuple is a list of point info
|
||||
tuples. The list has 1 or more items, a point tuple has
|
||||
four items:
|
||||
(point, smooth, name, kwargs)
|
||||
'point' is an (x, y) coordinate pair.
|
||||
|
||||
For a closed path, the initial moveTo point is defined as
|
||||
the last point of the last segment.
|
||||
|
||||
The 'points' list of "move" and "line" segments always contains
|
||||
exactly one point tuple.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def endPath(self):
|
||||
if self.currentPath is None:
|
||||
raise PenError("Path not begun.")
|
||||
points = self.currentPath
|
||||
self.currentPath = None
|
||||
if not points:
|
||||
return
|
||||
if len(points) == 1:
|
||||
# Not much more we can do than output a single move segment.
|
||||
pt, segmentType, smooth, name, kwargs = points[0]
|
||||
segments = [("move", [(pt, smooth, name, kwargs)])]
|
||||
self._flushContour(segments)
|
||||
return
|
||||
segments = []
|
||||
if points[0][1] == "move":
|
||||
# It's an open contour, insert a "move" segment for the first
|
||||
# point and remove that first point from the point list.
|
||||
pt, segmentType, smooth, name, kwargs = points[0]
|
||||
segments.append(("move", [(pt, smooth, name, kwargs)]))
|
||||
points.pop(0)
|
||||
else:
|
||||
# It's a closed contour. Locate the first on-curve point, and
|
||||
# rotate the point list so that it _ends_ with an on-curve
|
||||
# point.
|
||||
firstOnCurve = None
|
||||
for i in range(len(points)):
|
||||
segmentType = points[i][1]
|
||||
if segmentType is not None:
|
||||
firstOnCurve = i
|
||||
break
|
||||
if firstOnCurve is None:
|
||||
# Special case for quadratics: a contour with no on-curve
|
||||
# points. Add a "None" point. (See also the Pen protocol's
|
||||
# qCurveTo() method and fontTools.pens.basePen.py.)
|
||||
points.append((None, "qcurve", None, None, None))
|
||||
else:
|
||||
points = points[firstOnCurve + 1 :] + points[: firstOnCurve + 1]
|
||||
|
||||
currentSegment = []
|
||||
for pt, segmentType, smooth, name, kwargs in points:
|
||||
currentSegment.append((pt, smooth, name, kwargs))
|
||||
if segmentType is None:
|
||||
continue
|
||||
segments.append((segmentType, currentSegment))
|
||||
currentSegment = []
|
||||
|
||||
self._flushContour(segments)
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
if self.currentPath is None:
|
||||
raise PenError("Path not begun")
|
||||
self.currentPath.append((pt, segmentType, smooth, name, kwargs))
|
||||
|
||||
|
||||
class PointToSegmentPen(BasePointToSegmentPen):
|
||||
"""
|
||||
Adapter class that converts the PointPen protocol to the
|
||||
(Segment)Pen protocol.
|
||||
|
||||
NOTE: The segment pen does not support and will drop point names, identifiers
|
||||
and kwargs.
|
||||
"""
|
||||
|
||||
def __init__(self, segmentPen, outputImpliedClosingLine=False):
|
||||
BasePointToSegmentPen.__init__(self)
|
||||
self.pen = segmentPen
|
||||
self.outputImpliedClosingLine = outputImpliedClosingLine
|
||||
|
||||
def _flushContour(self, segments):
|
||||
if not segments:
|
||||
raise PenError("Must have at least one segment.")
|
||||
pen = self.pen
|
||||
if segments[0][0] == "move":
|
||||
# It's an open path.
|
||||
closed = False
|
||||
points = segments[0][1]
|
||||
if len(points) != 1:
|
||||
raise PenError(f"Illegal move segment point count: {len(points)}")
|
||||
movePt, _, _, _ = points[0]
|
||||
del segments[0]
|
||||
else:
|
||||
# It's a closed path, do a moveTo to the last
|
||||
# point of the last segment.
|
||||
closed = True
|
||||
segmentType, points = segments[-1]
|
||||
movePt, _, _, _ = points[-1]
|
||||
if movePt is None:
|
||||
# quad special case: a contour with no on-curve points contains
|
||||
# one "qcurve" segment that ends with a point that's None. We
|
||||
# must not output a moveTo() in that case.
|
||||
pass
|
||||
else:
|
||||
pen.moveTo(movePt)
|
||||
outputImpliedClosingLine = self.outputImpliedClosingLine
|
||||
nSegments = len(segments)
|
||||
lastPt = movePt
|
||||
for i in range(nSegments):
|
||||
segmentType, points = segments[i]
|
||||
points = [pt for pt, _, _, _ in points]
|
||||
if segmentType == "line":
|
||||
if len(points) != 1:
|
||||
raise PenError(f"Illegal line segment point count: {len(points)}")
|
||||
pt = points[0]
|
||||
# For closed contours, a 'lineTo' is always implied from the last oncurve
|
||||
# point to the starting point, thus we can omit it when the last and
|
||||
# starting point don't overlap.
|
||||
# However, when the last oncurve point is a "line" segment and has same
|
||||
# coordinates as the starting point of a closed contour, we need to output
|
||||
# the closing 'lineTo' explicitly (regardless of the value of the
|
||||
# 'outputImpliedClosingLine' option) in order to disambiguate this case from
|
||||
# the implied closing 'lineTo', otherwise the duplicate point would be lost.
|
||||
# See https://github.com/googlefonts/fontmake/issues/572.
|
||||
if (
|
||||
i + 1 != nSegments
|
||||
or outputImpliedClosingLine
|
||||
or not closed
|
||||
or pt == lastPt
|
||||
):
|
||||
pen.lineTo(pt)
|
||||
lastPt = pt
|
||||
elif segmentType == "curve":
|
||||
pen.curveTo(*points)
|
||||
lastPt = points[-1]
|
||||
elif segmentType == "qcurve":
|
||||
pen.qCurveTo(*points)
|
||||
lastPt = points[-1]
|
||||
else:
|
||||
raise PenError(f"Illegal segmentType: {segmentType}")
|
||||
if closed:
|
||||
pen.closePath()
|
||||
else:
|
||||
pen.endPath()
|
||||
|
||||
def addComponent(self, glyphName, transform, identifier=None, **kwargs):
|
||||
del identifier # unused
|
||||
del kwargs # unused
|
||||
self.pen.addComponent(glyphName, transform)
|
||||
|
||||
|
||||
class SegmentToPointPen(AbstractPen):
|
||||
"""
|
||||
Adapter class that converts the (Segment)Pen protocol to the
|
||||
PointPen protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, pointPen, guessSmooth=True):
|
||||
if guessSmooth:
|
||||
self.pen = GuessSmoothPointPen(pointPen)
|
||||
else:
|
||||
self.pen = pointPen
|
||||
self.contour = None
|
||||
|
||||
def _flushContour(self):
|
||||
pen = self.pen
|
||||
pen.beginPath()
|
||||
for pt, segmentType in self.contour:
|
||||
pen.addPoint(pt, segmentType=segmentType)
|
||||
pen.endPath()
|
||||
|
||||
def moveTo(self, pt):
|
||||
self.contour = []
|
||||
self.contour.append((pt, "move"))
|
||||
|
||||
def lineTo(self, pt):
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
self.contour.append((pt, "line"))
|
||||
|
||||
def curveTo(self, *pts):
|
||||
if not pts:
|
||||
raise TypeError("Must pass in at least one point")
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
for pt in pts[:-1]:
|
||||
self.contour.append((pt, None))
|
||||
self.contour.append((pts[-1], "curve"))
|
||||
|
||||
def qCurveTo(self, *pts):
|
||||
if not pts:
|
||||
raise TypeError("Must pass in at least one point")
|
||||
if pts[-1] is None:
|
||||
self.contour = []
|
||||
else:
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
for pt in pts[:-1]:
|
||||
self.contour.append((pt, None))
|
||||
if pts[-1] is not None:
|
||||
self.contour.append((pts[-1], "qcurve"))
|
||||
|
||||
def closePath(self):
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]:
|
||||
self.contour[0] = self.contour[-1]
|
||||
del self.contour[-1]
|
||||
else:
|
||||
# There's an implied line at the end, replace "move" with "line"
|
||||
# for the first point
|
||||
pt, tp = self.contour[0]
|
||||
if tp == "move":
|
||||
self.contour[0] = pt, "line"
|
||||
self._flushContour()
|
||||
self.contour = None
|
||||
|
||||
def endPath(self):
|
||||
if self.contour is None:
|
||||
raise PenError("Contour missing required initial moveTo")
|
||||
self._flushContour()
|
||||
self.contour = None
|
||||
|
||||
def addComponent(self, glyphName, transform):
|
||||
if self.contour is not None:
|
||||
raise PenError("Components must be added before or after contours")
|
||||
self.pen.addComponent(glyphName, transform)
|
||||
|
||||
|
||||
class GuessSmoothPointPen(AbstractPointPen):
|
||||
"""
|
||||
Filtering PointPen that tries to determine whether an on-curve point
|
||||
should be "smooth", ie. that it's a "tangent" point or a "curve" point.
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, error=0.05):
|
||||
self._outPen = outPen
|
||||
self._error = error
|
||||
self._points = None
|
||||
|
||||
def _flushContour(self):
|
||||
if self._points is None:
|
||||
raise PenError("Path not begun")
|
||||
points = self._points
|
||||
nPoints = len(points)
|
||||
if not nPoints:
|
||||
return
|
||||
if points[0][1] == "move":
|
||||
# Open path.
|
||||
indices = range(1, nPoints - 1)
|
||||
elif nPoints > 1:
|
||||
# Closed path. To avoid having to mod the contour index, we
|
||||
# simply abuse Python's negative index feature, and start at -1
|
||||
indices = range(-1, nPoints - 1)
|
||||
else:
|
||||
# closed path containing 1 point (!), ignore.
|
||||
indices = []
|
||||
for i in indices:
|
||||
pt, segmentType, _, name, kwargs = points[i]
|
||||
if segmentType is None:
|
||||
continue
|
||||
prev = i - 1
|
||||
next = i + 1
|
||||
if points[prev][1] is not None and points[next][1] is not None:
|
||||
continue
|
||||
# At least one of our neighbors is an off-curve point
|
||||
pt = points[i][0]
|
||||
prevPt = points[prev][0]
|
||||
nextPt = points[next][0]
|
||||
if pt != prevPt and pt != nextPt:
|
||||
dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1]
|
||||
dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1]
|
||||
a1 = math.atan2(dy1, dx1)
|
||||
a2 = math.atan2(dy2, dx2)
|
||||
if abs(a1 - a2) < self._error:
|
||||
points[i] = pt, segmentType, True, name, kwargs
|
||||
|
||||
for pt, segmentType, smooth, name, kwargs in points:
|
||||
self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
if self._points is not None:
|
||||
raise PenError("Path already begun")
|
||||
self._points = []
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self._outPen.beginPath(**kwargs)
|
||||
|
||||
def endPath(self):
|
||||
self._flushContour()
|
||||
self._outPen.endPath()
|
||||
self._points = None
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
if self._points is None:
|
||||
raise PenError("Path not begun")
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self._points.append((pt, segmentType, False, name, kwargs))
|
||||
|
||||
def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
|
||||
if self._points is not None:
|
||||
raise PenError("Components must be added before or after contours")
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self._outPen.addComponent(glyphName, transformation, **kwargs)
|
||||
|
||||
def addVarComponent(
|
||||
self, glyphName, transformation, location, identifier=None, **kwargs
|
||||
):
|
||||
if self._points is not None:
|
||||
raise PenError("VarComponents must be added before or after contours")
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self._outPen.addVarComponent(glyphName, transformation, location, **kwargs)
|
||||
|
||||
|
||||
class ReverseContourPointPen(AbstractPointPen):
|
||||
"""
|
||||
This is a PointPen that passes outline data to another PointPen, but
|
||||
reversing the winding direction of all contours. Components are simply
|
||||
passed through unchanged.
|
||||
|
||||
Closed contours are reversed in such a way that the first point remains
|
||||
the first point.
|
||||
"""
|
||||
|
||||
def __init__(self, outputPointPen):
|
||||
self.pen = outputPointPen
|
||||
# a place to store the points for the current sub path
|
||||
self.currentContour = None
|
||||
|
||||
def _flushContour(self):
|
||||
pen = self.pen
|
||||
contour = self.currentContour
|
||||
if not contour:
|
||||
pen.beginPath(identifier=self.currentContourIdentifier)
|
||||
pen.endPath()
|
||||
return
|
||||
|
||||
closed = contour[0][1] != "move"
|
||||
if not closed:
|
||||
lastSegmentType = "move"
|
||||
else:
|
||||
# Remove the first point and insert it at the end. When
|
||||
# the list of points gets reversed, this point will then
|
||||
# again be at the start. In other words, the following
|
||||
# will hold:
|
||||
# for N in range(len(originalContour)):
|
||||
# originalContour[N] == reversedContour[-N]
|
||||
contour.append(contour.pop(0))
|
||||
# Find the first on-curve point.
|
||||
firstOnCurve = None
|
||||
for i in range(len(contour)):
|
||||
if contour[i][1] is not None:
|
||||
firstOnCurve = i
|
||||
break
|
||||
if firstOnCurve is None:
|
||||
# There are no on-curve points, be basically have to
|
||||
# do nothing but contour.reverse().
|
||||
lastSegmentType = None
|
||||
else:
|
||||
lastSegmentType = contour[firstOnCurve][1]
|
||||
|
||||
contour.reverse()
|
||||
if not closed:
|
||||
# Open paths must start with a move, so we simply dump
|
||||
# all off-curve points leading up to the first on-curve.
|
||||
while contour[0][1] is None:
|
||||
contour.pop(0)
|
||||
pen.beginPath(identifier=self.currentContourIdentifier)
|
||||
for pt, nextSegmentType, smooth, name, kwargs in contour:
|
||||
if nextSegmentType is not None:
|
||||
segmentType = lastSegmentType
|
||||
lastSegmentType = nextSegmentType
|
||||
else:
|
||||
segmentType = None
|
||||
pen.addPoint(
|
||||
pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs
|
||||
)
|
||||
pen.endPath()
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
if self.currentContour is not None:
|
||||
raise PenError("Path already begun")
|
||||
self.currentContour = []
|
||||
self.currentContourIdentifier = identifier
|
||||
self.onCurve = []
|
||||
|
||||
def endPath(self):
|
||||
if self.currentContour is None:
|
||||
raise PenError("Path not begun")
|
||||
self._flushContour()
|
||||
self.currentContour = None
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
if self.currentContour is None:
|
||||
raise PenError("Path not begun")
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.currentContour.append((pt, segmentType, smooth, name, kwargs))
|
||||
|
||||
def addComponent(self, glyphName, transform, identifier=None, **kwargs):
|
||||
if self.currentContour is not None:
|
||||
raise PenError("Components must be added before or after contours")
|
||||
self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs)
|
||||
|
||||
|
||||
class DecomposingPointPen(LogMixin, AbstractPointPen):
|
||||
"""Implements a 'addComponent' method that decomposes components
|
||||
(i.e. draws them onto self as simple contours).
|
||||
It can also be used as a mixin class (e.g. see DecomposingRecordingPointPen).
|
||||
|
||||
You must override beginPath, addPoint, endPath. You may
|
||||
additionally override addVarComponent and addComponent.
|
||||
|
||||
By default a warning message is logged when a base glyph is missing;
|
||||
set the class variable ``skipMissingComponents`` to False if you want
|
||||
all instances of a sub-class to raise a :class:`MissingComponentError`
|
||||
exception by default.
|
||||
"""
|
||||
|
||||
skipMissingComponents = True
|
||||
# alias error for convenience
|
||||
MissingComponentError = MissingComponentError
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
glyphSet,
|
||||
*args,
|
||||
skipMissingComponents=None,
|
||||
reverseFlipped=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
|
||||
as components are looked up by their name.
|
||||
|
||||
If the optional 'reverseFlipped' argument is True, components whose transformation
|
||||
matrix has a negative determinant will be decomposed with a reversed path direction
|
||||
to compensate for the flip.
|
||||
|
||||
The optional 'skipMissingComponents' argument can be set to True/False to
|
||||
override the homonymous class attribute for a given pen instance.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.glyphSet = glyphSet
|
||||
self.skipMissingComponents = (
|
||||
self.__class__.skipMissingComponents
|
||||
if skipMissingComponents is None
|
||||
else skipMissingComponents
|
||||
)
|
||||
self.reverseFlipped = reverseFlipped
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
|
||||
"""Transform the points of the base glyph and draw it onto self.
|
||||
|
||||
The `identifier` parameter and any extra kwargs are ignored.
|
||||
"""
|
||||
from fontTools.pens.transformPen import TransformPointPen
|
||||
|
||||
try:
|
||||
glyph = self.glyphSet[baseGlyphName]
|
||||
except KeyError:
|
||||
if not self.skipMissingComponents:
|
||||
raise MissingComponentError(baseGlyphName)
|
||||
self.log.warning(
|
||||
"glyph '%s' is missing from glyphSet; skipped" % baseGlyphName
|
||||
)
|
||||
else:
|
||||
pen = self
|
||||
if transformation != Identity:
|
||||
pen = TransformPointPen(pen, transformation)
|
||||
if self.reverseFlipped:
|
||||
# if the transformation has a negative determinant, it will
|
||||
# reverse the contour direction of the component
|
||||
a, b, c, d = transformation[:4]
|
||||
det = a * d - b * c
|
||||
if a * d - b * c < 0:
|
||||
pen = ReverseContourPointPen(pen)
|
||||
glyph.drawPoints(pen)
|
||||
@@ -1,29 +0,0 @@
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["QtPen"]
|
||||
|
||||
|
||||
class QtPen(BasePen):
|
||||
def __init__(self, glyphSet, path=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
from PyQt5.QtGui import QPainterPath
|
||||
|
||||
path = QPainterPath()
|
||||
self.path = path
|
||||
|
||||
def _moveTo(self, p):
|
||||
self.path.moveTo(*p)
|
||||
|
||||
def _lineTo(self, p):
|
||||
self.path.lineTo(*p)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
self.path.cubicTo(*p1, *p2, *p3)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
self.path.quadTo(*p1, *p2)
|
||||
|
||||
def _closePath(self):
|
||||
self.path.closeSubpath()
|
||||
@@ -1,105 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
# Copyright 2023 Behdad Esfahbod. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from fontTools.qu2cu import quadratic_to_curves
|
||||
from fontTools.pens.filterPen import ContourFilterPen
|
||||
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||
import math
|
||||
|
||||
|
||||
class Qu2CuPen(ContourFilterPen):
|
||||
"""A filter pen to convert quadratic bezier splines to cubic curves
|
||||
using the FontTools SegmentPen protocol.
|
||||
|
||||
Args:
|
||||
|
||||
other_pen: another SegmentPen used to draw the transformed outline.
|
||||
max_err: maximum approximation error in font units. For optimal results,
|
||||
if you know the UPEM of the font, we recommend setting this to a
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: flip the contours' direction but keep starting point.
|
||||
stats: a dictionary counting the point numbers of cubic segments.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
other_pen,
|
||||
max_err,
|
||||
all_cubic=False,
|
||||
reverse_direction=False,
|
||||
stats=None,
|
||||
):
|
||||
if reverse_direction:
|
||||
other_pen = ReverseContourPen(other_pen)
|
||||
super().__init__(other_pen)
|
||||
self.all_cubic = all_cubic
|
||||
self.max_err = max_err
|
||||
self.stats = stats
|
||||
|
||||
def _quadratics_to_curve(self, q):
|
||||
curves = quadratic_to_curves(q, self.max_err, all_cubic=self.all_cubic)
|
||||
if self.stats is not None:
|
||||
for curve in curves:
|
||||
n = str(len(curve) - 2)
|
||||
self.stats[n] = self.stats.get(n, 0) + 1
|
||||
for curve in curves:
|
||||
if len(curve) == 4:
|
||||
yield ("curveTo", curve[1:])
|
||||
else:
|
||||
yield ("qCurveTo", curve[1:])
|
||||
|
||||
def filterContour(self, contour):
|
||||
quadratics = []
|
||||
currentPt = None
|
||||
newContour = []
|
||||
for op, args in contour:
|
||||
if op == "qCurveTo" and (
|
||||
self.all_cubic or (len(args) > 2 and args[-1] is not None)
|
||||
):
|
||||
if args[-1] is None:
|
||||
raise NotImplementedError(
|
||||
"oncurve-less contours with all_cubic not implemented"
|
||||
)
|
||||
quadratics.append((currentPt,) + args)
|
||||
else:
|
||||
if quadratics:
|
||||
newContour.extend(self._quadratics_to_curve(quadratics))
|
||||
quadratics = []
|
||||
newContour.append((op, args))
|
||||
currentPt = args[-1] if args else None
|
||||
if quadratics:
|
||||
newContour.extend(self._quadratics_to_curve(quadratics))
|
||||
|
||||
if not self.all_cubic:
|
||||
# Add back implicit oncurve points
|
||||
contour = newContour
|
||||
newContour = []
|
||||
for op, args in contour:
|
||||
if op == "qCurveTo" and newContour and newContour[-1][0] == "qCurveTo":
|
||||
pt0 = newContour[-1][1][-2]
|
||||
pt1 = newContour[-1][1][-1]
|
||||
pt2 = args[0]
|
||||
if (
|
||||
pt1 is not None
|
||||
and math.isclose(pt2[0] - pt1[0], pt1[0] - pt0[0])
|
||||
and math.isclose(pt2[1] - pt1[1], pt1[1] - pt0[1])
|
||||
):
|
||||
newArgs = newContour[-1][1][:-1] + args
|
||||
newContour[-1] = (op, newArgs)
|
||||
continue
|
||||
|
||||
newContour.append((op, args))
|
||||
|
||||
return newContour
|
||||
@@ -1,43 +0,0 @@
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
from Quartz.CoreGraphics import CGPathCreateMutable, CGPathMoveToPoint
|
||||
from Quartz.CoreGraphics import CGPathAddLineToPoint, CGPathAddCurveToPoint
|
||||
from Quartz.CoreGraphics import CGPathAddQuadCurveToPoint, CGPathCloseSubpath
|
||||
|
||||
|
||||
__all__ = ["QuartzPen"]
|
||||
|
||||
|
||||
class QuartzPen(BasePen):
|
||||
"""A pen that creates a CGPath
|
||||
|
||||
Parameters
|
||||
- path: an optional CGPath to add to
|
||||
- xform: an optional CGAffineTransform to apply to the path
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet, path=None, xform=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
path = CGPathCreateMutable()
|
||||
self.path = path
|
||||
self.xform = xform
|
||||
|
||||
def _moveTo(self, pt):
|
||||
x, y = pt
|
||||
CGPathMoveToPoint(self.path, self.xform, x, y)
|
||||
|
||||
def _lineTo(self, pt):
|
||||
x, y = pt
|
||||
CGPathAddLineToPoint(self.path, self.xform, x, y)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
(x1, y1), (x2, y2), (x3, y3) = p1, p2, p3
|
||||
CGPathAddCurveToPoint(self.path, self.xform, x1, y1, x2, y2, x3, y3)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
(x1, y1), (x2, y2) = p1, p2
|
||||
CGPathAddQuadCurveToPoint(self.path, self.xform, x1, y1, x2, y2)
|
||||
|
||||
def _closePath(self):
|
||||
CGPathCloseSubpath(self.path)
|
||||
@@ -1,331 +0,0 @@
|
||||
"""Pen recording operations that can be accessed or replayed."""
|
||||
|
||||
from fontTools.pens.basePen import AbstractPen, DecomposingPen
|
||||
from fontTools.pens.pointPen import AbstractPointPen, DecomposingPointPen
|
||||
|
||||
|
||||
__all__ = [
|
||||
"replayRecording",
|
||||
"RecordingPen",
|
||||
"DecomposingRecordingPen",
|
||||
"DecomposingRecordingPointPen",
|
||||
"RecordingPointPen",
|
||||
"lerpRecordings",
|
||||
]
|
||||
|
||||
|
||||
def replayRecording(recording, pen):
|
||||
"""Replay a recording, as produced by RecordingPen or DecomposingRecordingPen,
|
||||
to a pen.
|
||||
|
||||
Note that recording does not have to be produced by those pens.
|
||||
It can be any iterable of tuples of method name and tuple-of-arguments.
|
||||
Likewise, pen can be any objects receiving those method calls.
|
||||
"""
|
||||
for operator, operands in recording:
|
||||
getattr(pen, operator)(*operands)
|
||||
|
||||
|
||||
class RecordingPen(AbstractPen):
|
||||
"""Pen recording operations that can be accessed or replayed.
|
||||
|
||||
The recording can be accessed as pen.value; or replayed using
|
||||
pen.replay(otherPen).
|
||||
|
||||
:Example:
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.recordingPen import RecordingPen
|
||||
|
||||
glyph_name = 'dollar'
|
||||
font_path = 'MyFont.otf'
|
||||
|
||||
font = TTFont(font_path)
|
||||
glyphset = font.getGlyphSet()
|
||||
glyph = glyphset[glyph_name]
|
||||
|
||||
pen = RecordingPen()
|
||||
glyph.draw(pen)
|
||||
print(pen.value)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.value = []
|
||||
|
||||
def moveTo(self, p0):
|
||||
self.value.append(("moveTo", (p0,)))
|
||||
|
||||
def lineTo(self, p1):
|
||||
self.value.append(("lineTo", (p1,)))
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
self.value.append(("qCurveTo", points))
|
||||
|
||||
def curveTo(self, *points):
|
||||
self.value.append(("curveTo", points))
|
||||
|
||||
def closePath(self):
|
||||
self.value.append(("closePath", ()))
|
||||
|
||||
def endPath(self):
|
||||
self.value.append(("endPath", ()))
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
self.value.append(("addComponent", (glyphName, transformation)))
|
||||
|
||||
def addVarComponent(self, glyphName, transformation, location):
|
||||
self.value.append(("addVarComponent", (glyphName, transformation, location)))
|
||||
|
||||
def replay(self, pen):
|
||||
replayRecording(self.value, pen)
|
||||
|
||||
draw = replay
|
||||
|
||||
|
||||
class DecomposingRecordingPen(DecomposingPen, RecordingPen):
|
||||
"""Same as RecordingPen, except that it doesn't keep components
|
||||
as references, but draws them decomposed as regular contours.
|
||||
|
||||
The constructor takes a required 'glyphSet' positional argument,
|
||||
a dictionary of glyph objects (i.e. with a 'draw' method) keyed
|
||||
by thir name; other arguments are forwarded to the DecomposingPen's
|
||||
constructor::
|
||||
|
||||
>>> class SimpleGlyph(object):
|
||||
... def draw(self, pen):
|
||||
... pen.moveTo((0, 0))
|
||||
... pen.curveTo((1, 1), (2, 2), (3, 3))
|
||||
... pen.closePath()
|
||||
>>> class CompositeGlyph(object):
|
||||
... def draw(self, pen):
|
||||
... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
|
||||
>>> class MissingComponent(object):
|
||||
... def draw(self, pen):
|
||||
... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0))
|
||||
>>> class FlippedComponent(object):
|
||||
... def draw(self, pen):
|
||||
... pen.addComponent('a', (-1, 0, 0, 1, 0, 0))
|
||||
>>> glyphSet = {
|
||||
... 'a': SimpleGlyph(),
|
||||
... 'b': CompositeGlyph(),
|
||||
... 'c': MissingComponent(),
|
||||
... 'd': FlippedComponent(),
|
||||
... }
|
||||
>>> for name, glyph in sorted(glyphSet.items()):
|
||||
... pen = DecomposingRecordingPen(glyphSet)
|
||||
... try:
|
||||
... glyph.draw(pen)
|
||||
... except pen.MissingComponentError:
|
||||
... pass
|
||||
... print("{}: {}".format(name, pen.value))
|
||||
a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
|
||||
b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
|
||||
c: []
|
||||
d: [('moveTo', ((0, 0),)), ('curveTo', ((-1, 1), (-2, 2), (-3, 3))), ('closePath', ())]
|
||||
>>> for name, glyph in sorted(glyphSet.items()):
|
||||
... pen = DecomposingRecordingPen(
|
||||
... glyphSet, skipMissingComponents=True, reverseFlipped=True,
|
||||
... )
|
||||
... glyph.draw(pen)
|
||||
... print("{}: {}".format(name, pen.value))
|
||||
a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
|
||||
b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
|
||||
c: []
|
||||
d: [('moveTo', ((0, 0),)), ('lineTo', ((-3, 3),)), ('curveTo', ((-2, 2), (-1, 1), (0, 0))), ('closePath', ())]
|
||||
"""
|
||||
|
||||
# raises MissingComponentError(KeyError) if base glyph is not found in glyphSet
|
||||
skipMissingComponents = False
|
||||
|
||||
|
||||
class RecordingPointPen(AbstractPointPen):
|
||||
"""PointPen recording operations that can be accessed or replayed.
|
||||
|
||||
The recording can be accessed as pen.value; or replayed using
|
||||
pointPen.replay(otherPointPen).
|
||||
|
||||
:Example:
|
||||
|
||||
from defcon import Font
|
||||
from fontTools.pens.recordingPen import RecordingPointPen
|
||||
|
||||
glyph_name = 'a'
|
||||
font_path = 'MyFont.ufo'
|
||||
|
||||
font = Font(font_path)
|
||||
glyph = font[glyph_name]
|
||||
|
||||
pen = RecordingPointPen()
|
||||
glyph.drawPoints(pen)
|
||||
print(pen.value)
|
||||
|
||||
new_glyph = font.newGlyph('b')
|
||||
pen.replay(new_glyph.getPointPen())
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.value = []
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.value.append(("beginPath", (), kwargs))
|
||||
|
||||
def endPath(self):
|
||||
self.value.append(("endPath", (), {}))
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs))
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.value.append(("addComponent", (baseGlyphName, transformation), kwargs))
|
||||
|
||||
def addVarComponent(
|
||||
self, baseGlyphName, transformation, location, identifier=None, **kwargs
|
||||
):
|
||||
if identifier is not None:
|
||||
kwargs["identifier"] = identifier
|
||||
self.value.append(
|
||||
("addVarComponent", (baseGlyphName, transformation, location), kwargs)
|
||||
)
|
||||
|
||||
def replay(self, pointPen):
|
||||
for operator, args, kwargs in self.value:
|
||||
getattr(pointPen, operator)(*args, **kwargs)
|
||||
|
||||
drawPoints = replay
|
||||
|
||||
|
||||
class DecomposingRecordingPointPen(DecomposingPointPen, RecordingPointPen):
|
||||
"""Same as RecordingPointPen, except that it doesn't keep components
|
||||
as references, but draws them decomposed as regular contours.
|
||||
|
||||
The constructor takes a required 'glyphSet' positional argument,
|
||||
a dictionary of pointPen-drawable glyph objects (i.e. with a 'drawPoints' method)
|
||||
keyed by thir name; other arguments are forwarded to the DecomposingPointPen's
|
||||
constructor::
|
||||
|
||||
>>> from pprint import pprint
|
||||
>>> class SimpleGlyph(object):
|
||||
... def drawPoints(self, pen):
|
||||
... pen.beginPath()
|
||||
... pen.addPoint((0, 0), "line")
|
||||
... pen.addPoint((1, 1))
|
||||
... pen.addPoint((2, 2))
|
||||
... pen.addPoint((3, 3), "curve")
|
||||
... pen.endPath()
|
||||
>>> class CompositeGlyph(object):
|
||||
... def drawPoints(self, pen):
|
||||
... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
|
||||
>>> class MissingComponent(object):
|
||||
... def drawPoints(self, pen):
|
||||
... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0))
|
||||
>>> class FlippedComponent(object):
|
||||
... def drawPoints(self, pen):
|
||||
... pen.addComponent('a', (-1, 0, 0, 1, 0, 0))
|
||||
>>> glyphSet = {
|
||||
... 'a': SimpleGlyph(),
|
||||
... 'b': CompositeGlyph(),
|
||||
... 'c': MissingComponent(),
|
||||
... 'd': FlippedComponent(),
|
||||
... }
|
||||
>>> for name, glyph in sorted(glyphSet.items()):
|
||||
... pen = DecomposingRecordingPointPen(glyphSet)
|
||||
... try:
|
||||
... glyph.drawPoints(pen)
|
||||
... except pen.MissingComponentError:
|
||||
... pass
|
||||
... pprint({name: pen.value})
|
||||
{'a': [('beginPath', (), {}),
|
||||
('addPoint', ((0, 0), 'line', False, None), {}),
|
||||
('addPoint', ((1, 1), None, False, None), {}),
|
||||
('addPoint', ((2, 2), None, False, None), {}),
|
||||
('addPoint', ((3, 3), 'curve', False, None), {}),
|
||||
('endPath', (), {})]}
|
||||
{'b': [('beginPath', (), {}),
|
||||
('addPoint', ((-1, 1), 'line', False, None), {}),
|
||||
('addPoint', ((0, 2), None, False, None), {}),
|
||||
('addPoint', ((1, 3), None, False, None), {}),
|
||||
('addPoint', ((2, 4), 'curve', False, None), {}),
|
||||
('endPath', (), {})]}
|
||||
{'c': []}
|
||||
{'d': [('beginPath', (), {}),
|
||||
('addPoint', ((0, 0), 'line', False, None), {}),
|
||||
('addPoint', ((-1, 1), None, False, None), {}),
|
||||
('addPoint', ((-2, 2), None, False, None), {}),
|
||||
('addPoint', ((-3, 3), 'curve', False, None), {}),
|
||||
('endPath', (), {})]}
|
||||
>>> for name, glyph in sorted(glyphSet.items()):
|
||||
... pen = DecomposingRecordingPointPen(
|
||||
... glyphSet, skipMissingComponents=True, reverseFlipped=True,
|
||||
... )
|
||||
... glyph.drawPoints(pen)
|
||||
... pprint({name: pen.value})
|
||||
{'a': [('beginPath', (), {}),
|
||||
('addPoint', ((0, 0), 'line', False, None), {}),
|
||||
('addPoint', ((1, 1), None, False, None), {}),
|
||||
('addPoint', ((2, 2), None, False, None), {}),
|
||||
('addPoint', ((3, 3), 'curve', False, None), {}),
|
||||
('endPath', (), {})]}
|
||||
{'b': [('beginPath', (), {}),
|
||||
('addPoint', ((-1, 1), 'line', False, None), {}),
|
||||
('addPoint', ((0, 2), None, False, None), {}),
|
||||
('addPoint', ((1, 3), None, False, None), {}),
|
||||
('addPoint', ((2, 4), 'curve', False, None), {}),
|
||||
('endPath', (), {})]}
|
||||
{'c': []}
|
||||
{'d': [('beginPath', (), {}),
|
||||
('addPoint', ((0, 0), 'curve', False, None), {}),
|
||||
('addPoint', ((-3, 3), 'line', False, None), {}),
|
||||
('addPoint', ((-2, 2), None, False, None), {}),
|
||||
('addPoint', ((-1, 1), None, False, None), {}),
|
||||
('endPath', (), {})]}
|
||||
"""
|
||||
|
||||
# raises MissingComponentError(KeyError) if base glyph is not found in glyphSet
|
||||
skipMissingComponents = False
|
||||
|
||||
|
||||
def lerpRecordings(recording1, recording2, factor=0.5):
|
||||
"""Linearly interpolate between two recordings. The recordings
|
||||
must be decomposed, i.e. they must not contain any components.
|
||||
|
||||
Factor is typically between 0 and 1. 0 means the first recording,
|
||||
1 means the second recording, and 0.5 means the average of the
|
||||
two recordings. Other values are possible, and can be useful to
|
||||
extrapolate. Defaults to 0.5.
|
||||
|
||||
Returns a generator with the new recording.
|
||||
"""
|
||||
if len(recording1) != len(recording2):
|
||||
raise ValueError(
|
||||
"Mismatched lengths: %d and %d" % (len(recording1), len(recording2))
|
||||
)
|
||||
for (op1, args1), (op2, args2) in zip(recording1, recording2):
|
||||
if op1 != op2:
|
||||
raise ValueError("Mismatched operations: %s, %s" % (op1, op2))
|
||||
if op1 == "addComponent":
|
||||
raise ValueError("Cannot interpolate components")
|
||||
else:
|
||||
mid_args = [
|
||||
(x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
|
||||
for (x1, y1), (x2, y2) in zip(args1, args2)
|
||||
]
|
||||
yield (op1, mid_args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pen = RecordingPen()
|
||||
pen.moveTo((0, 0))
|
||||
pen.lineTo((0, 100))
|
||||
pen.curveTo((50, 75), (60, 50), (50, 25))
|
||||
pen.closePath()
|
||||
from pprint import pprint
|
||||
|
||||
pprint(pen.value)
|
||||
@@ -1,79 +0,0 @@
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from reportlab.graphics.shapes import Path
|
||||
|
||||
|
||||
__all__ = ["ReportLabPen"]
|
||||
|
||||
|
||||
class ReportLabPen(BasePen):
|
||||
"""A pen for drawing onto a ``reportlab.graphics.shapes.Path`` object."""
|
||||
|
||||
def __init__(self, glyphSet, path=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
path = Path()
|
||||
self.path = path
|
||||
|
||||
def _moveTo(self, p):
|
||||
(x, y) = p
|
||||
self.path.moveTo(x, y)
|
||||
|
||||
def _lineTo(self, p):
|
||||
(x, y) = p
|
||||
self.path.lineTo(x, y)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
(x1, y1) = p1
|
||||
(x2, y2) = p2
|
||||
(x3, y3) = p3
|
||||
self.path.curveTo(x1, y1, x2, y2, x3, y3)
|
||||
|
||||
def _closePath(self):
|
||||
self.path.closePath()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print(
|
||||
"Usage: reportLabPen.py <OTF/TTF font> <glyphname> [<image file to create>]"
|
||||
)
|
||||
print(
|
||||
" If no image file name is created, by default <glyphname>.png is created."
|
||||
)
|
||||
print(" example: reportLabPen.py Arial.TTF R test.png")
|
||||
print(
|
||||
" (The file format will be PNG, regardless of the image file name supplied)"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
from reportlab.lib import colors
|
||||
|
||||
path = sys.argv[1]
|
||||
glyphName = sys.argv[2]
|
||||
if len(sys.argv) > 3:
|
||||
imageFile = sys.argv[3]
|
||||
else:
|
||||
imageFile = "%s.png" % glyphName
|
||||
|
||||
font = TTFont(path) # it would work just as well with fontTools.t1Lib.T1Font
|
||||
gs = font.getGlyphSet()
|
||||
pen = ReportLabPen(gs, Path(fillColor=colors.red, strokeWidth=5))
|
||||
g = gs[glyphName]
|
||||
g.draw(pen)
|
||||
|
||||
w, h = g.width, 1000
|
||||
from reportlab.graphics import renderPM
|
||||
from reportlab.graphics.shapes import Group, Drawing, scale
|
||||
|
||||
# Everything is wrapped in a group to allow transformations.
|
||||
g = Group(pen.path)
|
||||
g.translate(0, 200)
|
||||
g.scale(0.3, 0.3)
|
||||
|
||||
d = Drawing(w, h)
|
||||
d.add(g)
|
||||
|
||||
renderPM.drawToFile(d, imageFile, fmt="PNG")
|
||||
@@ -1,96 +0,0 @@
|
||||
from fontTools.misc.arrayTools import pairwise
|
||||
from fontTools.pens.filterPen import ContourFilterPen
|
||||
|
||||
|
||||
__all__ = ["reversedContour", "ReverseContourPen"]
|
||||
|
||||
|
||||
class ReverseContourPen(ContourFilterPen):
|
||||
"""Filter pen that passes outline data to another pen, but reversing
|
||||
the winding direction of all contours. Components are simply passed
|
||||
through unchanged.
|
||||
|
||||
Closed contours are reversed in such a way that the first point remains
|
||||
the first point.
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, outputImpliedClosingLine=False):
|
||||
super().__init__(outPen)
|
||||
self.outputImpliedClosingLine = outputImpliedClosingLine
|
||||
|
||||
def filterContour(self, contour):
|
||||
return reversedContour(contour, self.outputImpliedClosingLine)
|
||||
|
||||
|
||||
def reversedContour(contour, outputImpliedClosingLine=False):
|
||||
"""Generator that takes a list of pen's (operator, operands) tuples,
|
||||
and yields them with the winding direction reversed.
|
||||
"""
|
||||
if not contour:
|
||||
return # nothing to do, stop iteration
|
||||
|
||||
# valid contours must have at least a starting and ending command,
|
||||
# can't have one without the other
|
||||
assert len(contour) > 1, "invalid contour"
|
||||
|
||||
# the type of the last command determines if the contour is closed
|
||||
contourType = contour.pop()[0]
|
||||
assert contourType in ("endPath", "closePath")
|
||||
closed = contourType == "closePath"
|
||||
|
||||
firstType, firstPts = contour.pop(0)
|
||||
assert firstType in ("moveTo", "qCurveTo"), (
|
||||
"invalid initial segment type: %r" % firstType
|
||||
)
|
||||
firstOnCurve = firstPts[-1]
|
||||
if firstType == "qCurveTo":
|
||||
# special case for TrueType paths contaning only off-curve points
|
||||
assert firstOnCurve is None, "off-curve only paths must end with 'None'"
|
||||
assert not contour, "only one qCurveTo allowed per off-curve path"
|
||||
firstPts = (firstPts[0],) + tuple(reversed(firstPts[1:-1])) + (None,)
|
||||
|
||||
if not contour:
|
||||
# contour contains only one segment, nothing to reverse
|
||||
if firstType == "moveTo":
|
||||
closed = False # single-point paths can't be closed
|
||||
else:
|
||||
closed = True # off-curve paths are closed by definition
|
||||
yield firstType, firstPts
|
||||
else:
|
||||
lastType, lastPts = contour[-1]
|
||||
lastOnCurve = lastPts[-1]
|
||||
if closed:
|
||||
# for closed paths, we keep the starting point
|
||||
yield firstType, firstPts
|
||||
if firstOnCurve != lastOnCurve:
|
||||
# emit an implied line between the last and first points
|
||||
yield "lineTo", (lastOnCurve,)
|
||||
contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,))
|
||||
|
||||
if len(contour) > 1:
|
||||
secondType, secondPts = contour[0]
|
||||
else:
|
||||
# contour has only two points, the second and last are the same
|
||||
secondType, secondPts = lastType, lastPts
|
||||
|
||||
if not outputImpliedClosingLine:
|
||||
# if a lineTo follows the initial moveTo, after reversing it
|
||||
# will be implied by the closePath, so we don't emit one;
|
||||
# unless the lineTo and moveTo overlap, in which case we keep the
|
||||
# duplicate points
|
||||
if secondType == "lineTo" and firstPts != secondPts:
|
||||
del contour[0]
|
||||
if contour:
|
||||
contour[-1] = (lastType, tuple(lastPts[:-1]) + secondPts)
|
||||
else:
|
||||
# for open paths, the last point will become the first
|
||||
yield firstType, (lastOnCurve,)
|
||||
contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,))
|
||||
|
||||
# we iterate over all segment pairs in reverse order, and yield
|
||||
# each one with the off-curve points reversed (if any), and
|
||||
# with the on-curve point of the following segment
|
||||
for (curType, curPts), (_, nextPts) in pairwise(contour, reverse=True):
|
||||
yield curType, tuple(reversed(curPts[:-1])) + (nextPts[-1],)
|
||||
|
||||
yield "closePath" if closed else "endPath", ()
|
||||
@@ -1,130 +0,0 @@
|
||||
from fontTools.misc.roundTools import noRound, otRound
|
||||
from fontTools.misc.transform import Transform
|
||||
from fontTools.pens.filterPen import FilterPen, FilterPointPen
|
||||
|
||||
|
||||
__all__ = ["RoundingPen", "RoundingPointPen"]
|
||||
|
||||
|
||||
class RoundingPen(FilterPen):
|
||||
"""
|
||||
Filter pen that rounds point coordinates and component XY offsets to integer. For
|
||||
rounding the component transform values, a separate round function can be passed to
|
||||
the pen.
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPen
|
||||
>>> recpen = RecordingPen()
|
||||
>>> roundpen = RoundingPen(recpen)
|
||||
>>> roundpen.moveTo((0.4, 0.6))
|
||||
>>> roundpen.lineTo((1.6, 2.5))
|
||||
>>> roundpen.qCurveTo((2.4, 4.6), (3.3, 5.7), (4.9, 6.1))
|
||||
>>> roundpen.curveTo((6.4, 8.6), (7.3, 9.7), (8.9, 10.1))
|
||||
>>> roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5))
|
||||
>>> recpen.value == [
|
||||
... ('moveTo', ((0, 1),)),
|
||||
... ('lineTo', ((2, 3),)),
|
||||
... ('qCurveTo', ((2, 5), (3, 6), (5, 6))),
|
||||
... ('curveTo', ((6, 9), (7, 10), (9, 10))),
|
||||
... ('addComponent', ('a', (1.5, 0, 0, 1.5, 11, -10))),
|
||||
... ]
|
||||
True
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, roundFunc=otRound, transformRoundFunc=noRound):
|
||||
super().__init__(outPen)
|
||||
self.roundFunc = roundFunc
|
||||
self.transformRoundFunc = transformRoundFunc
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._outPen.moveTo((self.roundFunc(pt[0]), self.roundFunc(pt[1])))
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._outPen.lineTo((self.roundFunc(pt[0]), self.roundFunc(pt[1])))
|
||||
|
||||
def curveTo(self, *points):
|
||||
self._outPen.curveTo(
|
||||
*((self.roundFunc(x), self.roundFunc(y)) for x, y in points)
|
||||
)
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
self._outPen.qCurveTo(
|
||||
*((self.roundFunc(x), self.roundFunc(y)) for x, y in points)
|
||||
)
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
xx, xy, yx, yy, dx, dy = transformation
|
||||
self._outPen.addComponent(
|
||||
glyphName,
|
||||
Transform(
|
||||
self.transformRoundFunc(xx),
|
||||
self.transformRoundFunc(xy),
|
||||
self.transformRoundFunc(yx),
|
||||
self.transformRoundFunc(yy),
|
||||
self.roundFunc(dx),
|
||||
self.roundFunc(dy),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RoundingPointPen(FilterPointPen):
|
||||
"""
|
||||
Filter point pen that rounds point coordinates and component XY offsets to integer.
|
||||
For rounding the component scale values, a separate round function can be passed to
|
||||
the pen.
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPointPen
|
||||
>>> recpen = RecordingPointPen()
|
||||
>>> roundpen = RoundingPointPen(recpen)
|
||||
>>> roundpen.beginPath()
|
||||
>>> roundpen.addPoint((0.4, 0.6), 'line')
|
||||
>>> roundpen.addPoint((1.6, 2.5), 'line')
|
||||
>>> roundpen.addPoint((2.4, 4.6))
|
||||
>>> roundpen.addPoint((3.3, 5.7))
|
||||
>>> roundpen.addPoint((4.9, 6.1), 'qcurve')
|
||||
>>> roundpen.endPath()
|
||||
>>> roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5))
|
||||
>>> recpen.value == [
|
||||
... ('beginPath', (), {}),
|
||||
... ('addPoint', ((0, 1), 'line', False, None), {}),
|
||||
... ('addPoint', ((2, 3), 'line', False, None), {}),
|
||||
... ('addPoint', ((2, 5), None, False, None), {}),
|
||||
... ('addPoint', ((3, 6), None, False, None), {}),
|
||||
... ('addPoint', ((5, 6), 'qcurve', False, None), {}),
|
||||
... ('endPath', (), {}),
|
||||
... ('addComponent', ('a', (1.5, 0, 0, 1.5, 11, -10)), {}),
|
||||
... ]
|
||||
True
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, roundFunc=otRound, transformRoundFunc=noRound):
|
||||
super().__init__(outPen)
|
||||
self.roundFunc = roundFunc
|
||||
self.transformRoundFunc = transformRoundFunc
|
||||
|
||||
def addPoint(
|
||||
self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
|
||||
):
|
||||
self._outPen.addPoint(
|
||||
(self.roundFunc(pt[0]), self.roundFunc(pt[1])),
|
||||
segmentType=segmentType,
|
||||
smooth=smooth,
|
||||
name=name,
|
||||
identifier=identifier,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
|
||||
xx, xy, yx, yy, dx, dy = transformation
|
||||
self._outPen.addComponent(
|
||||
baseGlyphName=baseGlyphName,
|
||||
transformation=Transform(
|
||||
self.transformRoundFunc(xx),
|
||||
self.transformRoundFunc(xy),
|
||||
self.transformRoundFunc(yx),
|
||||
self.transformRoundFunc(yy),
|
||||
self.roundFunc(dx),
|
||||
self.roundFunc(dy),
|
||||
),
|
||||
identifier=identifier,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -1,307 +0,0 @@
|
||||
"""Pen calculating area, center of mass, variance and standard-deviation,
|
||||
covariance and correlation, and slant, of glyph shapes."""
|
||||
|
||||
from math import sqrt, degrees, atan
|
||||
from fontTools.pens.basePen import BasePen, OpenContourError
|
||||
from fontTools.pens.momentsPen import MomentsPen
|
||||
|
||||
__all__ = ["StatisticsPen", "StatisticsControlPen"]
|
||||
|
||||
|
||||
class StatisticsBase:
|
||||
def __init__(self):
|
||||
self._zero()
|
||||
|
||||
def _zero(self):
|
||||
self.area = 0
|
||||
self.meanX = 0
|
||||
self.meanY = 0
|
||||
self.varianceX = 0
|
||||
self.varianceY = 0
|
||||
self.stddevX = 0
|
||||
self.stddevY = 0
|
||||
self.covariance = 0
|
||||
self.correlation = 0
|
||||
self.slant = 0
|
||||
|
||||
def _update(self):
|
||||
# XXX The variance formulas should never produce a negative value,
|
||||
# but due to reasons I don't understand, both of our pens do.
|
||||
# So we take the absolute value here.
|
||||
self.varianceX = abs(self.varianceX)
|
||||
self.varianceY = abs(self.varianceY)
|
||||
|
||||
self.stddevX = stddevX = sqrt(self.varianceX)
|
||||
self.stddevY = stddevY = sqrt(self.varianceY)
|
||||
|
||||
# Correlation(X,Y) = Covariance(X,Y) / ( stddev(X) * stddev(Y) )
|
||||
# https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient
|
||||
if stddevX * stddevY == 0:
|
||||
correlation = float("NaN")
|
||||
else:
|
||||
# XXX The above formula should never produce a value outside
|
||||
# the range [-1, 1], but due to reasons I don't understand,
|
||||
# (probably the same issue as above), it does. So we clamp.
|
||||
correlation = self.covariance / (stddevX * stddevY)
|
||||
correlation = max(-1, min(1, correlation))
|
||||
self.correlation = correlation if abs(correlation) > 1e-3 else 0
|
||||
|
||||
slant = (
|
||||
self.covariance / self.varianceY if self.varianceY != 0 else float("NaN")
|
||||
)
|
||||
self.slant = slant if abs(slant) > 1e-3 else 0
|
||||
|
||||
|
||||
class StatisticsPen(StatisticsBase, MomentsPen):
|
||||
"""Pen calculating area, center of mass, variance and
|
||||
standard-deviation, covariance and correlation, and slant,
|
||||
of glyph shapes.
|
||||
|
||||
Note that if the glyph shape is self-intersecting, the values
|
||||
are not correct (but well-defined). Moreover, area will be
|
||||
negative if contour directions are clockwise."""
|
||||
|
||||
def __init__(self, glyphset=None):
|
||||
MomentsPen.__init__(self, glyphset=glyphset)
|
||||
StatisticsBase.__init__(self)
|
||||
|
||||
def _closePath(self):
|
||||
MomentsPen._closePath(self)
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
area = self.area
|
||||
if not area:
|
||||
self._zero()
|
||||
return
|
||||
|
||||
# Center of mass
|
||||
# https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume
|
||||
self.meanX = meanX = self.momentX / area
|
||||
self.meanY = meanY = self.momentY / area
|
||||
|
||||
# Var(X) = E[X^2] - E[X]^2
|
||||
self.varianceX = self.momentXX / area - meanX * meanX
|
||||
self.varianceY = self.momentYY / area - meanY * meanY
|
||||
|
||||
# Covariance(X,Y) = (E[X.Y] - E[X]E[Y])
|
||||
self.covariance = self.momentXY / area - meanX * meanY
|
||||
|
||||
StatisticsBase._update(self)
|
||||
|
||||
|
||||
class StatisticsControlPen(StatisticsBase, BasePen):
|
||||
"""Pen calculating area, center of mass, variance and
|
||||
standard-deviation, covariance and correlation, and slant,
|
||||
of glyph shapes, using the control polygon only.
|
||||
|
||||
Note that if the glyph shape is self-intersecting, the values
|
||||
are not correct (but well-defined). Moreover, area will be
|
||||
negative if contour directions are clockwise."""
|
||||
|
||||
def __init__(self, glyphset=None):
|
||||
BasePen.__init__(self, glyphset)
|
||||
StatisticsBase.__init__(self)
|
||||
self._nodes = []
|
||||
|
||||
def _moveTo(self, pt):
|
||||
self._nodes.append(complex(*pt))
|
||||
|
||||
def _lineTo(self, pt):
|
||||
self._nodes.append(complex(*pt))
|
||||
|
||||
def _qCurveToOne(self, pt1, pt2):
|
||||
for pt in (pt1, pt2):
|
||||
self._nodes.append(complex(*pt))
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
for pt in (pt1, pt2, pt3):
|
||||
self._nodes.append(complex(*pt))
|
||||
|
||||
def _closePath(self):
|
||||
self._update()
|
||||
|
||||
def _endPath(self):
|
||||
p0 = self._getCurrentPoint()
|
||||
if p0 != self.__startPoint:
|
||||
raise OpenContourError("Glyph statistics not defined on open contours.")
|
||||
|
||||
def _update(self):
|
||||
nodes = self._nodes
|
||||
n = len(nodes)
|
||||
|
||||
# Triangle formula
|
||||
self.area = (
|
||||
sum(
|
||||
(p0.real * p1.imag - p1.real * p0.imag)
|
||||
for p0, p1 in zip(nodes, nodes[1:] + nodes[:1])
|
||||
)
|
||||
/ 2
|
||||
)
|
||||
|
||||
# Center of mass
|
||||
# https://en.wikipedia.org/wiki/Center_of_mass#A_system_of_particles
|
||||
sumNodes = sum(nodes)
|
||||
self.meanX = meanX = sumNodes.real / n
|
||||
self.meanY = meanY = sumNodes.imag / n
|
||||
|
||||
if n > 1:
|
||||
# Var(X) = (sum[X^2] - sum[X]^2 / n) / (n - 1)
|
||||
# https://www.statisticshowto.com/probability-and-statistics/descriptive-statistics/sample-variance/
|
||||
self.varianceX = varianceX = (
|
||||
sum(p.real * p.real for p in nodes)
|
||||
- (sumNodes.real * sumNodes.real) / n
|
||||
) / (n - 1)
|
||||
self.varianceY = varianceY = (
|
||||
sum(p.imag * p.imag for p in nodes)
|
||||
- (sumNodes.imag * sumNodes.imag) / n
|
||||
) / (n - 1)
|
||||
|
||||
# Covariance(X,Y) = (sum[X.Y] - sum[X].sum[Y] / n) / (n - 1)
|
||||
self.covariance = covariance = (
|
||||
sum(p.real * p.imag for p in nodes)
|
||||
- (sumNodes.real * sumNodes.imag) / n
|
||||
) / (n - 1)
|
||||
else:
|
||||
self.varianceX = varianceX = 0
|
||||
self.varianceY = varianceY = 0
|
||||
self.covariance = covariance = 0
|
||||
|
||||
StatisticsBase._update(self)
|
||||
|
||||
|
||||
def _test(glyphset, upem, glyphs, quiet=False, *, control=False):
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.misc.transform import Scale
|
||||
|
||||
wght_sum = 0
|
||||
wght_sum_perceptual = 0
|
||||
wdth_sum = 0
|
||||
slnt_sum = 0
|
||||
slnt_sum_perceptual = 0
|
||||
for glyph_name in glyphs:
|
||||
glyph = glyphset[glyph_name]
|
||||
if control:
|
||||
pen = StatisticsControlPen(glyphset=glyphset)
|
||||
else:
|
||||
pen = StatisticsPen(glyphset=glyphset)
|
||||
transformer = TransformPen(pen, Scale(1.0 / upem))
|
||||
glyph.draw(transformer)
|
||||
|
||||
area = abs(pen.area)
|
||||
width = glyph.width
|
||||
wght_sum += area
|
||||
wght_sum_perceptual += pen.area * width
|
||||
wdth_sum += width
|
||||
slnt_sum += pen.slant
|
||||
slnt_sum_perceptual += pen.slant * width
|
||||
|
||||
if quiet:
|
||||
continue
|
||||
|
||||
print()
|
||||
print("glyph:", glyph_name)
|
||||
|
||||
for item in [
|
||||
"area",
|
||||
"momentX",
|
||||
"momentY",
|
||||
"momentXX",
|
||||
"momentYY",
|
||||
"momentXY",
|
||||
"meanX",
|
||||
"meanY",
|
||||
"varianceX",
|
||||
"varianceY",
|
||||
"stddevX",
|
||||
"stddevY",
|
||||
"covariance",
|
||||
"correlation",
|
||||
"slant",
|
||||
]:
|
||||
print("%s: %g" % (item, getattr(pen, item)))
|
||||
|
||||
if not quiet:
|
||||
print()
|
||||
print("font:")
|
||||
|
||||
print("weight: %g" % (wght_sum * upem / wdth_sum))
|
||||
print("weight (perceptual): %g" % (wght_sum_perceptual / wdth_sum))
|
||||
print("width: %g" % (wdth_sum / upem / len(glyphs)))
|
||||
slant = slnt_sum / len(glyphs)
|
||||
print("slant: %g" % slant)
|
||||
print("slant angle: %g" % -degrees(atan(slant)))
|
||||
slant_perceptual = slnt_sum_perceptual / wdth_sum
|
||||
print("slant (perceptual): %g" % slant_perceptual)
|
||||
print("slant (perceptual) angle: %g" % -degrees(atan(slant_perceptual)))
|
||||
|
||||
|
||||
def main(args):
|
||||
"""Report font glyph shape geometricsl statistics"""
|
||||
|
||||
if args is None:
|
||||
import sys
|
||||
|
||||
args = sys.argv[1:]
|
||||
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
"fonttools pens.statisticsPen",
|
||||
description="Report font glyph shape geometricsl statistics",
|
||||
)
|
||||
parser.add_argument("font", metavar="font.ttf", help="Font file.")
|
||||
parser.add_argument("glyphs", metavar="glyph-name", help="Glyph names.", nargs="*")
|
||||
parser.add_argument(
|
||||
"-y",
|
||||
metavar="<number>",
|
||||
help="Face index into a collection to open. Zero based.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--control",
|
||||
action="store_true",
|
||||
help="Use the control-box pen instead of the Green therem.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q", "--quiet", action="store_true", help="Only report font-wide statistics."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--variations",
|
||||
metavar="AXIS=LOC",
|
||||
default="",
|
||||
help="List of space separated locations. A location consist in "
|
||||
"the name of a variation axis, followed by '=' and a number. E.g.: "
|
||||
"wght=700 wdth=80. The default is the location of the base master.",
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
glyphs = options.glyphs
|
||||
fontNumber = int(options.y) if options.y is not None else 0
|
||||
|
||||
location = {}
|
||||
for tag_v in options.variations.split():
|
||||
fields = tag_v.split("=")
|
||||
tag = fields[0].strip()
|
||||
v = int(fields[1])
|
||||
location[tag] = v
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
font = TTFont(options.font, fontNumber=fontNumber)
|
||||
if not glyphs:
|
||||
glyphs = font.getGlyphOrder()
|
||||
_test(
|
||||
font.getGlyphSet(location=location),
|
||||
font["head"].unitsPerEm,
|
||||
glyphs,
|
||||
quiet=options.quiet,
|
||||
control=options.control,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
main(sys.argv[1:])
|
||||
@@ -1,307 +0,0 @@
|
||||
from typing import Callable
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
def pointToString(pt, ntos=str):
|
||||
return " ".join(ntos(i) for i in pt)
|
||||
|
||||
|
||||
class SVGPathPen(BasePen):
|
||||
"""Pen to draw SVG path d commands.
|
||||
|
||||
Example::
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((1, 1))
|
||||
>>> pen.curveTo((2, 2), (3, 3), (4, 4))
|
||||
>>> pen.closePath()
|
||||
>>> pen.getCommands()
|
||||
'M0 0 1 1C2 2 3 3 4 4Z'
|
||||
|
||||
Args:
|
||||
glyphSet: a dictionary of drawable glyph objects keyed by name
|
||||
used to resolve component references in composite glyphs.
|
||||
ntos: a callable that takes a number and returns a string, to
|
||||
customize how numbers are formatted (default: str).
|
||||
|
||||
Note:
|
||||
Fonts have a coordinate system where Y grows up, whereas in SVG,
|
||||
Y grows down. As such, rendering path data from this pen in
|
||||
SVG typically results in upside-down glyphs. You can fix this
|
||||
by wrapping the data from this pen in an SVG group element with
|
||||
transform, or wrap this pen in a transform pen. For example:
|
||||
|
||||
spen = svgPathPen.SVGPathPen(glyphset)
|
||||
pen= TransformPen(spen , (1, 0, 0, -1, 0, 0))
|
||||
glyphset[glyphname].draw(pen)
|
||||
print(tpen.getCommands())
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet, ntos: Callable[[float], str] = str):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
self._commands = []
|
||||
self._lastCommand = None
|
||||
self._lastX = None
|
||||
self._lastY = None
|
||||
self._ntos = ntos
|
||||
|
||||
def _handleAnchor(self):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.moveTo((10, 10))
|
||||
>>> pen._commands
|
||||
['M10 10']
|
||||
"""
|
||||
if self._lastCommand == "M":
|
||||
self._commands.pop(-1)
|
||||
|
||||
def _moveTo(self, pt):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen._commands
|
||||
['M0 0']
|
||||
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((10, 0))
|
||||
>>> pen._commands
|
||||
['M10 0']
|
||||
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 10))
|
||||
>>> pen._commands
|
||||
['M0 10']
|
||||
"""
|
||||
self._handleAnchor()
|
||||
t = "M%s" % (pointToString(pt, self._ntos))
|
||||
self._commands.append(t)
|
||||
self._lastCommand = "M"
|
||||
self._lastX, self._lastY = pt
|
||||
|
||||
def _lineTo(self, pt):
|
||||
"""
|
||||
# duplicate point
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((10, 10))
|
||||
>>> pen.lineTo((10, 10))
|
||||
>>> pen._commands
|
||||
['M10 10']
|
||||
|
||||
# vertical line
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((10, 10))
|
||||
>>> pen.lineTo((10, 0))
|
||||
>>> pen._commands
|
||||
['M10 10', 'V0']
|
||||
|
||||
# horizontal line
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((10, 10))
|
||||
>>> pen.lineTo((0, 10))
|
||||
>>> pen._commands
|
||||
['M10 10', 'H0']
|
||||
|
||||
# basic
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.lineTo((70, 80))
|
||||
>>> pen._commands
|
||||
['L70 80']
|
||||
|
||||
# basic following a moveto
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.moveTo((0, 0))
|
||||
>>> pen.lineTo((10, 10))
|
||||
>>> pen._commands
|
||||
['M0 0', ' 10 10']
|
||||
"""
|
||||
x, y = pt
|
||||
# duplicate point
|
||||
if x == self._lastX and y == self._lastY:
|
||||
return
|
||||
# vertical line
|
||||
elif x == self._lastX:
|
||||
cmd = "V"
|
||||
pts = self._ntos(y)
|
||||
# horizontal line
|
||||
elif y == self._lastY:
|
||||
cmd = "H"
|
||||
pts = self._ntos(x)
|
||||
# previous was a moveto
|
||||
elif self._lastCommand == "M":
|
||||
cmd = None
|
||||
pts = " " + pointToString(pt, self._ntos)
|
||||
# basic
|
||||
else:
|
||||
cmd = "L"
|
||||
pts = pointToString(pt, self._ntos)
|
||||
# write the string
|
||||
t = ""
|
||||
if cmd:
|
||||
t += cmd
|
||||
self._lastCommand = cmd
|
||||
t += pts
|
||||
self._commands.append(t)
|
||||
# store for future reference
|
||||
self._lastX, self._lastY = pt
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.curveTo((10, 20), (30, 40), (50, 60))
|
||||
>>> pen._commands
|
||||
['C10 20 30 40 50 60']
|
||||
"""
|
||||
t = "C"
|
||||
t += pointToString(pt1, self._ntos) + " "
|
||||
t += pointToString(pt2, self._ntos) + " "
|
||||
t += pointToString(pt3, self._ntos)
|
||||
self._commands.append(t)
|
||||
self._lastCommand = "C"
|
||||
self._lastX, self._lastY = pt3
|
||||
|
||||
def _qCurveToOne(self, pt1, pt2):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.qCurveTo((10, 20), (30, 40))
|
||||
>>> pen._commands
|
||||
['Q10 20 30 40']
|
||||
>>> from fontTools.misc.roundTools import otRound
|
||||
>>> pen = SVGPathPen(None, ntos=lambda v: str(otRound(v)))
|
||||
>>> pen.qCurveTo((3, 3), (7, 5), (11, 4))
|
||||
>>> pen._commands
|
||||
['Q3 3 5 4', 'Q7 5 11 4']
|
||||
"""
|
||||
assert pt2 is not None
|
||||
t = "Q"
|
||||
t += pointToString(pt1, self._ntos) + " "
|
||||
t += pointToString(pt2, self._ntos)
|
||||
self._commands.append(t)
|
||||
self._lastCommand = "Q"
|
||||
self._lastX, self._lastY = pt2
|
||||
|
||||
def _closePath(self):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.closePath()
|
||||
>>> pen._commands
|
||||
['Z']
|
||||
"""
|
||||
self._commands.append("Z")
|
||||
self._lastCommand = "Z"
|
||||
self._lastX = self._lastY = None
|
||||
|
||||
def _endPath(self):
|
||||
"""
|
||||
>>> pen = SVGPathPen(None)
|
||||
>>> pen.endPath()
|
||||
>>> pen._commands
|
||||
[]
|
||||
"""
|
||||
self._lastCommand = None
|
||||
self._lastX = self._lastY = None
|
||||
|
||||
def getCommands(self):
|
||||
return "".join(self._commands)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Generate per-character SVG from font and text"""
|
||||
|
||||
if args is None:
|
||||
import sys
|
||||
|
||||
args = sys.argv[1:]
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
"fonttools pens.svgPathPen", description="Generate SVG from text"
|
||||
)
|
||||
parser.add_argument("font", metavar="font.ttf", help="Font file.")
|
||||
parser.add_argument("text", metavar="text", nargs="?", help="Text string.")
|
||||
parser.add_argument(
|
||||
"-y",
|
||||
metavar="<number>",
|
||||
help="Face index into a collection to open. Zero based.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--glyphs",
|
||||
metavar="whitespace-separated list of glyph names",
|
||||
type=str,
|
||||
help="Glyphs to show. Exclusive with text option",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--variations",
|
||||
metavar="AXIS=LOC",
|
||||
default="",
|
||||
help="List of space separated locations. A location consist in "
|
||||
"the name of a variation axis, followed by '=' and a number. E.g.: "
|
||||
"wght=700 wdth=80. The default is the location of the base master.",
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
fontNumber = int(options.y) if options.y is not None else 0
|
||||
|
||||
font = TTFont(options.font, fontNumber=fontNumber)
|
||||
text = options.text
|
||||
glyphs = options.glyphs
|
||||
|
||||
location = {}
|
||||
for tag_v in options.variations.split():
|
||||
fields = tag_v.split("=")
|
||||
tag = fields[0].strip()
|
||||
v = float(fields[1])
|
||||
location[tag] = v
|
||||
|
||||
hhea = font["hhea"]
|
||||
ascent, descent = hhea.ascent, hhea.descent
|
||||
|
||||
glyphset = font.getGlyphSet(location=location)
|
||||
cmap = font["cmap"].getBestCmap()
|
||||
|
||||
if glyphs is not None and text is not None:
|
||||
raise ValueError("Options --glyphs and --text are exclusive")
|
||||
|
||||
if glyphs is None:
|
||||
glyphs = " ".join(cmap[ord(u)] for u in text)
|
||||
|
||||
glyphs = glyphs.split()
|
||||
|
||||
s = ""
|
||||
width = 0
|
||||
for g in glyphs:
|
||||
glyph = glyphset[g]
|
||||
|
||||
pen = SVGPathPen(glyphset)
|
||||
glyph.draw(pen)
|
||||
commands = pen.getCommands()
|
||||
|
||||
s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % (
|
||||
width,
|
||||
ascent,
|
||||
commands,
|
||||
)
|
||||
|
||||
width += glyph.width
|
||||
|
||||
print('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
print(
|
||||
'<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">'
|
||||
% (width, ascent - descent)
|
||||
)
|
||||
print(s, end="")
|
||||
print("</svg>")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
import doctest
|
||||
|
||||
sys.exit(doctest.testmod().failed)
|
||||
|
||||
sys.exit(main())
|
||||
@@ -1,68 +0,0 @@
|
||||
# Copyright (c) 2009 Type Supply LLC
|
||||
# Author: Tal Leming
|
||||
|
||||
from fontTools.misc.roundTools import otRound, roundFunc
|
||||
from fontTools.misc.psCharStrings import T2CharString
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from fontTools.cffLib.specializer import specializeCommands, commandsToProgram
|
||||
|
||||
|
||||
class T2CharStringPen(BasePen):
|
||||
"""Pen to draw Type 2 CharStrings.
|
||||
|
||||
The 'roundTolerance' argument controls the rounding of point coordinates.
|
||||
It is defined as the maximum absolute difference between the original
|
||||
float and the rounded integer value.
|
||||
The default tolerance of 0.5 means that all floats are rounded to integer;
|
||||
a value of 0 disables rounding; values in between will only round floats
|
||||
which are close to their integral part within the tolerated range.
|
||||
"""
|
||||
|
||||
def __init__(self, width, glyphSet, roundTolerance=0.5, CFF2=False):
|
||||
super(T2CharStringPen, self).__init__(glyphSet)
|
||||
self.round = roundFunc(roundTolerance)
|
||||
self._CFF2 = CFF2
|
||||
self._width = width
|
||||
self._commands = []
|
||||
self._p0 = (0, 0)
|
||||
|
||||
def _p(self, pt):
|
||||
p0 = self._p0
|
||||
pt = self._p0 = (self.round(pt[0]), self.round(pt[1]))
|
||||
return [pt[0] - p0[0], pt[1] - p0[1]]
|
||||
|
||||
def _moveTo(self, pt):
|
||||
self._commands.append(("rmoveto", self._p(pt)))
|
||||
|
||||
def _lineTo(self, pt):
|
||||
self._commands.append(("rlineto", self._p(pt)))
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
_p = self._p
|
||||
self._commands.append(("rrcurveto", _p(pt1) + _p(pt2) + _p(pt3)))
|
||||
|
||||
def _closePath(self):
|
||||
pass
|
||||
|
||||
def _endPath(self):
|
||||
pass
|
||||
|
||||
def getCharString(self, private=None, globalSubrs=None, optimize=True):
|
||||
commands = self._commands
|
||||
if optimize:
|
||||
maxstack = 48 if not self._CFF2 else 513
|
||||
commands = specializeCommands(
|
||||
commands, generalizeFirst=False, maxstack=maxstack
|
||||
)
|
||||
program = commandsToProgram(commands)
|
||||
if self._width is not None:
|
||||
assert (
|
||||
not self._CFF2
|
||||
), "CFF2 does not allow encoding glyph width in CharString."
|
||||
program.insert(0, otRound(self._width))
|
||||
if not self._CFF2:
|
||||
program.append("endchar")
|
||||
charString = T2CharString(
|
||||
program=program, private=private, globalSubrs=globalSubrs
|
||||
)
|
||||
return charString
|
||||
@@ -1,55 +0,0 @@
|
||||
"""Pen multiplexing drawing to one or more pens."""
|
||||
|
||||
from fontTools.pens.basePen import AbstractPen
|
||||
|
||||
|
||||
__all__ = ["TeePen"]
|
||||
|
||||
|
||||
class TeePen(AbstractPen):
|
||||
"""Pen multiplexing drawing to one or more pens.
|
||||
|
||||
Use either as TeePen(pen1, pen2, ...) or TeePen(iterableOfPens)."""
|
||||
|
||||
def __init__(self, *pens):
|
||||
if len(pens) == 1:
|
||||
pens = pens[0]
|
||||
self.pens = pens
|
||||
|
||||
def moveTo(self, p0):
|
||||
for pen in self.pens:
|
||||
pen.moveTo(p0)
|
||||
|
||||
def lineTo(self, p1):
|
||||
for pen in self.pens:
|
||||
pen.lineTo(p1)
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
for pen in self.pens:
|
||||
pen.qCurveTo(*points)
|
||||
|
||||
def curveTo(self, *points):
|
||||
for pen in self.pens:
|
||||
pen.curveTo(*points)
|
||||
|
||||
def closePath(self):
|
||||
for pen in self.pens:
|
||||
pen.closePath()
|
||||
|
||||
def endPath(self):
|
||||
for pen in self.pens:
|
||||
pen.endPath()
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
for pen in self.pens:
|
||||
pen.addComponent(glyphName, transformation)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from fontTools.pens.basePen import _TestPen
|
||||
|
||||
pen = TeePen(_TestPen(), _TestPen())
|
||||
pen.moveTo((0, 0))
|
||||
pen.lineTo((0, 100))
|
||||
pen.curveTo((50, 75), (60, 50), (50, 25))
|
||||
pen.closePath()
|
||||
@@ -1,110 +0,0 @@
|
||||
from fontTools.pens.filterPen import FilterPen, FilterPointPen
|
||||
|
||||
|
||||
__all__ = ["TransformPen", "TransformPointPen"]
|
||||
|
||||
|
||||
class TransformPen(FilterPen):
|
||||
"""Pen that transforms all coordinates using a Affine transformation,
|
||||
and passes them to another pen.
|
||||
"""
|
||||
|
||||
def __init__(self, outPen, transformation):
|
||||
"""The 'outPen' argument is another pen object. It will receive the
|
||||
transformed coordinates. The 'transformation' argument can either
|
||||
be a six-tuple, or a fontTools.misc.transform.Transform object.
|
||||
"""
|
||||
super(TransformPen, self).__init__(outPen)
|
||||
if not hasattr(transformation, "transformPoint"):
|
||||
from fontTools.misc.transform import Transform
|
||||
|
||||
transformation = Transform(*transformation)
|
||||
self._transformation = transformation
|
||||
self._transformPoint = transformation.transformPoint
|
||||
self._stack = []
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._outPen.moveTo(self._transformPoint(pt))
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._outPen.lineTo(self._transformPoint(pt))
|
||||
|
||||
def curveTo(self, *points):
|
||||
self._outPen.curveTo(*self._transformPoints(points))
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
if points[-1] is None:
|
||||
points = self._transformPoints(points[:-1]) + [None]
|
||||
else:
|
||||
points = self._transformPoints(points)
|
||||
self._outPen.qCurveTo(*points)
|
||||
|
||||
def _transformPoints(self, points):
|
||||
transformPoint = self._transformPoint
|
||||
return [transformPoint(pt) for pt in points]
|
||||
|
||||
def closePath(self):
|
||||
self._outPen.closePath()
|
||||
|
||||
def endPath(self):
|
||||
self._outPen.endPath()
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
transformation = self._transformation.transform(transformation)
|
||||
self._outPen.addComponent(glyphName, transformation)
|
||||
|
||||
|
||||
class TransformPointPen(FilterPointPen):
|
||||
"""PointPen that transforms all coordinates using a Affine transformation,
|
||||
and passes them to another PointPen.
|
||||
|
||||
>>> from fontTools.pens.recordingPen import RecordingPointPen
|
||||
>>> rec = RecordingPointPen()
|
||||
>>> pen = TransformPointPen(rec, (2, 0, 0, 2, -10, 5))
|
||||
>>> v = iter(rec.value)
|
||||
>>> pen.beginPath(identifier="contour-0")
|
||||
>>> next(v)
|
||||
('beginPath', (), {'identifier': 'contour-0'})
|
||||
>>> pen.addPoint((100, 100), "line")
|
||||
>>> next(v)
|
||||
('addPoint', ((190, 205), 'line', False, None), {})
|
||||
>>> pen.endPath()
|
||||
>>> next(v)
|
||||
('endPath', (), {})
|
||||
>>> pen.addComponent("a", (1, 0, 0, 1, -10, 5), identifier="component-0")
|
||||
>>> next(v)
|
||||
('addComponent', ('a', <Transform [2 0 0 2 -30 15]>), {'identifier': 'component-0'})
|
||||
"""
|
||||
|
||||
def __init__(self, outPointPen, transformation):
|
||||
"""The 'outPointPen' argument is another point pen object.
|
||||
It will receive the transformed coordinates.
|
||||
The 'transformation' argument can either be a six-tuple, or a
|
||||
fontTools.misc.transform.Transform object.
|
||||
"""
|
||||
super().__init__(outPointPen)
|
||||
if not hasattr(transformation, "transformPoint"):
|
||||
from fontTools.misc.transform import Transform
|
||||
|
||||
transformation = Transform(*transformation)
|
||||
self._transformation = transformation
|
||||
self._transformPoint = transformation.transformPoint
|
||||
|
||||
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
|
||||
self._outPen.addPoint(
|
||||
self._transformPoint(pt), segmentType, smooth, name, **kwargs
|
||||
)
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, **kwargs):
|
||||
transformation = self._transformation.transform(transformation)
|
||||
self._outPen.addComponent(baseGlyphName, transformation, **kwargs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from fontTools.pens.basePen import _TestPen
|
||||
|
||||
pen = TransformPen(_TestPen(None), (2, 0, 0.5, 2, -10, 0))
|
||||
pen.moveTo((0, 0))
|
||||
pen.lineTo((0, 100))
|
||||
pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
|
||||
pen.closePath()
|
||||
@@ -1,335 +0,0 @@
|
||||
from array import array
|
||||
from typing import Any, Callable, Dict, Optional, Tuple
|
||||
from fontTools.misc.fixedTools import MAX_F2DOT14, floatToFixedToFloat
|
||||
from fontTools.misc.loggingTools import LogMixin
|
||||
from fontTools.pens.pointPen import AbstractPointPen
|
||||
from fontTools.misc.roundTools import otRound
|
||||
from fontTools.pens.basePen import LoggingPen, PenError
|
||||
from fontTools.pens.transformPen import TransformPen, TransformPointPen
|
||||
from fontTools.ttLib.tables import ttProgram
|
||||
from fontTools.ttLib.tables._g_l_y_f import flagOnCurve, flagCubic
|
||||
from fontTools.ttLib.tables._g_l_y_f import Glyph
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
||||
from fontTools.ttLib.tables._g_l_y_f import dropImpliedOnCurvePoints
|
||||
import math
|
||||
|
||||
|
||||
__all__ = ["TTGlyphPen", "TTGlyphPointPen"]
|
||||
|
||||
|
||||
class _TTGlyphBasePen:
|
||||
def __init__(
|
||||
self,
|
||||
glyphSet: Optional[Dict[str, Any]],
|
||||
handleOverflowingTransforms: bool = True,
|
||||
) -> None:
|
||||
"""
|
||||
Construct a new pen.
|
||||
|
||||
Args:
|
||||
glyphSet (Dict[str, Any]): A glyphset object, used to resolve components.
|
||||
handleOverflowingTransforms (bool): See below.
|
||||
|
||||
If ``handleOverflowingTransforms`` is True, the components' transform values
|
||||
are checked that they don't overflow the limits of a F2Dot14 number:
|
||||
-2.0 <= v < +2.0. If any transform value exceeds these, the composite
|
||||
glyph is decomposed.
|
||||
|
||||
An exception to this rule is done for values that are very close to +2.0
|
||||
(both for consistency with the -2.0 case, and for the relative frequency
|
||||
these occur in real fonts). When almost +2.0 values occur (and all other
|
||||
values are within the range -2.0 <= x <= +2.0), they are clamped to the
|
||||
maximum positive value that can still be encoded as an F2Dot14: i.e.
|
||||
1.99993896484375.
|
||||
|
||||
If False, no check is done and all components are translated unmodified
|
||||
into the glyf table, followed by an inevitable ``struct.error`` once an
|
||||
attempt is made to compile them.
|
||||
|
||||
If both contours and components are present in a glyph, the components
|
||||
are decomposed.
|
||||
"""
|
||||
self.glyphSet = glyphSet
|
||||
self.handleOverflowingTransforms = handleOverflowingTransforms
|
||||
self.init()
|
||||
|
||||
def _decompose(
|
||||
self,
|
||||
glyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float],
|
||||
):
|
||||
tpen = self.transformPen(self, transformation)
|
||||
getattr(self.glyphSet[glyphName], self.drawMethod)(tpen)
|
||||
|
||||
def _isClosed(self):
|
||||
"""
|
||||
Check if the current path is closed.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def init(self) -> None:
|
||||
self.points = []
|
||||
self.endPts = []
|
||||
self.types = []
|
||||
self.components = []
|
||||
|
||||
def addComponent(
|
||||
self,
|
||||
baseGlyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float],
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Add a sub glyph.
|
||||
"""
|
||||
self.components.append((baseGlyphName, transformation))
|
||||
|
||||
def _buildComponents(self, componentFlags):
|
||||
if self.handleOverflowingTransforms:
|
||||
# we can't encode transform values > 2 or < -2 in F2Dot14,
|
||||
# so we must decompose the glyph if any transform exceeds these
|
||||
overflowing = any(
|
||||
s > 2 or s < -2
|
||||
for (glyphName, transformation) in self.components
|
||||
for s in transformation[:4]
|
||||
)
|
||||
components = []
|
||||
for glyphName, transformation in self.components:
|
||||
if glyphName not in self.glyphSet:
|
||||
self.log.warning(f"skipped non-existing component '{glyphName}'")
|
||||
continue
|
||||
if self.points or (self.handleOverflowingTransforms and overflowing):
|
||||
# can't have both coordinates and components, so decompose
|
||||
self._decompose(glyphName, transformation)
|
||||
continue
|
||||
|
||||
component = GlyphComponent()
|
||||
component.glyphName = glyphName
|
||||
component.x, component.y = (otRound(v) for v in transformation[4:])
|
||||
# quantize floats to F2Dot14 so we get same values as when decompiled
|
||||
# from a binary glyf table
|
||||
transformation = tuple(
|
||||
floatToFixedToFloat(v, 14) for v in transformation[:4]
|
||||
)
|
||||
if transformation != (1, 0, 0, 1):
|
||||
if self.handleOverflowingTransforms and any(
|
||||
MAX_F2DOT14 < s <= 2 for s in transformation
|
||||
):
|
||||
# clamp values ~= +2.0 so we can keep the component
|
||||
transformation = tuple(
|
||||
MAX_F2DOT14 if MAX_F2DOT14 < s <= 2 else s
|
||||
for s in transformation
|
||||
)
|
||||
component.transform = (transformation[:2], transformation[2:])
|
||||
component.flags = componentFlags
|
||||
components.append(component)
|
||||
return components
|
||||
|
||||
def glyph(
|
||||
self,
|
||||
componentFlags: int = 0x04,
|
||||
dropImpliedOnCurves: bool = False,
|
||||
*,
|
||||
round: Callable[[float], int] = otRound,
|
||||
) -> Glyph:
|
||||
"""
|
||||
Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
|
||||
|
||||
Args:
|
||||
componentFlags: Flags to use for component glyphs. (default: 0x04)
|
||||
|
||||
dropImpliedOnCurves: Whether to remove implied-oncurve points. (default: False)
|
||||
"""
|
||||
if not self._isClosed():
|
||||
raise PenError("Didn't close last contour.")
|
||||
components = self._buildComponents(componentFlags)
|
||||
|
||||
glyph = Glyph()
|
||||
glyph.coordinates = GlyphCoordinates(self.points)
|
||||
glyph.endPtsOfContours = self.endPts
|
||||
glyph.flags = array("B", self.types)
|
||||
self.init()
|
||||
|
||||
if components:
|
||||
# If both components and contours were present, they have by now
|
||||
# been decomposed by _buildComponents.
|
||||
glyph.components = components
|
||||
glyph.numberOfContours = -1
|
||||
else:
|
||||
glyph.numberOfContours = len(glyph.endPtsOfContours)
|
||||
glyph.program = ttProgram.Program()
|
||||
glyph.program.fromBytecode(b"")
|
||||
if dropImpliedOnCurves:
|
||||
dropImpliedOnCurvePoints(glyph)
|
||||
glyph.coordinates.toInt(round=round)
|
||||
|
||||
return glyph
|
||||
|
||||
|
||||
class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
|
||||
"""
|
||||
Pen used for drawing to a TrueType glyph.
|
||||
|
||||
This pen can be used to construct or modify glyphs in a TrueType format
|
||||
font. After using the pen to draw, use the ``.glyph()`` method to retrieve
|
||||
a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
|
||||
"""
|
||||
|
||||
drawMethod = "draw"
|
||||
transformPen = TransformPen
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
glyphSet: Optional[Dict[str, Any]] = None,
|
||||
handleOverflowingTransforms: bool = True,
|
||||
outputImpliedClosingLine: bool = False,
|
||||
) -> None:
|
||||
super().__init__(glyphSet, handleOverflowingTransforms)
|
||||
self.outputImpliedClosingLine = outputImpliedClosingLine
|
||||
|
||||
def _addPoint(self, pt: Tuple[float, float], tp: int) -> None:
|
||||
self.points.append(pt)
|
||||
self.types.append(tp)
|
||||
|
||||
def _popPoint(self) -> None:
|
||||
self.points.pop()
|
||||
self.types.pop()
|
||||
|
||||
def _isClosed(self) -> bool:
|
||||
return (not self.points) or (
|
||||
self.endPts and self.endPts[-1] == len(self.points) - 1
|
||||
)
|
||||
|
||||
def lineTo(self, pt: Tuple[float, float]) -> None:
|
||||
self._addPoint(pt, flagOnCurve)
|
||||
|
||||
def moveTo(self, pt: Tuple[float, float]) -> None:
|
||||
if not self._isClosed():
|
||||
raise PenError('"move"-type point must begin a new contour.')
|
||||
self._addPoint(pt, flagOnCurve)
|
||||
|
||||
def curveTo(self, *points) -> None:
|
||||
assert len(points) % 2 == 1
|
||||
for pt in points[:-1]:
|
||||
self._addPoint(pt, flagCubic)
|
||||
|
||||
# last point is None if there are no on-curve points
|
||||
if points[-1] is not None:
|
||||
self._addPoint(points[-1], 1)
|
||||
|
||||
def qCurveTo(self, *points) -> None:
|
||||
assert len(points) >= 1
|
||||
for pt in points[:-1]:
|
||||
self._addPoint(pt, 0)
|
||||
|
||||
# last point is None if there are no on-curve points
|
||||
if points[-1] is not None:
|
||||
self._addPoint(points[-1], 1)
|
||||
|
||||
def closePath(self) -> None:
|
||||
endPt = len(self.points) - 1
|
||||
|
||||
# ignore anchors (one-point paths)
|
||||
if endPt == 0 or (self.endPts and endPt == self.endPts[-1] + 1):
|
||||
self._popPoint()
|
||||
return
|
||||
|
||||
if not self.outputImpliedClosingLine:
|
||||
# if first and last point on this path are the same, remove last
|
||||
startPt = 0
|
||||
if self.endPts:
|
||||
startPt = self.endPts[-1] + 1
|
||||
if self.points[startPt] == self.points[endPt]:
|
||||
self._popPoint()
|
||||
endPt -= 1
|
||||
|
||||
self.endPts.append(endPt)
|
||||
|
||||
def endPath(self) -> None:
|
||||
# TrueType contours are always "closed"
|
||||
self.closePath()
|
||||
|
||||
|
||||
class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen):
|
||||
"""
|
||||
Point pen used for drawing to a TrueType glyph.
|
||||
|
||||
This pen can be used to construct or modify glyphs in a TrueType format
|
||||
font. After using the pen to draw, use the ``.glyph()`` method to retrieve
|
||||
a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
|
||||
"""
|
||||
|
||||
drawMethod = "drawPoints"
|
||||
transformPen = TransformPointPen
|
||||
|
||||
def init(self) -> None:
|
||||
super().init()
|
||||
self._currentContourStartIndex = None
|
||||
|
||||
def _isClosed(self) -> bool:
|
||||
return self._currentContourStartIndex is None
|
||||
|
||||
def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
|
||||
"""
|
||||
Start a new sub path.
|
||||
"""
|
||||
if not self._isClosed():
|
||||
raise PenError("Didn't close previous contour.")
|
||||
self._currentContourStartIndex = len(self.points)
|
||||
|
||||
def endPath(self) -> None:
|
||||
"""
|
||||
End the current sub path.
|
||||
"""
|
||||
# TrueType contours are always "closed"
|
||||
if self._isClosed():
|
||||
raise PenError("Contour is already closed.")
|
||||
if self._currentContourStartIndex == len(self.points):
|
||||
# ignore empty contours
|
||||
self._currentContourStartIndex = None
|
||||
return
|
||||
|
||||
contourStart = self.endPts[-1] + 1 if self.endPts else 0
|
||||
self.endPts.append(len(self.points) - 1)
|
||||
self._currentContourStartIndex = None
|
||||
|
||||
# Resolve types for any cubic segments
|
||||
flags = self.types
|
||||
for i in range(contourStart, len(flags)):
|
||||
if flags[i] == "curve":
|
||||
j = i - 1
|
||||
if j < contourStart:
|
||||
j = len(flags) - 1
|
||||
while flags[j] == 0:
|
||||
flags[j] = flagCubic
|
||||
j -= 1
|
||||
flags[i] = flagOnCurve
|
||||
|
||||
def addPoint(
|
||||
self,
|
||||
pt: Tuple[float, float],
|
||||
segmentType: Optional[str] = None,
|
||||
smooth: bool = False,
|
||||
name: Optional[str] = None,
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Add a point to the current sub path.
|
||||
"""
|
||||
if self._isClosed():
|
||||
raise PenError("Can't add a point to a closed contour.")
|
||||
if segmentType is None:
|
||||
self.types.append(0)
|
||||
elif segmentType in ("line", "move"):
|
||||
self.types.append(flagOnCurve)
|
||||
elif segmentType == "qcurve":
|
||||
self.types.append(flagOnCurve)
|
||||
elif segmentType == "curve":
|
||||
self.types.append("curve")
|
||||
else:
|
||||
raise AssertionError(segmentType)
|
||||
|
||||
self.points.append(pt)
|
||||
@@ -1,29 +0,0 @@
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
__all__ = ["WxPen"]
|
||||
|
||||
|
||||
class WxPen(BasePen):
|
||||
def __init__(self, glyphSet, path=None):
|
||||
BasePen.__init__(self, glyphSet)
|
||||
if path is None:
|
||||
import wx
|
||||
|
||||
path = wx.GraphicsRenderer.GetDefaultRenderer().CreatePath()
|
||||
self.path = path
|
||||
|
||||
def _moveTo(self, p):
|
||||
self.path.MoveToPoint(*p)
|
||||
|
||||
def _lineTo(self, p):
|
||||
self.path.AddLineToPoint(*p)
|
||||
|
||||
def _curveToOne(self, p1, p2, p3):
|
||||
self.path.AddCurveToPoint(*p1 + p2 + p3)
|
||||
|
||||
def _qCurveToOne(self, p1, p2):
|
||||
self.path.AddQuadCurveToPoint(*p1 + p2)
|
||||
|
||||
def _closePath(self):
|
||||
self.path.CloseSubpath()
|
||||
@@ -1,15 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .qu2cu import *
|
||||
@@ -1,7 +0,0 @@
|
||||
import sys
|
||||
|
||||
from .cli import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,57 +0,0 @@
|
||||
"""Benchmark the qu2cu algorithm performance."""
|
||||
|
||||
from .qu2cu import *
|
||||
from fontTools.cu2qu import curve_to_quadratic
|
||||
import random
|
||||
import timeit
|
||||
|
||||
MAX_ERR = 0.5
|
||||
NUM_CURVES = 5
|
||||
|
||||
|
||||
def generate_curves(n):
|
||||
points = [
|
||||
tuple(float(random.randint(0, 2048)) for coord in range(2))
|
||||
for point in range(1 + 3 * n)
|
||||
]
|
||||
curves = []
|
||||
for i in range(n):
|
||||
curves.append(tuple(points[i * 3 : i * 3 + 4]))
|
||||
return curves
|
||||
|
||||
|
||||
def setup_quadratic_to_curves():
|
||||
curves = generate_curves(NUM_CURVES)
|
||||
quadratics = [curve_to_quadratic(curve, MAX_ERR) for curve in curves]
|
||||
return quadratics, MAX_ERR
|
||||
|
||||
|
||||
def run_benchmark(module, function, setup_suffix="", repeat=25, number=1):
|
||||
setup_func = "setup_" + function
|
||||
if setup_suffix:
|
||||
print("%s with %s:" % (function, setup_suffix), end="")
|
||||
setup_func += "_" + setup_suffix
|
||||
else:
|
||||
print("%s:" % function, end="")
|
||||
|
||||
def wrapper(function, setup_func):
|
||||
function = globals()[function]
|
||||
setup_func = globals()[setup_func]
|
||||
|
||||
def wrapped():
|
||||
return function(*setup_func())
|
||||
|
||||
return wrapped
|
||||
|
||||
results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number)
|
||||
print("\t%5.1fus" % (min(results) * 1000000.0 / number))
|
||||
|
||||
|
||||
def main():
|
||||
"""Benchmark the qu2cu algorithm performance."""
|
||||
run_benchmark("qu2cu", "quadratic_to_curves")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
random.seed(1)
|
||||
main()
|
||||
@@ -1,125 +0,0 @@
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
from fontTools.misc.cliTools import makeOutputFileName
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.qu2cuPen import Qu2CuPen
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
import fontTools
|
||||
|
||||
|
||||
logger = logging.getLogger("fontTools.qu2cu")
|
||||
|
||||
|
||||
def _font_to_cubic(input_path, output_path=None, **kwargs):
|
||||
font = TTFont(input_path)
|
||||
logger.info("Converting curves for %s", input_path)
|
||||
|
||||
stats = {} if kwargs["dump_stats"] else None
|
||||
qu2cu_kwargs = {
|
||||
"stats": stats,
|
||||
"max_err": kwargs["max_err_em"] * font["head"].unitsPerEm,
|
||||
"all_cubic": kwargs["all_cubic"],
|
||||
}
|
||||
|
||||
assert "gvar" not in font, "Cannot convert variable font"
|
||||
glyphSet = font.getGlyphSet()
|
||||
glyphOrder = font.getGlyphOrder()
|
||||
glyf = font["glyf"]
|
||||
for glyphName in glyphOrder:
|
||||
glyph = glyphSet[glyphName]
|
||||
ttpen = TTGlyphPen(glyphSet)
|
||||
pen = Qu2CuPen(ttpen, **qu2cu_kwargs)
|
||||
glyph.draw(pen)
|
||||
glyf[glyphName] = ttpen.glyph(dropImpliedOnCurves=True)
|
||||
|
||||
font["head"].glyphDataFormat = 1
|
||||
|
||||
if kwargs["dump_stats"]:
|
||||
logger.info("Stats: %s", stats)
|
||||
|
||||
logger.info("Saving %s", output_path)
|
||||
font.save(output_path)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Convert an OpenType font from quadratic to cubic curves"""
|
||||
parser = argparse.ArgumentParser(prog="qu2cu")
|
||||
parser.add_argument("--version", action="version", version=fontTools.__version__)
|
||||
parser.add_argument(
|
||||
"infiles",
|
||||
nargs="+",
|
||||
metavar="INPUT",
|
||||
help="one or more input TTF source file(s).",
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", action="count", default=0)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--conversion-error",
|
||||
type=float,
|
||||
metavar="ERROR",
|
||||
default=0.001,
|
||||
help="maxiumum approximation error measured in EM (default: 0.001)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--all-cubic",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="whether to only use cubic curves",
|
||||
)
|
||||
|
||||
output_parser = parser.add_mutually_exclusive_group()
|
||||
output_parser.add_argument(
|
||||
"-o",
|
||||
"--output-file",
|
||||
default=None,
|
||||
metavar="OUTPUT",
|
||||
help=("output filename for the converted TTF."),
|
||||
)
|
||||
output_parser.add_argument(
|
||||
"-d",
|
||||
"--output-dir",
|
||||
default=None,
|
||||
metavar="DIRECTORY",
|
||||
help="output directory where to save converted TTFs",
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
if not options.verbose:
|
||||
level = "WARNING"
|
||||
elif options.verbose == 1:
|
||||
level = "INFO"
|
||||
else:
|
||||
level = "DEBUG"
|
||||
logging.basicConfig(level=level)
|
||||
|
||||
if len(options.infiles) > 1 and options.output_file:
|
||||
parser.error("-o/--output-file can't be used with multile inputs")
|
||||
|
||||
if options.output_dir:
|
||||
output_dir = options.output_dir
|
||||
if not os.path.exists(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
elif not os.path.isdir(output_dir):
|
||||
parser.error("'%s' is not a directory" % output_dir)
|
||||
output_paths = [
|
||||
os.path.join(output_dir, os.path.basename(p)) for p in options.infiles
|
||||
]
|
||||
elif options.output_file:
|
||||
output_paths = [options.output_file]
|
||||
else:
|
||||
output_paths = [
|
||||
makeOutputFileName(p, overWrite=True, suffix=".cubic")
|
||||
for p in options.infiles
|
||||
]
|
||||
|
||||
kwargs = dict(
|
||||
dump_stats=options.verbose > 0,
|
||||
max_err_em=options.conversion_error,
|
||||
all_cubic=options.all_cubic,
|
||||
)
|
||||
|
||||
for input_path, output_path in zip(options.infiles, output_paths):
|
||||
_font_to_cubic(input_path, output_path, **kwargs)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user