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: raise OpenContourError( "Glyph statistics 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)