xref: /aosp_15_r20/external/fonttools/Lib/fontTools/misc/symfont.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.pens.basePen import BasePen
2from functools import partial
3from itertools import count
4import sympy as sp
5import sys
6
7n = 3  # Max Bezier degree; 3 for cubic, 2 for quadratic
8
9t, x, y = sp.symbols("t x y", real=True)
10c = sp.symbols("c", real=False)  # Complex representation instead of x/y
11
12X = tuple(sp.symbols("x:%d" % (n + 1), real=True))
13Y = tuple(sp.symbols("y:%d" % (n + 1), real=True))
14P = tuple(zip(*(sp.symbols("p:%d[%s]" % (n + 1, w), real=True) for w in "01")))
15C = tuple(sp.symbols("c:%d" % (n + 1), real=False))
16
17# Cubic Bernstein basis functions
18BinomialCoefficient = [(1, 0)]
19for i in range(1, n + 1):
20    last = BinomialCoefficient[-1]
21    this = tuple(last[j - 1] + last[j] for j in range(len(last))) + (0,)
22    BinomialCoefficient.append(this)
23BinomialCoefficient = tuple(tuple(item[:-1]) for item in BinomialCoefficient)
24del last, this
25
26BernsteinPolynomial = tuple(
27    tuple(c * t**i * (1 - t) ** (n - i) for i, c in enumerate(coeffs))
28    for n, coeffs in enumerate(BinomialCoefficient)
29)
30
31BezierCurve = tuple(
32    tuple(
33        sum(P[i][j] * bernstein for i, bernstein in enumerate(bernsteins))
34        for j in range(2)
35    )
36    for n, bernsteins in enumerate(BernsteinPolynomial)
37)
38BezierCurveC = tuple(
39    sum(C[i] * bernstein for i, bernstein in enumerate(bernsteins))
40    for n, bernsteins in enumerate(BernsteinPolynomial)
41)
42
43
44def green(f, curveXY):
45    f = -sp.integrate(sp.sympify(f), y)
46    f = f.subs({x: curveXY[0], y: curveXY[1]})
47    f = sp.integrate(f * sp.diff(curveXY[0], t), (t, 0, 1))
48    return f
49
50
51class _BezierFuncsLazy(dict):
52    def __init__(self, symfunc):
53        self._symfunc = symfunc
54        self._bezfuncs = {}
55
56    def __missing__(self, i):
57        args = ["p%d" % d for d in range(i + 1)]
58        f = green(self._symfunc, BezierCurve[i])
59        f = sp.gcd_terms(f.collect(sum(P, ())))  # Optimize
60        return sp.lambdify(args, f)
61
62
63class GreenPen(BasePen):
64    _BezierFuncs = {}
65
66    @classmethod
67    def _getGreenBezierFuncs(celf, func):
68        funcstr = str(func)
69        if not funcstr in celf._BezierFuncs:
70            celf._BezierFuncs[funcstr] = _BezierFuncsLazy(func)
71        return celf._BezierFuncs[funcstr]
72
73    def __init__(self, func, glyphset=None):
74        BasePen.__init__(self, glyphset)
75        self._funcs = self._getGreenBezierFuncs(func)
76        self.value = 0
77
78    def _moveTo(self, p0):
79        self.__startPoint = p0
80
81    def _closePath(self):
82        p0 = self._getCurrentPoint()
83        if p0 != self.__startPoint:
84            self._lineTo(self.__startPoint)
85
86    def _endPath(self):
87        p0 = self._getCurrentPoint()
88        if p0 != self.__startPoint:
89            # Green theorem is not defined on open contours.
90            raise NotImplementedError
91
92    def _lineTo(self, p1):
93        p0 = self._getCurrentPoint()
94        self.value += self._funcs[1](p0, p1)
95
96    def _qCurveToOne(self, p1, p2):
97        p0 = self._getCurrentPoint()
98        self.value += self._funcs[2](p0, p1, p2)
99
100    def _curveToOne(self, p1, p2, p3):
101        p0 = self._getCurrentPoint()
102        self.value += self._funcs[3](p0, p1, p2, p3)
103
104
105# Sample pens.
106# Do not use this in real code.
107# Use fontTools.pens.momentsPen.MomentsPen instead.
108AreaPen = partial(GreenPen, func=1)
109MomentXPen = partial(GreenPen, func=x)
110MomentYPen = partial(GreenPen, func=y)
111MomentXXPen = partial(GreenPen, func=x * x)
112MomentYYPen = partial(GreenPen, func=y * y)
113MomentXYPen = partial(GreenPen, func=x * y)
114
115
116def printGreenPen(penName, funcs, file=sys.stdout, docstring=None):
117    if docstring is not None:
118        print('"""%s"""' % docstring)
119
120    print(
121        """from fontTools.pens.basePen import BasePen, OpenContourError
122try:
123	import cython
124
125	COMPILED = cython.compiled
126except (AttributeError, ImportError):
127	# if cython not installed, use mock module with no-op decorators and types
128	from fontTools.misc import cython
129
130	COMPILED = False
131
132
133__all__ = ["%s"]
134
135class %s(BasePen):
136
137	def __init__(self, glyphset=None):
138		BasePen.__init__(self, glyphset)
139"""
140        % (penName, penName),
141        file=file,
142    )
143    for name, f in funcs:
144        print("		self.%s = 0" % name, file=file)
145    print(
146        """
147	def _moveTo(self, p0):
148		self.__startPoint = p0
149
150	def _closePath(self):
151		p0 = self._getCurrentPoint()
152		if p0 != self.__startPoint:
153			self._lineTo(self.__startPoint)
154
155	def _endPath(self):
156		p0 = self._getCurrentPoint()
157		if p0 != self.__startPoint:
158			# Green theorem is not defined on open contours.
159			raise OpenContourError(
160							"Green theorem is not defined on open contours."
161			)
162""",
163        end="",
164        file=file,
165    )
166
167    for n in (1, 2, 3):
168        subs = {P[i][j]: [X, Y][j][i] for i in range(n + 1) for j in range(2)}
169        greens = [green(f, BezierCurve[n]) for name, f in funcs]
170        greens = [sp.gcd_terms(f.collect(sum(P, ()))) for f in greens]  # Optimize
171        greens = [f.subs(subs) for f in greens]  # Convert to p to x/y
172        defs, exprs = sp.cse(
173            greens,
174            optimizations="basic",
175            symbols=(sp.Symbol("r%d" % i) for i in count()),
176        )
177
178        print()
179        for name, value in defs:
180            print("	@cython.locals(%s=cython.double)" % name, file=file)
181        if n == 1:
182            print(
183                """\
184	@cython.locals(x0=cython.double, y0=cython.double)
185	@cython.locals(x1=cython.double, y1=cython.double)
186	def _lineTo(self, p1):
187		x0,y0 = self._getCurrentPoint()
188		x1,y1 = p1
189""",
190                file=file,
191            )
192        elif n == 2:
193            print(
194                """\
195	@cython.locals(x0=cython.double, y0=cython.double)
196	@cython.locals(x1=cython.double, y1=cython.double)
197	@cython.locals(x2=cython.double, y2=cython.double)
198	def _qCurveToOne(self, p1, p2):
199		x0,y0 = self._getCurrentPoint()
200		x1,y1 = p1
201		x2,y2 = p2
202""",
203                file=file,
204            )
205        elif n == 3:
206            print(
207                """\
208	@cython.locals(x0=cython.double, y0=cython.double)
209	@cython.locals(x1=cython.double, y1=cython.double)
210	@cython.locals(x2=cython.double, y2=cython.double)
211	@cython.locals(x3=cython.double, y3=cython.double)
212	def _curveToOne(self, p1, p2, p3):
213		x0,y0 = self._getCurrentPoint()
214		x1,y1 = p1
215		x2,y2 = p2
216		x3,y3 = p3
217""",
218                file=file,
219            )
220        for name, value in defs:
221            print("		%s = %s" % (name, value), file=file)
222
223        print(file=file)
224        for name, value in zip([f[0] for f in funcs], exprs):
225            print("		self.%s += %s" % (name, value), file=file)
226
227    print(
228        """
229if __name__ == '__main__':
230	from fontTools.misc.symfont import x, y, printGreenPen
231	printGreenPen('%s', ["""
232        % penName,
233        file=file,
234    )
235    for name, f in funcs:
236        print("		      ('%s', %s)," % (name, str(f)), file=file)
237    print("		     ])", file=file)
238
239
240if __name__ == "__main__":
241    pen = AreaPen()
242    pen.moveTo((100, 100))
243    pen.lineTo((100, 200))
244    pen.lineTo((200, 200))
245    pen.curveTo((200, 250), (300, 300), (250, 350))
246    pen.lineTo((200, 100))
247    pen.closePath()
248    print(pen.value)
249