Kurtgitgit commited on
Commit
af91822
·
verified ·
1 Parent(s): a4aa38a

Upload folder using huggingface_hub (part 6)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +7 -0
  2. venv/Lib/site-packages/fontTools/pens/__pycache__/quartzPen.cpython-311.pyc +0 -0
  3. venv/Lib/site-packages/fontTools/pens/__pycache__/recordingPen.cpython-311.pyc +0 -0
  4. venv/Lib/site-packages/fontTools/pens/__pycache__/reportLabPen.cpython-311.pyc +0 -0
  5. venv/Lib/site-packages/fontTools/pens/__pycache__/reverseContourPen.cpython-311.pyc +0 -0
  6. venv/Lib/site-packages/fontTools/pens/__pycache__/roundingPen.cpython-311.pyc +0 -0
  7. venv/Lib/site-packages/fontTools/pens/__pycache__/statisticsPen.cpython-311.pyc +0 -0
  8. venv/Lib/site-packages/fontTools/pens/__pycache__/svgPathPen.cpython-311.pyc +0 -0
  9. venv/Lib/site-packages/fontTools/pens/__pycache__/t2CharStringPen.cpython-311.pyc +0 -0
  10. venv/Lib/site-packages/fontTools/pens/__pycache__/teePen.cpython-311.pyc +0 -0
  11. venv/Lib/site-packages/fontTools/pens/__pycache__/transformPen.cpython-311.pyc +0 -0
  12. venv/Lib/site-packages/fontTools/pens/__pycache__/ttGlyphPen.cpython-311.pyc +0 -0
  13. venv/Lib/site-packages/fontTools/pens/__pycache__/wxPen.cpython-311.pyc +0 -0
  14. venv/Lib/site-packages/fontTools/pens/areaPen.py +52 -0
  15. venv/Lib/site-packages/fontTools/pens/basePen.py +475 -0
  16. venv/Lib/site-packages/fontTools/pens/boundsPen.py +98 -0
  17. venv/Lib/site-packages/fontTools/pens/cairoPen.py +26 -0
  18. venv/Lib/site-packages/fontTools/pens/cocoaPen.py +26 -0
  19. venv/Lib/site-packages/fontTools/pens/cu2quPen.py +325 -0
  20. venv/Lib/site-packages/fontTools/pens/explicitClosingLinePen.py +101 -0
  21. venv/Lib/site-packages/fontTools/pens/filterPen.py +433 -0
  22. venv/Lib/site-packages/fontTools/pens/freetypePen.py +462 -0
  23. venv/Lib/site-packages/fontTools/pens/hashPointPen.py +89 -0
  24. venv/Lib/site-packages/fontTools/pens/momentsPen.c +0 -0
  25. venv/Lib/site-packages/fontTools/pens/momentsPen.cp311-win_amd64.pyd +0 -0
  26. venv/Lib/site-packages/fontTools/pens/momentsPen.py +879 -0
  27. venv/Lib/site-packages/fontTools/pens/perimeterPen.py +69 -0
  28. venv/Lib/site-packages/fontTools/pens/pointInsidePen.py +192 -0
  29. venv/Lib/site-packages/fontTools/pens/pointPen.py +643 -0
  30. venv/Lib/site-packages/fontTools/pens/qtPen.py +29 -0
  31. venv/Lib/site-packages/fontTools/pens/qu2cuPen.py +105 -0
  32. venv/Lib/site-packages/fontTools/pens/quartzPen.py +43 -0
  33. venv/Lib/site-packages/fontTools/pens/recordingPen.py +335 -0
  34. venv/Lib/site-packages/fontTools/pens/reportLabPen.py +79 -0
  35. venv/Lib/site-packages/fontTools/pens/reverseContourPen.py +96 -0
  36. venv/Lib/site-packages/fontTools/pens/roundingPen.py +130 -0
  37. venv/Lib/site-packages/fontTools/pens/statisticsPen.py +312 -0
  38. venv/Lib/site-packages/fontTools/pens/svgPathPen.py +310 -0
  39. venv/Lib/site-packages/fontTools/pens/t2CharStringPen.py +88 -0
  40. venv/Lib/site-packages/fontTools/pens/teePen.py +55 -0
  41. venv/Lib/site-packages/fontTools/pens/transformPen.py +115 -0
  42. venv/Lib/site-packages/fontTools/pens/ttGlyphPen.py +335 -0
  43. venv/Lib/site-packages/fontTools/pens/wxPen.py +29 -0
  44. venv/Lib/site-packages/fontTools/qu2cu/__init__.py +15 -0
  45. venv/Lib/site-packages/fontTools/qu2cu/__main__.py +7 -0
  46. venv/Lib/site-packages/fontTools/qu2cu/__pycache__/__init__.cpython-311.pyc +0 -0
  47. venv/Lib/site-packages/fontTools/qu2cu/__pycache__/__main__.cpython-311.pyc +0 -0
  48. venv/Lib/site-packages/fontTools/qu2cu/__pycache__/benchmark.cpython-311.pyc +0 -0
  49. venv/Lib/site-packages/fontTools/qu2cu/__pycache__/cli.cpython-311.pyc +0 -0
  50. venv/Lib/site-packages/fontTools/qu2cu/__pycache__/qu2cu.cpython-311.pyc +0 -0
.gitattributes CHANGED
@@ -62,3 +62,10 @@ venv/Lib/site-packages/fontTools/feaLib/__pycache__/parser.cpython-311.pyc filte
62
  venv/Lib/site-packages/fontTools/feaLib/lexer.cp311-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
63
  venv/Lib/site-packages/fontTools/misc/bezierTools.cp311-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
64
  venv/Lib/site-packages/fontTools/otlLib/__pycache__/builder.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
62
  venv/Lib/site-packages/fontTools/feaLib/lexer.cp311-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
63
  venv/Lib/site-packages/fontTools/misc/bezierTools.cp311-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
64
  venv/Lib/site-packages/fontTools/otlLib/__pycache__/builder.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
65
+ venv/Lib/site-packages/fontTools/qu2cu/qu2cu.cp311-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
66
+ venv/Lib/site-packages/fontTools/subset/__pycache__/__init__.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
67
+ venv/Lib/site-packages/fontTools/ttLib/tables/__pycache__/_g_l_y_f.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
68
+ venv/Lib/site-packages/fontTools/ttLib/tables/__pycache__/otConverters.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
69
+ venv/Lib/site-packages/fontTools/ttLib/tables/__pycache__/otTables.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
70
+ venv/Lib/site-packages/fontTools/ufoLib/__pycache__/__init__.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
71
+ venv/Lib/site-packages/fontTools/varLib/iup.cp311-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
venv/Lib/site-packages/fontTools/pens/__pycache__/quartzPen.cpython-311.pyc ADDED
Binary file (2.76 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/recordingPen.cpython-311.pyc ADDED
Binary file (16.4 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/reportLabPen.cpython-311.pyc ADDED
Binary file (4.02 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/reverseContourPen.cpython-311.pyc ADDED
Binary file (4.03 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/roundingPen.cpython-311.pyc ADDED
Binary file (7.49 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/statisticsPen.cpython-311.pyc ADDED
Binary file (14.9 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/svgPathPen.cpython-311.pyc ADDED
Binary file (12.5 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/t2CharStringPen.cpython-311.pyc ADDED
Binary file (5.15 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/teePen.cpython-311.pyc ADDED
Binary file (3.17 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/transformPen.cpython-311.pyc ADDED
Binary file (7.09 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/ttGlyphPen.cpython-311.pyc ADDED
Binary file (18.2 kB). View file
 
venv/Lib/site-packages/fontTools/pens/__pycache__/wxPen.cpython-311.pyc ADDED
Binary file (2.08 kB). View file
 
venv/Lib/site-packages/fontTools/pens/areaPen.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Calculate the area of a glyph."""
2
+
3
+ from fontTools.pens.basePen import BasePen
4
+
5
+
6
+ __all__ = ["AreaPen"]
7
+
8
+
9
+ class AreaPen(BasePen):
10
+ def __init__(self, glyphset=None):
11
+ BasePen.__init__(self, glyphset)
12
+ self.value = 0
13
+
14
+ def _moveTo(self, p0):
15
+ self._p0 = self._startPoint = p0
16
+
17
+ def _lineTo(self, p1):
18
+ x0, y0 = self._p0
19
+ x1, y1 = p1
20
+ self.value -= (x1 - x0) * (y1 + y0) * 0.5
21
+ self._p0 = p1
22
+
23
+ def _qCurveToOne(self, p1, p2):
24
+ # https://github.com/Pomax/bezierinfo/issues/44
25
+ p0 = self._p0
26
+ x0, y0 = p0[0], p0[1]
27
+ x1, y1 = p1[0] - x0, p1[1] - y0
28
+ x2, y2 = p2[0] - x0, p2[1] - y0
29
+ self.value -= (x2 * y1 - x1 * y2) / 3
30
+ self._lineTo(p2)
31
+ self._p0 = p2
32
+
33
+ def _curveToOne(self, p1, p2, p3):
34
+ # https://github.com/Pomax/bezierinfo/issues/44
35
+ p0 = self._p0
36
+ x0, y0 = p0[0], p0[1]
37
+ x1, y1 = p1[0] - x0, p1[1] - y0
38
+ x2, y2 = p2[0] - x0, p2[1] - y0
39
+ x3, y3 = p3[0] - x0, p3[1] - y0
40
+ self.value -= (x1 * (-y2 - y3) + x2 * (y1 - 2 * y3) + x3 * (y1 + 2 * y2)) * 0.15
41
+ self._lineTo(p3)
42
+ self._p0 = p3
43
+
44
+ def _closePath(self):
45
+ self._lineTo(self._startPoint)
46
+ del self._p0, self._startPoint
47
+
48
+ def _endPath(self):
49
+ if self._p0 != self._startPoint:
50
+ # Area is not defined for open contours.
51
+ raise NotImplementedError
52
+ del self._p0, self._startPoint
venv/Lib/site-packages/fontTools/pens/basePen.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """fontTools.pens.basePen.py -- Tools and base classes to build pen objects.
2
+
3
+ The Pen Protocol
4
+
5
+ A Pen is a kind of object that standardizes the way how to "draw" outlines:
6
+ it is a middle man between an outline and a drawing. In other words:
7
+ it is an abstraction for drawing outlines, making sure that outline objects
8
+ don't need to know the details about how and where they're being drawn, and
9
+ that drawings don't need to know the details of how outlines are stored.
10
+
11
+ The most basic pattern is this::
12
+
13
+ outline.draw(pen) # 'outline' draws itself onto 'pen'
14
+
15
+ Pens can be used to render outlines to the screen, but also to construct
16
+ new outlines. Eg. an outline object can be both a drawable object (it has a
17
+ draw() method) as well as a pen itself: you *build* an outline using pen
18
+ methods.
19
+
20
+ The AbstractPen class defines the Pen protocol. It implements almost
21
+ nothing (only no-op closePath() and endPath() methods), but is useful
22
+ for documentation purposes. Subclassing it basically tells the reader:
23
+ "this class implements the Pen protocol.". An examples of an AbstractPen
24
+ subclass is :py:class:`fontTools.pens.transformPen.TransformPen`.
25
+
26
+ The BasePen class is a base implementation useful for pens that actually
27
+ draw (for example a pen renders outlines using a native graphics engine).
28
+ BasePen contains a lot of base functionality, making it very easy to build
29
+ a pen that fully conforms to the pen protocol. Note that if you subclass
30
+ BasePen, you *don't* override moveTo(), lineTo(), etc., but _moveTo(),
31
+ _lineTo(), etc. See the BasePen doc string for details. Examples of
32
+ BasePen subclasses are fontTools.pens.boundsPen.BoundsPen and
33
+ fontTools.pens.cocoaPen.CocoaPen.
34
+
35
+ Coordinates are usually expressed as (x, y) tuples, but generally any
36
+ sequence of length 2 will do.
37
+ """
38
+
39
+ from typing import Tuple, Dict
40
+
41
+ from fontTools.misc.loggingTools import LogMixin
42
+ from fontTools.misc.transform import DecomposedTransform, Identity
43
+
44
+ __all__ = [
45
+ "AbstractPen",
46
+ "NullPen",
47
+ "BasePen",
48
+ "PenError",
49
+ "decomposeSuperBezierSegment",
50
+ "decomposeQuadraticSegment",
51
+ ]
52
+
53
+
54
+ class PenError(Exception):
55
+ """Represents an error during penning."""
56
+
57
+
58
+ class OpenContourError(PenError):
59
+ pass
60
+
61
+
62
+ class AbstractPen:
63
+ def moveTo(self, pt: Tuple[float, float]) -> None:
64
+ """Begin a new sub path, set the current point to 'pt'. You must
65
+ end each sub path with a call to pen.closePath() or pen.endPath().
66
+ """
67
+ raise NotImplementedError
68
+
69
+ def lineTo(self, pt: Tuple[float, float]) -> None:
70
+ """Draw a straight line from the current point to 'pt'."""
71
+ raise NotImplementedError
72
+
73
+ def curveTo(self, *points: Tuple[float, float]) -> None:
74
+ """Draw a cubic bezier with an arbitrary number of control points.
75
+
76
+ The last point specified is on-curve, all others are off-curve
77
+ (control) points. If the number of control points is > 2, the
78
+ segment is split into multiple bezier segments. This works
79
+ like this:
80
+
81
+ Let n be the number of control points (which is the number of
82
+ arguments to this call minus 1). If n==2, a plain vanilla cubic
83
+ bezier is drawn. If n==1, we fall back to a quadratic segment and
84
+ if n==0 we draw a straight line. It gets interesting when n>2:
85
+ n-1 PostScript-style cubic segments will be drawn as if it were
86
+ one curve. See decomposeSuperBezierSegment().
87
+
88
+ The conversion algorithm used for n>2 is inspired by NURB
89
+ splines, and is conceptually equivalent to the TrueType "implied
90
+ points" principle. See also decomposeQuadraticSegment().
91
+ """
92
+ raise NotImplementedError
93
+
94
+ def qCurveTo(self, *points: Tuple[float, float]) -> None:
95
+ """Draw a whole string of quadratic curve segments.
96
+
97
+ The last point specified is on-curve, all others are off-curve
98
+ points.
99
+
100
+ This method implements TrueType-style curves, breaking up curves
101
+ using 'implied points': between each two consequtive off-curve points,
102
+ there is one implied point exactly in the middle between them. See
103
+ also decomposeQuadraticSegment().
104
+
105
+ The last argument (normally the on-curve point) may be None.
106
+ This is to support contours that have NO on-curve points (a rarely
107
+ seen feature of TrueType outlines).
108
+ """
109
+ raise NotImplementedError
110
+
111
+ def closePath(self) -> None:
112
+ """Close the current sub path. You must call either pen.closePath()
113
+ or pen.endPath() after each sub path.
114
+ """
115
+ pass
116
+
117
+ def endPath(self) -> None:
118
+ """End the current sub path, but don't close it. You must call
119
+ either pen.closePath() or pen.endPath() after each sub path.
120
+ """
121
+ pass
122
+
123
+ def addComponent(
124
+ self,
125
+ glyphName: str,
126
+ transformation: Tuple[float, float, float, float, float, float],
127
+ ) -> None:
128
+ """Add a sub glyph. The 'transformation' argument must be a 6-tuple
129
+ containing an affine transformation, or a Transform object from the
130
+ fontTools.misc.transform module. More precisely: it should be a
131
+ sequence containing 6 numbers.
132
+ """
133
+ raise NotImplementedError
134
+
135
+ def addVarComponent(
136
+ self,
137
+ glyphName: str,
138
+ transformation: DecomposedTransform,
139
+ location: Dict[str, float],
140
+ ) -> None:
141
+ """Add a VarComponent sub glyph. The 'transformation' argument
142
+ must be a DecomposedTransform from the fontTools.misc.transform module,
143
+ and the 'location' argument must be a dictionary mapping axis tags
144
+ to their locations.
145
+ """
146
+ # GlyphSet decomposes for us
147
+ raise AttributeError
148
+
149
+
150
+ class NullPen(AbstractPen):
151
+ """A pen that does nothing."""
152
+
153
+ def moveTo(self, pt):
154
+ pass
155
+
156
+ def lineTo(self, pt):
157
+ pass
158
+
159
+ def curveTo(self, *points):
160
+ pass
161
+
162
+ def qCurveTo(self, *points):
163
+ pass
164
+
165
+ def closePath(self):
166
+ pass
167
+
168
+ def endPath(self):
169
+ pass
170
+
171
+ def addComponent(self, glyphName, transformation):
172
+ pass
173
+
174
+ def addVarComponent(self, glyphName, transformation, location):
175
+ pass
176
+
177
+
178
+ class LoggingPen(LogMixin, AbstractPen):
179
+ """A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)"""
180
+
181
+ pass
182
+
183
+
184
+ class MissingComponentError(KeyError):
185
+ """Indicates a component pointing to a non-existent glyph in the glyphset."""
186
+
187
+
188
+ class DecomposingPen(LoggingPen):
189
+ """Implements a 'addComponent' method that decomposes components
190
+ (i.e. draws them onto self as simple contours).
191
+ It can also be used as a mixin class (e.g. see ContourRecordingPen).
192
+
193
+ You must override moveTo, lineTo, curveTo and qCurveTo. You may
194
+ additionally override closePath, endPath and addComponent.
195
+
196
+ By default a warning message is logged when a base glyph is missing;
197
+ set the class variable ``skipMissingComponents`` to False if you want
198
+ all instances of a sub-class to raise a :class:`MissingComponentError`
199
+ exception by default.
200
+ """
201
+
202
+ skipMissingComponents = True
203
+ # alias error for convenience
204
+ MissingComponentError = MissingComponentError
205
+
206
+ def __init__(
207
+ self,
208
+ glyphSet,
209
+ *args,
210
+ skipMissingComponents=None,
211
+ reverseFlipped=False,
212
+ **kwargs,
213
+ ):
214
+ """Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
215
+ as components are looked up by their name.
216
+
217
+ If the optional 'reverseFlipped' argument is True, components whose transformation
218
+ matrix has a negative determinant will be decomposed with a reversed path direction
219
+ to compensate for the flip.
220
+
221
+ The optional 'skipMissingComponents' argument can be set to True/False to
222
+ override the homonymous class attribute for a given pen instance.
223
+ """
224
+ super(DecomposingPen, self).__init__(*args, **kwargs)
225
+ self.glyphSet = glyphSet
226
+ self.skipMissingComponents = (
227
+ self.__class__.skipMissingComponents
228
+ if skipMissingComponents is None
229
+ else skipMissingComponents
230
+ )
231
+ self.reverseFlipped = reverseFlipped
232
+
233
+ def addComponent(self, glyphName, transformation):
234
+ """Transform the points of the base glyph and draw it onto self."""
235
+ from fontTools.pens.transformPen import TransformPen
236
+
237
+ try:
238
+ glyph = self.glyphSet[glyphName]
239
+ except KeyError:
240
+ if not self.skipMissingComponents:
241
+ raise MissingComponentError(glyphName)
242
+ self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName)
243
+ else:
244
+ pen = self
245
+ if transformation != Identity:
246
+ pen = TransformPen(pen, transformation)
247
+ if self.reverseFlipped:
248
+ # if the transformation has a negative determinant, it will
249
+ # reverse the contour direction of the component
250
+ a, b, c, d = transformation[:4]
251
+ det = a * d - b * c
252
+ if det < 0:
253
+ from fontTools.pens.reverseContourPen import ReverseContourPen
254
+
255
+ pen = ReverseContourPen(pen)
256
+ glyph.draw(pen)
257
+
258
+ def addVarComponent(self, glyphName, transformation, location):
259
+ # GlyphSet decomposes for us
260
+ raise AttributeError
261
+
262
+
263
+ class BasePen(DecomposingPen):
264
+ """Base class for drawing pens. You must override _moveTo, _lineTo and
265
+ _curveToOne. You may additionally override _closePath, _endPath,
266
+ addComponent, addVarComponent, and/or _qCurveToOne. You should not
267
+ override any other methods.
268
+ """
269
+
270
+ def __init__(self, glyphSet=None):
271
+ super(BasePen, self).__init__(glyphSet)
272
+ self.__currentPoint = None
273
+
274
+ # must override
275
+
276
+ def _moveTo(self, pt):
277
+ raise NotImplementedError
278
+
279
+ def _lineTo(self, pt):
280
+ raise NotImplementedError
281
+
282
+ def _curveToOne(self, pt1, pt2, pt3):
283
+ raise NotImplementedError
284
+
285
+ # may override
286
+
287
+ def _closePath(self):
288
+ pass
289
+
290
+ def _endPath(self):
291
+ pass
292
+
293
+ def _qCurveToOne(self, pt1, pt2):
294
+ """This method implements the basic quadratic curve type. The
295
+ default implementation delegates the work to the cubic curve
296
+ function. Optionally override with a native implementation.
297
+ """
298
+ pt0x, pt0y = self.__currentPoint
299
+ pt1x, pt1y = pt1
300
+ pt2x, pt2y = pt2
301
+ mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
302
+ mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
303
+ mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
304
+ mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
305
+ self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
306
+
307
+ # don't override
308
+
309
+ def _getCurrentPoint(self):
310
+ """Return the current point. This is not part of the public
311
+ interface, yet is useful for subclasses.
312
+ """
313
+ return self.__currentPoint
314
+
315
+ def closePath(self):
316
+ self._closePath()
317
+ self.__currentPoint = None
318
+
319
+ def endPath(self):
320
+ self._endPath()
321
+ self.__currentPoint = None
322
+
323
+ def moveTo(self, pt):
324
+ self._moveTo(pt)
325
+ self.__currentPoint = pt
326
+
327
+ def lineTo(self, pt):
328
+ self._lineTo(pt)
329
+ self.__currentPoint = pt
330
+
331
+ def curveTo(self, *points):
332
+ n = len(points) - 1 # 'n' is the number of control points
333
+ assert n >= 0
334
+ if n == 2:
335
+ # The common case, we have exactly two BCP's, so this is a standard
336
+ # cubic bezier. Even though decomposeSuperBezierSegment() handles
337
+ # this case just fine, we special-case it anyway since it's so
338
+ # common.
339
+ self._curveToOne(*points)
340
+ self.__currentPoint = points[-1]
341
+ elif n > 2:
342
+ # n is the number of control points; split curve into n-1 cubic
343
+ # bezier segments. The algorithm used here is inspired by NURB
344
+ # splines and the TrueType "implied point" principle, and ensures
345
+ # the smoothest possible connection between two curve segments,
346
+ # with no disruption in the curvature. It is practical since it
347
+ # allows one to construct multiple bezier segments with a much
348
+ # smaller amount of points.
349
+ _curveToOne = self._curveToOne
350
+ for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
351
+ _curveToOne(pt1, pt2, pt3)
352
+ self.__currentPoint = pt3
353
+ elif n == 1:
354
+ self.qCurveTo(*points)
355
+ elif n == 0:
356
+ self.lineTo(points[0])
357
+ else:
358
+ raise AssertionError("can't get there from here")
359
+
360
+ def qCurveTo(self, *points):
361
+ n = len(points) - 1 # 'n' is the number of control points
362
+ assert n >= 0
363
+ if points[-1] is None:
364
+ # Special case for TrueType quadratics: it is possible to
365
+ # define a contour with NO on-curve points. BasePen supports
366
+ # this by allowing the final argument (the expected on-curve
367
+ # point) to be None. We simulate the feature by making the implied
368
+ # on-curve point between the last and the first off-curve points
369
+ # explicit.
370
+ x, y = points[-2] # last off-curve point
371
+ nx, ny = points[0] # first off-curve point
372
+ impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
373
+ self.__currentPoint = impliedStartPoint
374
+ self._moveTo(impliedStartPoint)
375
+ points = points[:-1] + (impliedStartPoint,)
376
+ if n > 0:
377
+ # Split the string of points into discrete quadratic curve
378
+ # segments. Between any two consecutive off-curve points
379
+ # there's an implied on-curve point exactly in the middle.
380
+ # This is where the segment splits.
381
+ _qCurveToOne = self._qCurveToOne
382
+ for pt1, pt2 in decomposeQuadraticSegment(points):
383
+ _qCurveToOne(pt1, pt2)
384
+ self.__currentPoint = pt2
385
+ else:
386
+ self.lineTo(points[0])
387
+
388
+
389
+ def decomposeSuperBezierSegment(points):
390
+ """Split the SuperBezier described by 'points' into a list of regular
391
+ bezier segments. The 'points' argument must be a sequence with length
392
+ 3 or greater, containing (x, y) coordinates. The last point is the
393
+ destination on-curve point, the rest of the points are off-curve points.
394
+ The start point should not be supplied.
395
+
396
+ This function returns a list of (pt1, pt2, pt3) tuples, which each
397
+ specify a regular curveto-style bezier segment.
398
+ """
399
+ n = len(points) - 1
400
+ assert n > 1
401
+ bezierSegments = []
402
+ pt1, pt2, pt3 = points[0], None, None
403
+ for i in range(2, n + 1):
404
+ # calculate points in between control points.
405
+ nDivisions = min(i, 3, n - i + 2)
406
+ for j in range(1, nDivisions):
407
+ factor = j / nDivisions
408
+ temp1 = points[i - 1]
409
+ temp2 = points[i - 2]
410
+ temp = (
411
+ temp2[0] + factor * (temp1[0] - temp2[0]),
412
+ temp2[1] + factor * (temp1[1] - temp2[1]),
413
+ )
414
+ if pt2 is None:
415
+ pt2 = temp
416
+ else:
417
+ pt3 = (0.5 * (pt2[0] + temp[0]), 0.5 * (pt2[1] + temp[1]))
418
+ bezierSegments.append((pt1, pt2, pt3))
419
+ pt1, pt2, pt3 = temp, None, None
420
+ bezierSegments.append((pt1, points[-2], points[-1]))
421
+ return bezierSegments
422
+
423
+
424
+ def decomposeQuadraticSegment(points):
425
+ """Split the quadratic curve segment described by 'points' into a list
426
+ of "atomic" quadratic segments. The 'points' argument must be a sequence
427
+ with length 2 or greater, containing (x, y) coordinates. The last point
428
+ is the destination on-curve point, the rest of the points are off-curve
429
+ points. The start point should not be supplied.
430
+
431
+ This function returns a list of (pt1, pt2) tuples, which each specify a
432
+ plain quadratic bezier segment.
433
+ """
434
+ n = len(points) - 1
435
+ assert n > 0
436
+ quadSegments = []
437
+ for i in range(n - 1):
438
+ x, y = points[i]
439
+ nx, ny = points[i + 1]
440
+ impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
441
+ quadSegments.append((points[i], impliedPt))
442
+ quadSegments.append((points[-2], points[-1]))
443
+ return quadSegments
444
+
445
+
446
+ class _TestPen(BasePen):
447
+ """Test class that prints PostScript to stdout."""
448
+
449
+ def _moveTo(self, pt):
450
+ print("%s %s moveto" % (pt[0], pt[1]))
451
+
452
+ def _lineTo(self, pt):
453
+ print("%s %s lineto" % (pt[0], pt[1]))
454
+
455
+ def _curveToOne(self, bcp1, bcp2, pt):
456
+ print(
457
+ "%s %s %s %s %s %s curveto"
458
+ % (bcp1[0], bcp1[1], bcp2[0], bcp2[1], pt[0], pt[1])
459
+ )
460
+
461
+ def _closePath(self):
462
+ print("closepath")
463
+
464
+
465
+ if __name__ == "__main__":
466
+ pen = _TestPen(None)
467
+ pen.moveTo((0, 0))
468
+ pen.lineTo((0, 100))
469
+ pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
470
+ pen.closePath()
471
+
472
+ pen = _TestPen(None)
473
+ # testing the "no on-curve point" scenario
474
+ pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
475
+ pen.closePath()
venv/Lib/site-packages/fontTools/pens/boundsPen.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.misc.arrayTools import updateBounds, pointInRect, unionRect
2
+ from fontTools.misc.bezierTools import calcCubicBounds, calcQuadraticBounds
3
+ from fontTools.pens.basePen import BasePen
4
+
5
+
6
+ __all__ = ["BoundsPen", "ControlBoundsPen"]
7
+
8
+
9
+ class ControlBoundsPen(BasePen):
10
+ """Pen to calculate the "control bounds" of a shape. This is the
11
+ bounding box of all control points, so may be larger than the
12
+ actual bounding box if there are curves that don't have points
13
+ on their extremes.
14
+
15
+ When the shape has been drawn, the bounds are available as the
16
+ ``bounds`` attribute of the pen object. It's a 4-tuple::
17
+
18
+ (xMin, yMin, xMax, yMax).
19
+
20
+ If ``ignoreSinglePoints`` is True, single points are ignored.
21
+ """
22
+
23
+ def __init__(self, glyphSet, ignoreSinglePoints=False):
24
+ BasePen.__init__(self, glyphSet)
25
+ self.ignoreSinglePoints = ignoreSinglePoints
26
+ self.init()
27
+
28
+ def init(self):
29
+ self.bounds = None
30
+ self._start = None
31
+
32
+ def _moveTo(self, pt):
33
+ self._start = pt
34
+ if not self.ignoreSinglePoints:
35
+ self._addMoveTo()
36
+
37
+ def _addMoveTo(self):
38
+ if self._start is None:
39
+ return
40
+ bounds = self.bounds
41
+ if bounds:
42
+ self.bounds = updateBounds(bounds, self._start)
43
+ else:
44
+ x, y = self._start
45
+ self.bounds = (x, y, x, y)
46
+ self._start = None
47
+
48
+ def _lineTo(self, pt):
49
+ self._addMoveTo()
50
+ self.bounds = updateBounds(self.bounds, pt)
51
+
52
+ def _curveToOne(self, bcp1, bcp2, pt):
53
+ self._addMoveTo()
54
+ bounds = self.bounds
55
+ bounds = updateBounds(bounds, bcp1)
56
+ bounds = updateBounds(bounds, bcp2)
57
+ bounds = updateBounds(bounds, pt)
58
+ self.bounds = bounds
59
+
60
+ def _qCurveToOne(self, bcp, pt):
61
+ self._addMoveTo()
62
+ bounds = self.bounds
63
+ bounds = updateBounds(bounds, bcp)
64
+ bounds = updateBounds(bounds, pt)
65
+ self.bounds = bounds
66
+
67
+
68
+ class BoundsPen(ControlBoundsPen):
69
+ """Pen to calculate the bounds of a shape. It calculates the
70
+ correct bounds even when the shape contains curves that don't
71
+ have points on their extremes. This is somewhat slower to compute
72
+ than the "control bounds".
73
+
74
+ When the shape has been drawn, the bounds are available as the
75
+ ``bounds`` attribute of the pen object. It's a 4-tuple::
76
+
77
+ (xMin, yMin, xMax, yMax)
78
+ """
79
+
80
+ def _curveToOne(self, bcp1, bcp2, pt):
81
+ self._addMoveTo()
82
+ bounds = self.bounds
83
+ bounds = updateBounds(bounds, pt)
84
+ if not pointInRect(bcp1, bounds) or not pointInRect(bcp2, bounds):
85
+ bounds = unionRect(
86
+ bounds, calcCubicBounds(self._getCurrentPoint(), bcp1, bcp2, pt)
87
+ )
88
+ self.bounds = bounds
89
+
90
+ def _qCurveToOne(self, bcp, pt):
91
+ self._addMoveTo()
92
+ bounds = self.bounds
93
+ bounds = updateBounds(bounds, pt)
94
+ if not pointInRect(bcp, bounds):
95
+ bounds = unionRect(
96
+ bounds, calcQuadraticBounds(self._getCurrentPoint(), bcp, pt)
97
+ )
98
+ self.bounds = bounds
venv/Lib/site-packages/fontTools/pens/cairoPen.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pen to draw to a Cairo graphics library context."""
2
+
3
+ from fontTools.pens.basePen import BasePen
4
+
5
+
6
+ __all__ = ["CairoPen"]
7
+
8
+
9
+ class CairoPen(BasePen):
10
+ """Pen to draw to a Cairo graphics library context."""
11
+
12
+ def __init__(self, glyphSet, context):
13
+ BasePen.__init__(self, glyphSet)
14
+ self.context = context
15
+
16
+ def _moveTo(self, p):
17
+ self.context.move_to(*p)
18
+
19
+ def _lineTo(self, p):
20
+ self.context.line_to(*p)
21
+
22
+ def _curveToOne(self, p1, p2, p3):
23
+ self.context.curve_to(*p1, *p2, *p3)
24
+
25
+ def _closePath(self):
26
+ self.context.close_path()
venv/Lib/site-packages/fontTools/pens/cocoaPen.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.pens.basePen import BasePen
2
+
3
+
4
+ __all__ = ["CocoaPen"]
5
+
6
+
7
+ class CocoaPen(BasePen):
8
+ def __init__(self, glyphSet, path=None):
9
+ BasePen.__init__(self, glyphSet)
10
+ if path is None:
11
+ from AppKit import NSBezierPath
12
+
13
+ path = NSBezierPath.bezierPath()
14
+ self.path = path
15
+
16
+ def _moveTo(self, p):
17
+ self.path.moveToPoint_(p)
18
+
19
+ def _lineTo(self, p):
20
+ self.path.lineToPoint_(p)
21
+
22
+ def _curveToOne(self, p1, p2, p3):
23
+ self.path.curveToPoint_controlPoint1_controlPoint2_(p3, p1, p2)
24
+
25
+ def _closePath(self):
26
+ self.path.closePath()
venv/Lib/site-packages/fontTools/pens/cu2quPen.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2016 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import operator
16
+ from fontTools.cu2qu import curve_to_quadratic, curves_to_quadratic
17
+ from fontTools.pens.basePen import decomposeSuperBezierSegment
18
+ from fontTools.pens.filterPen import FilterPen
19
+ from fontTools.pens.reverseContourPen import ReverseContourPen
20
+ from fontTools.pens.pointPen import BasePointToSegmentPen
21
+ from fontTools.pens.pointPen import ReverseContourPointPen
22
+
23
+
24
+ class Cu2QuPen(FilterPen):
25
+ """A filter pen to convert cubic bezier curves to quadratic b-splines
26
+ using the FontTools SegmentPen protocol.
27
+
28
+ Args:
29
+
30
+ other_pen: another SegmentPen used to draw the transformed outline.
31
+ max_err: maximum approximation error in font units. For optimal results,
32
+ if you know the UPEM of the font, we recommend setting this to a
33
+ value equal, or close to UPEM / 1000.
34
+ reverse_direction: flip the contours' direction but keep starting point.
35
+ stats: a dictionary counting the point numbers of quadratic segments.
36
+ all_quadratic: if True (default), only quadratic b-splines are generated.
37
+ if False, quadratic curves or cubic curves are generated depending
38
+ on which one is more economical.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ other_pen,
44
+ max_err,
45
+ reverse_direction=False,
46
+ stats=None,
47
+ all_quadratic=True,
48
+ ):
49
+ if reverse_direction:
50
+ other_pen = ReverseContourPen(other_pen)
51
+ super().__init__(other_pen)
52
+ self.max_err = max_err
53
+ self.stats = stats
54
+ self.all_quadratic = all_quadratic
55
+
56
+ def _convert_curve(self, pt1, pt2, pt3):
57
+ curve = (self.current_pt, pt1, pt2, pt3)
58
+ result = curve_to_quadratic(curve, self.max_err, self.all_quadratic)
59
+ if self.stats is not None:
60
+ n = str(len(result) - 2)
61
+ self.stats[n] = self.stats.get(n, 0) + 1
62
+ if self.all_quadratic:
63
+ self.qCurveTo(*result[1:])
64
+ else:
65
+ if len(result) == 3:
66
+ self.qCurveTo(*result[1:])
67
+ else:
68
+ assert len(result) == 4
69
+ super().curveTo(*result[1:])
70
+
71
+ def curveTo(self, *points):
72
+ n = len(points)
73
+ if n == 3:
74
+ # this is the most common case, so we special-case it
75
+ self._convert_curve(*points)
76
+ elif n > 3:
77
+ for segment in decomposeSuperBezierSegment(points):
78
+ self._convert_curve(*segment)
79
+ else:
80
+ self.qCurveTo(*points)
81
+
82
+
83
+ class Cu2QuPointPen(BasePointToSegmentPen):
84
+ """A filter pen to convert cubic bezier curves to quadratic b-splines
85
+ using the FontTools PointPen protocol.
86
+
87
+ Args:
88
+ other_point_pen: another PointPen used to draw the transformed outline.
89
+ max_err: maximum approximation error in font units. For optimal results,
90
+ if you know the UPEM of the font, we recommend setting this to a
91
+ value equal, or close to UPEM / 1000.
92
+ reverse_direction: reverse the winding direction of all contours.
93
+ stats: a dictionary counting the point numbers of quadratic segments.
94
+ all_quadratic: if True (default), only quadratic b-splines are generated.
95
+ if False, quadratic curves or cubic curves are generated depending
96
+ on which one is more economical.
97
+ """
98
+
99
+ __points_required = {
100
+ "move": (1, operator.eq),
101
+ "line": (1, operator.eq),
102
+ "qcurve": (2, operator.ge),
103
+ "curve": (3, operator.eq),
104
+ }
105
+
106
+ def __init__(
107
+ self,
108
+ other_point_pen,
109
+ max_err,
110
+ reverse_direction=False,
111
+ stats=None,
112
+ all_quadratic=True,
113
+ ):
114
+ BasePointToSegmentPen.__init__(self)
115
+ if reverse_direction:
116
+ self.pen = ReverseContourPointPen(other_point_pen)
117
+ else:
118
+ self.pen = other_point_pen
119
+ self.max_err = max_err
120
+ self.stats = stats
121
+ self.all_quadratic = all_quadratic
122
+
123
+ def _flushContour(self, segments):
124
+ assert len(segments) >= 1
125
+ closed = segments[0][0] != "move"
126
+ new_segments = []
127
+ prev_points = segments[-1][1]
128
+ prev_on_curve = prev_points[-1][0]
129
+ for segment_type, points in segments:
130
+ if segment_type == "curve":
131
+ for sub_points in self._split_super_bezier_segments(points):
132
+ on_curve, smooth, name, kwargs = sub_points[-1]
133
+ bcp1, bcp2 = sub_points[0][0], sub_points[1][0]
134
+ cubic = [prev_on_curve, bcp1, bcp2, on_curve]
135
+ quad = curve_to_quadratic(cubic, self.max_err, self.all_quadratic)
136
+ if self.stats is not None:
137
+ n = str(len(quad) - 2)
138
+ self.stats[n] = self.stats.get(n, 0) + 1
139
+ new_points = [(pt, False, None, {}) for pt in quad[1:-1]]
140
+ new_points.append((on_curve, smooth, name, kwargs))
141
+ if self.all_quadratic or len(new_points) == 2:
142
+ new_segments.append(["qcurve", new_points])
143
+ else:
144
+ new_segments.append(["curve", new_points])
145
+ prev_on_curve = sub_points[-1][0]
146
+ else:
147
+ new_segments.append([segment_type, points])
148
+ prev_on_curve = points[-1][0]
149
+ if closed:
150
+ # the BasePointToSegmentPen.endPath method that calls _flushContour
151
+ # rotates the point list of closed contours so that they end with
152
+ # the first on-curve point. We restore the original starting point.
153
+ new_segments = new_segments[-1:] + new_segments[:-1]
154
+ self._drawPoints(new_segments)
155
+
156
+ def _split_super_bezier_segments(self, points):
157
+ sub_segments = []
158
+ # n is the number of control points
159
+ n = len(points) - 1
160
+ if n == 2:
161
+ # a simple bezier curve segment
162
+ sub_segments.append(points)
163
+ elif n > 2:
164
+ # a "super" bezier; decompose it
165
+ on_curve, smooth, name, kwargs = points[-1]
166
+ num_sub_segments = n - 1
167
+ for i, sub_points in enumerate(
168
+ decomposeSuperBezierSegment([pt for pt, _, _, _ in points])
169
+ ):
170
+ new_segment = []
171
+ for point in sub_points[:-1]:
172
+ new_segment.append((point, False, None, {}))
173
+ if i == (num_sub_segments - 1):
174
+ # the last on-curve keeps its original attributes
175
+ new_segment.append((on_curve, smooth, name, kwargs))
176
+ else:
177
+ # on-curves of sub-segments are always "smooth"
178
+ new_segment.append((sub_points[-1], True, None, {}))
179
+ sub_segments.append(new_segment)
180
+ else:
181
+ raise AssertionError("expected 2 control points, found: %d" % n)
182
+ return sub_segments
183
+
184
+ def _drawPoints(self, segments):
185
+ pen = self.pen
186
+ pen.beginPath()
187
+ last_offcurves = []
188
+ points_required = self.__points_required
189
+ for i, (segment_type, points) in enumerate(segments):
190
+ if segment_type in points_required:
191
+ n, op = points_required[segment_type]
192
+ assert op(len(points), n), (
193
+ f"illegal {segment_type!r} segment point count: "
194
+ f"expected {n}, got {len(points)}"
195
+ )
196
+ offcurves = points[:-1]
197
+ if i == 0:
198
+ # any off-curve points preceding the first on-curve
199
+ # will be appended at the end of the contour
200
+ last_offcurves = offcurves
201
+ else:
202
+ for pt, smooth, name, kwargs in offcurves:
203
+ pen.addPoint(pt, None, smooth, name, **kwargs)
204
+ pt, smooth, name, kwargs = points[-1]
205
+ if pt is None:
206
+ assert segment_type == "qcurve"
207
+ # special quadratic contour with no on-curve points:
208
+ # we need to skip the "None" point. See also the Pen
209
+ # protocol's qCurveTo() method and fontTools.pens.basePen
210
+ pass
211
+ else:
212
+ pen.addPoint(pt, segment_type, smooth, name, **kwargs)
213
+ else:
214
+ raise AssertionError("unexpected segment type: %r" % segment_type)
215
+ for pt, smooth, name, kwargs in last_offcurves:
216
+ pen.addPoint(pt, None, smooth, name, **kwargs)
217
+ pen.endPath()
218
+
219
+ def addComponent(self, baseGlyphName, transformation):
220
+ assert self.currentPath is None
221
+ self.pen.addComponent(baseGlyphName, transformation)
222
+
223
+
224
+ class Cu2QuMultiPen:
225
+ """A filter multi-pen to convert cubic bezier curves to quadratic b-splines
226
+ in a interpolation-compatible manner, using the FontTools SegmentPen protocol.
227
+
228
+ Args:
229
+
230
+ other_pens: list of SegmentPens used to draw the transformed outlines.
231
+ max_err: maximum approximation error in font units. For optimal results,
232
+ if you know the UPEM of the font, we recommend setting this to a
233
+ value equal, or close to UPEM / 1000.
234
+ reverse_direction: flip the contours' direction but keep starting point.
235
+
236
+ This pen does not follow the normal SegmentPen protocol. Instead, its
237
+ moveTo/lineTo/qCurveTo/curveTo methods take a list of tuples that are
238
+ arguments that would normally be passed to a SegmentPen, one item for
239
+ each of the pens in other_pens.
240
+ """
241
+
242
+ # TODO Simplify like 3e8ebcdce592fe8a59ca4c3a294cc9724351e1ce
243
+ # Remove start_pts and _add_moveTO
244
+
245
+ def __init__(self, other_pens, max_err, reverse_direction=False):
246
+ if reverse_direction:
247
+ other_pens = [
248
+ ReverseContourPen(pen, outputImpliedClosingLine=True)
249
+ for pen in other_pens
250
+ ]
251
+ self.pens = other_pens
252
+ self.max_err = max_err
253
+ self.start_pts = None
254
+ self.current_pts = None
255
+
256
+ def _check_contour_is_open(self):
257
+ if self.current_pts is None:
258
+ raise AssertionError("moveTo is required")
259
+
260
+ def _check_contour_is_closed(self):
261
+ if self.current_pts is not None:
262
+ raise AssertionError("closePath or endPath is required")
263
+
264
+ def _add_moveTo(self):
265
+ if self.start_pts is not None:
266
+ for pt, pen in zip(self.start_pts, self.pens):
267
+ pen.moveTo(*pt)
268
+ self.start_pts = None
269
+
270
+ def moveTo(self, pts):
271
+ self._check_contour_is_closed()
272
+ self.start_pts = self.current_pts = pts
273
+ self._add_moveTo()
274
+
275
+ def lineTo(self, pts):
276
+ self._check_contour_is_open()
277
+ self._add_moveTo()
278
+ for pt, pen in zip(pts, self.pens):
279
+ pen.lineTo(*pt)
280
+ self.current_pts = pts
281
+
282
+ def qCurveTo(self, pointsList):
283
+ self._check_contour_is_open()
284
+ if len(pointsList[0]) == 1:
285
+ self.lineTo([(points[0],) for points in pointsList])
286
+ return
287
+ self._add_moveTo()
288
+ current_pts = []
289
+ for points, pen in zip(pointsList, self.pens):
290
+ pen.qCurveTo(*points)
291
+ current_pts.append((points[-1],))
292
+ self.current_pts = current_pts
293
+
294
+ def _curves_to_quadratic(self, pointsList):
295
+ curves = []
296
+ for current_pt, points in zip(self.current_pts, pointsList):
297
+ curves.append(current_pt + points)
298
+ quadratics = curves_to_quadratic(curves, [self.max_err] * len(curves))
299
+ pointsList = []
300
+ for quadratic in quadratics:
301
+ pointsList.append(quadratic[1:])
302
+ self.qCurveTo(pointsList)
303
+
304
+ def curveTo(self, pointsList):
305
+ self._check_contour_is_open()
306
+ self._curves_to_quadratic(pointsList)
307
+
308
+ def closePath(self):
309
+ self._check_contour_is_open()
310
+ if self.start_pts is None:
311
+ for pen in self.pens:
312
+ pen.closePath()
313
+ self.current_pts = self.start_pts = None
314
+
315
+ def endPath(self):
316
+ self._check_contour_is_open()
317
+ if self.start_pts is None:
318
+ for pen in self.pens:
319
+ pen.endPath()
320
+ self.current_pts = self.start_pts = None
321
+
322
+ def addComponent(self, glyphName, transformations):
323
+ self._check_contour_is_closed()
324
+ for trans, pen in zip(transformations, self.pens):
325
+ pen.addComponent(glyphName, trans)
venv/Lib/site-packages/fontTools/pens/explicitClosingLinePen.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.pens.filterPen import ContourFilterPen
2
+
3
+
4
+ class ExplicitClosingLinePen(ContourFilterPen):
5
+ """A filter pen that adds an explicit lineTo to the first point of each closed
6
+ contour if the end point of the last segment is not already the same as the first point.
7
+ Otherwise, it passes the contour through unchanged.
8
+
9
+ >>> from pprint import pprint
10
+ >>> from fontTools.pens.recordingPen import RecordingPen
11
+ >>> rec = RecordingPen()
12
+ >>> pen = ExplicitClosingLinePen(rec)
13
+ >>> pen.moveTo((0, 0))
14
+ >>> pen.lineTo((100, 0))
15
+ >>> pen.lineTo((100, 100))
16
+ >>> pen.closePath()
17
+ >>> pprint(rec.value)
18
+ [('moveTo', ((0, 0),)),
19
+ ('lineTo', ((100, 0),)),
20
+ ('lineTo', ((100, 100),)),
21
+ ('lineTo', ((0, 0),)),
22
+ ('closePath', ())]
23
+ >>> rec = RecordingPen()
24
+ >>> pen = ExplicitClosingLinePen(rec)
25
+ >>> pen.moveTo((0, 0))
26
+ >>> pen.lineTo((100, 0))
27
+ >>> pen.lineTo((100, 100))
28
+ >>> pen.lineTo((0, 0))
29
+ >>> pen.closePath()
30
+ >>> pprint(rec.value)
31
+ [('moveTo', ((0, 0),)),
32
+ ('lineTo', ((100, 0),)),
33
+ ('lineTo', ((100, 100),)),
34
+ ('lineTo', ((0, 0),)),
35
+ ('closePath', ())]
36
+ >>> rec = RecordingPen()
37
+ >>> pen = ExplicitClosingLinePen(rec)
38
+ >>> pen.moveTo((0, 0))
39
+ >>> pen.curveTo((100, 0), (0, 100), (100, 100))
40
+ >>> pen.closePath()
41
+ >>> pprint(rec.value)
42
+ [('moveTo', ((0, 0),)),
43
+ ('curveTo', ((100, 0), (0, 100), (100, 100))),
44
+ ('lineTo', ((0, 0),)),
45
+ ('closePath', ())]
46
+ >>> rec = RecordingPen()
47
+ >>> pen = ExplicitClosingLinePen(rec)
48
+ >>> pen.moveTo((0, 0))
49
+ >>> pen.curveTo((100, 0), (0, 100), (100, 100))
50
+ >>> pen.lineTo((0, 0))
51
+ >>> pen.closePath()
52
+ >>> pprint(rec.value)
53
+ [('moveTo', ((0, 0),)),
54
+ ('curveTo', ((100, 0), (0, 100), (100, 100))),
55
+ ('lineTo', ((0, 0),)),
56
+ ('closePath', ())]
57
+ >>> rec = RecordingPen()
58
+ >>> pen = ExplicitClosingLinePen(rec)
59
+ >>> pen.moveTo((0, 0))
60
+ >>> pen.curveTo((100, 0), (0, 100), (0, 0))
61
+ >>> pen.closePath()
62
+ >>> pprint(rec.value)
63
+ [('moveTo', ((0, 0),)),
64
+ ('curveTo', ((100, 0), (0, 100), (0, 0))),
65
+ ('closePath', ())]
66
+ >>> rec = RecordingPen()
67
+ >>> pen = ExplicitClosingLinePen(rec)
68
+ >>> pen.moveTo((0, 0))
69
+ >>> pen.closePath()
70
+ >>> pprint(rec.value)
71
+ [('moveTo', ((0, 0),)), ('closePath', ())]
72
+ >>> rec = RecordingPen()
73
+ >>> pen = ExplicitClosingLinePen(rec)
74
+ >>> pen.closePath()
75
+ >>> pprint(rec.value)
76
+ [('closePath', ())]
77
+ >>> rec = RecordingPen()
78
+ >>> pen = ExplicitClosingLinePen(rec)
79
+ >>> pen.moveTo((0, 0))
80
+ >>> pen.lineTo((100, 0))
81
+ >>> pen.lineTo((100, 100))
82
+ >>> pen.endPath()
83
+ >>> pprint(rec.value)
84
+ [('moveTo', ((0, 0),)),
85
+ ('lineTo', ((100, 0),)),
86
+ ('lineTo', ((100, 100),)),
87
+ ('endPath', ())]
88
+ """
89
+
90
+ def filterContour(self, contour):
91
+ if (
92
+ not contour
93
+ or contour[0][0] != "moveTo"
94
+ or contour[-1][0] != "closePath"
95
+ or len(contour) < 3
96
+ ):
97
+ return
98
+ movePt = contour[0][1][0]
99
+ lastSeg = contour[-2][1]
100
+ if lastSeg and movePt != lastSeg[-1]:
101
+ contour[-1:] = [("lineTo", (movePt,)), ("closePath", ())]
venv/Lib/site-packages/fontTools/pens/filterPen.py ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from fontTools.pens.basePen import AbstractPen, DecomposingPen
4
+ from fontTools.pens.pointPen import (
5
+ AbstractPointPen,
6
+ DecomposingPointPen,
7
+ ReverseFlipped,
8
+ )
9
+ from fontTools.pens.recordingPen import RecordingPen
10
+
11
+
12
+ class _PassThruComponentsMixin(object):
13
+ def addComponent(self, glyphName, transformation, **kwargs):
14
+ self._outPen.addComponent(glyphName, transformation, **kwargs)
15
+
16
+
17
+ class FilterPen(_PassThruComponentsMixin, AbstractPen):
18
+ """Base class for pens that apply some transformation to the coordinates
19
+ they receive and pass them to another pen.
20
+
21
+ You can override any of its methods. The default implementation does
22
+ nothing, but passes the commands unmodified to the other pen.
23
+
24
+ >>> from fontTools.pens.recordingPen import RecordingPen
25
+ >>> rec = RecordingPen()
26
+ >>> pen = FilterPen(rec)
27
+ >>> v = iter(rec.value)
28
+
29
+ >>> pen.moveTo((0, 0))
30
+ >>> next(v)
31
+ ('moveTo', ((0, 0),))
32
+
33
+ >>> pen.lineTo((1, 1))
34
+ >>> next(v)
35
+ ('lineTo', ((1, 1),))
36
+
37
+ >>> pen.curveTo((2, 2), (3, 3), (4, 4))
38
+ >>> next(v)
39
+ ('curveTo', ((2, 2), (3, 3), (4, 4)))
40
+
41
+ >>> pen.qCurveTo((5, 5), (6, 6), (7, 7), (8, 8))
42
+ >>> next(v)
43
+ ('qCurveTo', ((5, 5), (6, 6), (7, 7), (8, 8)))
44
+
45
+ >>> pen.closePath()
46
+ >>> next(v)
47
+ ('closePath', ())
48
+
49
+ >>> pen.moveTo((9, 9))
50
+ >>> next(v)
51
+ ('moveTo', ((9, 9),))
52
+
53
+ >>> pen.endPath()
54
+ >>> next(v)
55
+ ('endPath', ())
56
+
57
+ >>> pen.addComponent('foo', (1, 0, 0, 1, 0, 0))
58
+ >>> next(v)
59
+ ('addComponent', ('foo', (1, 0, 0, 1, 0, 0)))
60
+ """
61
+
62
+ def __init__(self, outPen):
63
+ self._outPen = outPen
64
+ self.current_pt = None
65
+
66
+ def moveTo(self, pt):
67
+ self._outPen.moveTo(pt)
68
+ self.current_pt = pt
69
+
70
+ def lineTo(self, pt):
71
+ self._outPen.lineTo(pt)
72
+ self.current_pt = pt
73
+
74
+ def curveTo(self, *points):
75
+ self._outPen.curveTo(*points)
76
+ self.current_pt = points[-1]
77
+
78
+ def qCurveTo(self, *points):
79
+ self._outPen.qCurveTo(*points)
80
+ self.current_pt = points[-1]
81
+
82
+ def closePath(self):
83
+ self._outPen.closePath()
84
+ self.current_pt = None
85
+
86
+ def endPath(self):
87
+ self._outPen.endPath()
88
+ self.current_pt = None
89
+
90
+
91
+ class ContourFilterPen(_PassThruComponentsMixin, RecordingPen):
92
+ """A "buffered" filter pen that accumulates contour data, passes
93
+ it through a ``filterContour`` method when the contour is closed or ended,
94
+ and finally draws the result with the output pen.
95
+
96
+ Components are passed through unchanged.
97
+ """
98
+
99
+ def __init__(self, outPen):
100
+ super(ContourFilterPen, self).__init__()
101
+ self._outPen = outPen
102
+
103
+ def closePath(self):
104
+ super(ContourFilterPen, self).closePath()
105
+ self._flushContour()
106
+
107
+ def endPath(self):
108
+ super(ContourFilterPen, self).endPath()
109
+ self._flushContour()
110
+
111
+ def _flushContour(self):
112
+ result = self.filterContour(self.value)
113
+ if result is not None:
114
+ self.value = result
115
+ self.replay(self._outPen)
116
+ self.value = []
117
+
118
+ def filterContour(self, contour):
119
+ """Subclasses must override this to perform the filtering.
120
+
121
+ The contour is a list of pen (operator, operands) tuples.
122
+ Operators are strings corresponding to the AbstractPen methods:
123
+ "moveTo", "lineTo", "curveTo", "qCurveTo", "closePath" and
124
+ "endPath". The operands are the positional arguments that are
125
+ passed to each method.
126
+
127
+ If the method doesn't return a value (i.e. returns None), it's
128
+ assumed that the argument was modified in-place.
129
+ Otherwise, the return value is drawn with the output pen.
130
+ """
131
+ return # or return contour
132
+
133
+
134
+ class FilterPointPen(_PassThruComponentsMixin, AbstractPointPen):
135
+ """Baseclass for point pens that apply some transformation to the
136
+ coordinates they receive and pass them to another point pen.
137
+
138
+ You can override any of its methods. The default implementation does
139
+ nothing, but passes the commands unmodified to the other pen.
140
+
141
+ >>> from fontTools.pens.recordingPen import RecordingPointPen
142
+ >>> rec = RecordingPointPen()
143
+ >>> pen = FilterPointPen(rec)
144
+ >>> v = iter(rec.value)
145
+ >>> pen.beginPath(identifier="abc")
146
+ >>> next(v)
147
+ ('beginPath', (), {'identifier': 'abc'})
148
+ >>> pen.addPoint((1, 2), "line", False)
149
+ >>> next(v)
150
+ ('addPoint', ((1, 2), 'line', False, None), {})
151
+ >>> pen.addComponent("a", (2, 0, 0, 2, 10, -10), identifier="0001")
152
+ >>> next(v)
153
+ ('addComponent', ('a', (2, 0, 0, 2, 10, -10)), {'identifier': '0001'})
154
+ >>> pen.endPath()
155
+ >>> next(v)
156
+ ('endPath', (), {})
157
+ """
158
+
159
+ def __init__(self, outPen):
160
+ self._outPen = outPen
161
+
162
+ def beginPath(self, identifier=None, **kwargs):
163
+ kwargs = dict(kwargs)
164
+ if identifier is not None:
165
+ kwargs["identifier"] = identifier
166
+ self._outPen.beginPath(**kwargs)
167
+
168
+ def endPath(self):
169
+ self._outPen.endPath()
170
+
171
+ def addPoint(
172
+ self,
173
+ pt,
174
+ segmentType=None,
175
+ smooth=False,
176
+ name=None,
177
+ identifier=None,
178
+ **kwargs,
179
+ ):
180
+ kwargs = dict(kwargs)
181
+ if identifier is not None:
182
+ kwargs["identifier"] = identifier
183
+ self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
184
+
185
+
186
+ class _DecomposingFilterMixinBase:
187
+ """Base mixin class with common `addComponent` logic for decomposing filter pens."""
188
+
189
+ def addComponent(self, baseGlyphName, transformation, **kwargs):
190
+ # only decompose the component if it's included in the set
191
+ if self.include is None or baseGlyphName in self.include:
192
+ # if we're decomposing nested components, temporarily set include to None
193
+ include_bak = self.include
194
+ if self.decomposeNested and self.include:
195
+ self.include = None
196
+ try:
197
+ super().addComponent(baseGlyphName, transformation, **kwargs)
198
+ finally:
199
+ if self.include != include_bak:
200
+ self.include = include_bak
201
+ else:
202
+ _PassThruComponentsMixin.addComponent(
203
+ self, baseGlyphName, transformation, **kwargs
204
+ )
205
+
206
+
207
+ class _DecomposingFilterPenMixin(_DecomposingFilterMixinBase):
208
+ """Mixin class that decomposes components as regular contours for segment pens.
209
+
210
+ Used by DecomposingFilterPen.
211
+
212
+ Takes two required parameters, another segment pen 'outPen' to draw
213
+ with, and a 'glyphSet' dict of drawable glyph objects to draw components from.
214
+
215
+ The 'skipMissingComponents' and 'reverseFlipped' optional arguments work the
216
+ same as in the DecomposingPen. reverseFlipped is bool only (True/False).
217
+
218
+ In addition, the decomposing filter pens also take the following two options:
219
+
220
+ 'include' is an optional set of component base glyph names to consider for
221
+ decomposition; the default include=None means decompose all components no matter
222
+ the base glyph name).
223
+
224
+ 'decomposeNested' (bool) controls whether to recurse decomposition into nested
225
+ components of components (this only matters when 'include' was also provided);
226
+ if False, only decompose top-level components included in the set, but not
227
+ also their children.
228
+ """
229
+
230
+ # raises MissingComponentError if base glyph is not found in glyphSet
231
+ skipMissingComponents = False
232
+
233
+ def __init__(
234
+ self,
235
+ outPen,
236
+ glyphSet,
237
+ skipMissingComponents=None,
238
+ reverseFlipped: bool = False,
239
+ include: set[str] | None = None,
240
+ decomposeNested: bool = True,
241
+ **kwargs,
242
+ ):
243
+ assert isinstance(
244
+ reverseFlipped, bool
245
+ ), f"Expected bool, got {type(reverseFlipped).__name__}"
246
+ super().__init__(
247
+ outPen=outPen,
248
+ glyphSet=glyphSet,
249
+ skipMissingComponents=skipMissingComponents,
250
+ reverseFlipped=reverseFlipped,
251
+ **kwargs,
252
+ )
253
+ self.include = include
254
+ self.decomposeNested = decomposeNested
255
+
256
+
257
+ class _DecomposingFilterPointPenMixin(_DecomposingFilterMixinBase):
258
+ """Mixin class that decomposes components as regular contours for point pens.
259
+
260
+ Takes two required parameters, another point pen 'outPen' to draw
261
+ with, and a 'glyphSet' dict of drawable glyph objects to draw components from.
262
+
263
+ The 'skipMissingComponents' and 'reverseFlipped' optional arguments work the
264
+ same as in the DecomposingPointPen. reverseFlipped accepts bool | ReverseFlipped
265
+ (see DecomposingPointPen).
266
+
267
+ In addition, the decomposing filter pens also take the following two options:
268
+
269
+ 'include' is an optional set of component base glyph names to consider for
270
+ decomposition; the default include=None means decompose all components no matter
271
+ the base glyph name).
272
+
273
+ 'decomposeNested' (bool) controls whether to recurse decomposition into nested
274
+ components of components (this only matters when 'include' was also provided);
275
+ if False, only decompose top-level components included in the set, but not
276
+ also their children.
277
+ """
278
+
279
+ # raises MissingComponentError if base glyph is not found in glyphSet
280
+ skipMissingComponents = False
281
+
282
+ def __init__(
283
+ self,
284
+ outPen,
285
+ glyphSet,
286
+ skipMissingComponents=None,
287
+ reverseFlipped: bool | ReverseFlipped = False,
288
+ include: set[str] | None = None,
289
+ decomposeNested: bool = True,
290
+ **kwargs,
291
+ ):
292
+ super().__init__(
293
+ outPen=outPen,
294
+ glyphSet=glyphSet,
295
+ skipMissingComponents=skipMissingComponents,
296
+ reverseFlipped=reverseFlipped,
297
+ **kwargs,
298
+ )
299
+ self.include = include
300
+ self.decomposeNested = decomposeNested
301
+
302
+
303
+ class DecomposingFilterPen(_DecomposingFilterPenMixin, DecomposingPen, FilterPen):
304
+ """Filter pen that draws components as regular contours."""
305
+
306
+ pass
307
+
308
+
309
+ class DecomposingFilterPointPen(
310
+ _DecomposingFilterPointPenMixin, DecomposingPointPen, FilterPointPen
311
+ ):
312
+ """Filter point pen that draws components as regular contours."""
313
+
314
+ pass
315
+
316
+
317
+ class ContourFilterPointPen(_PassThruComponentsMixin, AbstractPointPen):
318
+ """A "buffered" filter point pen that accumulates contour data, passes
319
+ it through a ``filterContour`` method when the contour is closed or ended,
320
+ and finally draws the result with the output point pen.
321
+
322
+ Components are passed through unchanged.
323
+
324
+ The ``filterContour`` method can modify the contour in-place (return None)
325
+ or return a new contour to replace it.
326
+ """
327
+
328
+ def __init__(self, outPen):
329
+ self._outPen = outPen
330
+ self.currentContour = None
331
+ self.currentContourKwargs = None
332
+
333
+ def beginPath(self, identifier=None, **kwargs):
334
+ if self.currentContour is not None:
335
+ raise ValueError("Path already begun")
336
+ kwargs = dict(kwargs)
337
+ if identifier is not None:
338
+ kwargs["identifier"] = identifier
339
+ self.currentContour = []
340
+ self.currentContourKwargs = kwargs
341
+
342
+ def endPath(self):
343
+ if self.currentContour is None:
344
+ raise ValueError("Path not begun")
345
+ self._flushContour()
346
+ self.currentContour = None
347
+ self.currentContourKwargs = None
348
+
349
+ def _flushContour(self):
350
+ """Flush the current contour to the output pen."""
351
+ result = self.filterContour(self.currentContour)
352
+ if result is not None:
353
+ self.currentContour = result
354
+
355
+ # Draw the filtered contour
356
+ self._outPen.beginPath(**self.currentContourKwargs)
357
+ for pt, segmentType, smooth, name, kwargs in self.currentContour:
358
+ self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
359
+ self._outPen.endPath()
360
+
361
+ def filterContour(self, contour):
362
+ """Subclasses must override this to perform the filtering.
363
+
364
+ The contour is a list of (pt, segmentType, smooth, name, kwargs) tuples.
365
+ If the method doesn't return a value (i.e. returns None), it's
366
+ assumed that the contour was modified in-place.
367
+ Otherwise, the return value replaces the original contour.
368
+ """
369
+ return # or return contour
370
+
371
+ def addPoint(
372
+ self,
373
+ pt,
374
+ segmentType=None,
375
+ smooth=False,
376
+ name=None,
377
+ identifier=None,
378
+ **kwargs,
379
+ ):
380
+ if self.currentContour is None:
381
+ raise ValueError("Path not begun")
382
+ kwargs = dict(kwargs)
383
+ if identifier is not None:
384
+ kwargs["identifier"] = identifier
385
+ self.currentContour.append((pt, segmentType, smooth, name, kwargs))
386
+
387
+
388
+ class OnCurveFirstPointPen(ContourFilterPointPen):
389
+ """Filter point pen that ensures closed contours start with an on-curve point.
390
+
391
+ If a closed contour starts with an off-curve point (segmentType=None), it rotates
392
+ the points list so that the first on-curve point (segmentType != None) becomes
393
+ the start point. Open contours and contours already starting with on-curve points
394
+ are passed through unchanged.
395
+
396
+ >>> from fontTools.pens.recordingPen import RecordingPointPen
397
+ >>> rec = RecordingPointPen()
398
+ >>> pen = OnCurveFirstPointPen(rec)
399
+ >>> # Closed contour starting with off-curve - will be rotated
400
+ >>> pen.beginPath()
401
+ >>> pen.addPoint((0, 0), None) # off-curve
402
+ >>> pen.addPoint((100, 100), "line") # on-curve - will become start
403
+ >>> pen.addPoint((200, 0), None) # off-curve
404
+ >>> pen.addPoint((300, 100), "curve") # on-curve
405
+ >>> pen.endPath()
406
+ >>> # The contour should now start with (100, 100) "line"
407
+ >>> rec.value[0]
408
+ ('beginPath', (), {})
409
+ >>> rec.value[1]
410
+ ('addPoint', ((100, 100), 'line', False, None), {})
411
+ >>> rec.value[2]
412
+ ('addPoint', ((200, 0), None, False, None), {})
413
+ >>> rec.value[3]
414
+ ('addPoint', ((300, 100), 'curve', False, None), {})
415
+ >>> rec.value[4]
416
+ ('addPoint', ((0, 0), None, False, None), {})
417
+ """
418
+
419
+ def filterContour(self, contour):
420
+ """Rotate closed contour to start with first on-curve point if needed."""
421
+ if not contour:
422
+ return
423
+
424
+ # Check if it's a closed contour (no "move" segmentType)
425
+ is_closed = contour[0][1] != "move"
426
+
427
+ if is_closed and contour[0][1] is None:
428
+ # Closed contour starting with off-curve - need to rotate
429
+ # Find the first on-curve point
430
+ for i, (pt, segmentType, smooth, name, kwargs) in enumerate(contour):
431
+ if segmentType is not None:
432
+ # Rotate the points list so it starts with the first on-curve point
433
+ return contour[i:] + contour[:i]
venv/Lib/site-packages/fontTools/pens/freetypePen.py ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Pen to rasterize paths with FreeType."""
4
+
5
+ __all__ = ["FreeTypePen"]
6
+
7
+ import os
8
+ import ctypes
9
+ import platform
10
+ import subprocess
11
+ import collections
12
+ import math
13
+
14
+ import freetype
15
+ from freetype.raw import FT_Outline_Get_Bitmap, FT_Outline_Get_BBox, FT_Outline_Get_CBox
16
+ from freetype.ft_types import FT_Pos
17
+ from freetype.ft_structs import FT_Vector, FT_BBox, FT_Bitmap, FT_Outline
18
+ from freetype.ft_enums import (
19
+ FT_OUTLINE_NONE,
20
+ FT_OUTLINE_EVEN_ODD_FILL,
21
+ FT_PIXEL_MODE_GRAY,
22
+ FT_CURVE_TAG_ON,
23
+ FT_CURVE_TAG_CONIC,
24
+ FT_CURVE_TAG_CUBIC,
25
+ )
26
+ from freetype.ft_errors import FT_Exception
27
+
28
+ from fontTools.pens.basePen import BasePen, PenError
29
+ from fontTools.misc.roundTools import otRound
30
+ from fontTools.misc.transform import Transform
31
+
32
+ Contour = collections.namedtuple("Contour", ("points", "tags"))
33
+
34
+
35
+ class FreeTypePen(BasePen):
36
+ """Pen to rasterize paths with FreeType. Requires `freetype-py` module.
37
+
38
+ Constructs ``FT_Outline`` from the paths, and renders it within a bitmap
39
+ buffer.
40
+
41
+ For ``array()`` and ``show()``, `numpy` and `matplotlib` must be installed.
42
+ For ``image()``, `Pillow` is required. Each module is lazily loaded when the
43
+ corresponding method is called.
44
+
45
+ Args:
46
+ glyphSet: a dictionary of drawable glyph objects keyed by name
47
+ used to resolve component references in composite glyphs.
48
+
49
+ Examples:
50
+ If `numpy` and `matplotlib` is available, the following code will
51
+ show the glyph image of `fi` in a new window::
52
+
53
+ from fontTools.ttLib import TTFont
54
+ from fontTools.pens.freetypePen import FreeTypePen
55
+ from fontTools.misc.transform import Offset
56
+ pen = FreeTypePen(None)
57
+ font = TTFont('SourceSansPro-Regular.otf')
58
+ glyph = font.getGlyphSet()['fi']
59
+ glyph.draw(pen)
60
+ width, ascender, descender = glyph.width, font['OS/2'].usWinAscent, -font['OS/2'].usWinDescent
61
+ height = ascender - descender
62
+ pen.show(width=width, height=height, transform=Offset(0, -descender))
63
+
64
+ Combining with `uharfbuzz`, you can typeset a chunk of glyphs in a pen::
65
+
66
+ import uharfbuzz as hb
67
+ from fontTools.pens.freetypePen import FreeTypePen
68
+ from fontTools.pens.transformPen import TransformPen
69
+ from fontTools.misc.transform import Offset
70
+
71
+ en1, en2, ar, ja = 'Typesetting', 'Jeff', 'صف الحروف', 'たいぷせっと'
72
+ for text, font_path, direction, typo_ascender, typo_descender, vhea_ascender, vhea_descender, contain, features in (
73
+ (en1, 'NotoSans-Regular.ttf', 'ltr', 2189, -600, None, None, False, {"kern": True, "liga": True}),
74
+ (en2, 'NotoSans-Regular.ttf', 'ltr', 2189, -600, None, None, True, {"kern": True, "liga": True}),
75
+ (ar, 'NotoSansArabic-Regular.ttf', 'rtl', 1374, -738, None, None, False, {"kern": True, "liga": True}),
76
+ (ja, 'NotoSansJP-Regular.otf', 'ltr', 880, -120, 500, -500, False, {"palt": True, "kern": True}),
77
+ (ja, 'NotoSansJP-Regular.otf', 'ttb', 880, -120, 500, -500, False, {"vert": True, "vpal": True, "vkrn": True})
78
+ ):
79
+ blob = hb.Blob.from_file_path(font_path)
80
+ face = hb.Face(blob)
81
+ font = hb.Font(face)
82
+ buf = hb.Buffer()
83
+ buf.direction = direction
84
+ buf.add_str(text)
85
+ buf.guess_segment_properties()
86
+ hb.shape(font, buf, features)
87
+
88
+ x, y = 0, 0
89
+ pen = FreeTypePen(None)
90
+ for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
91
+ gid = info.codepoint
92
+ transformed = TransformPen(pen, Offset(x + pos.x_offset, y + pos.y_offset))
93
+ font.draw_glyph_with_pen(gid, transformed)
94
+ x += pos.x_advance
95
+ y += pos.y_advance
96
+
97
+ offset, width, height = None, None, None
98
+ if direction in ('ltr', 'rtl'):
99
+ offset = (0, -typo_descender)
100
+ width = x
101
+ height = typo_ascender - typo_descender
102
+ else:
103
+ offset = (-vhea_descender, -y)
104
+ width = vhea_ascender - vhea_descender
105
+ height = -y
106
+ pen.show(width=width, height=height, transform=Offset(*offset), contain=contain)
107
+
108
+ For Jupyter Notebook, the rendered image will be displayed in a cell if
109
+ you replace ``show()`` with ``image()`` in the examples.
110
+ """
111
+
112
+ def __init__(self, glyphSet):
113
+ BasePen.__init__(self, glyphSet)
114
+ self.contours = []
115
+
116
+ def outline(self, transform=None, evenOdd=False):
117
+ """Converts the current contours to ``FT_Outline``.
118
+
119
+ Args:
120
+ transform: An optional 6-tuple containing an affine transformation,
121
+ or a ``Transform`` object from the ``fontTools.misc.transform``
122
+ module.
123
+ evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
124
+ """
125
+ transform = transform or Transform()
126
+ if not hasattr(transform, "transformPoint"):
127
+ transform = Transform(*transform)
128
+ n_contours = len(self.contours)
129
+ n_points = sum((len(contour.points) for contour in self.contours))
130
+ points = []
131
+ for contour in self.contours:
132
+ for point in contour.points:
133
+ point = transform.transformPoint(point)
134
+ points.append(
135
+ FT_Vector(
136
+ FT_Pos(otRound(point[0] * 64)), FT_Pos(otRound(point[1] * 64))
137
+ )
138
+ )
139
+ tags = []
140
+ for contour in self.contours:
141
+ for tag in contour.tags:
142
+ tags.append(tag)
143
+ contours = []
144
+ contours_sum = 0
145
+ for contour in self.contours:
146
+ contours_sum += len(contour.points)
147
+ contours.append(contours_sum - 1)
148
+ flags = FT_OUTLINE_EVEN_ODD_FILL if evenOdd else FT_OUTLINE_NONE
149
+ return FT_Outline(
150
+ (ctypes.c_short)(n_contours),
151
+ (ctypes.c_short)(n_points),
152
+ (FT_Vector * n_points)(*points),
153
+ (ctypes.c_ubyte * n_points)(*tags),
154
+ (ctypes.c_short * n_contours)(*contours),
155
+ (ctypes.c_int)(flags),
156
+ )
157
+
158
+ def buffer(
159
+ self, width=None, height=None, transform=None, contain=False, evenOdd=False
160
+ ):
161
+ """Renders the current contours within a bitmap buffer.
162
+
163
+ Args:
164
+ width: Image width of the bitmap in pixels. If omitted, it
165
+ automatically fits to the bounding box of the contours.
166
+ height: Image height of the bitmap in pixels. If omitted, it
167
+ automatically fits to the bounding box of the contours.
168
+ transform: An optional 6-tuple containing an affine transformation,
169
+ or a ``Transform`` object from the ``fontTools.misc.transform``
170
+ module. The bitmap size is not affected by this matrix.
171
+ contain: If ``True``, the image size will be automatically expanded
172
+ so that it fits to the bounding box of the paths. Useful for
173
+ rendering glyphs with negative sidebearings without clipping.
174
+ evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
175
+
176
+ Returns:
177
+ A tuple of ``(buffer, size)``, where ``buffer`` is a ``bytes``
178
+ object of the resulted bitmap and ``size`` is a 2-tuple of its
179
+ dimension.
180
+
181
+ Notes:
182
+ The image size should always be given explicitly if you need to get
183
+ a proper glyph image. When ``width`` and ``height`` are omitted, it
184
+ forcifully fits to the bounding box and the side bearings get
185
+ cropped. If you pass ``0`` to both ``width`` and ``height`` and set
186
+ ``contain`` to ``True``, it expands to the bounding box while
187
+ maintaining the origin of the contours, meaning that LSB will be
188
+ maintained but RSB won’t. The difference between the two becomes
189
+ more obvious when rotate or skew transformation is applied.
190
+
191
+ Example:
192
+ .. code-block:: pycon
193
+
194
+ >>>
195
+ >> pen = FreeTypePen(None)
196
+ >> glyph.draw(pen)
197
+ >> buf, size = pen.buffer(width=500, height=1000)
198
+ >> type(buf), len(buf), size
199
+ (<class 'bytes'>, 500000, (500, 1000))
200
+ """
201
+ transform = transform or Transform()
202
+ if not hasattr(transform, "transformPoint"):
203
+ transform = Transform(*transform)
204
+ contain_x, contain_y = contain or width is None, contain or height is None
205
+ if contain_x or contain_y:
206
+ dx, dy = transform.dx, transform.dy
207
+ bbox = self.bbox
208
+ p1, p2, p3, p4 = (
209
+ transform.transformPoint((bbox[0], bbox[1])),
210
+ transform.transformPoint((bbox[2], bbox[1])),
211
+ transform.transformPoint((bbox[0], bbox[3])),
212
+ transform.transformPoint((bbox[2], bbox[3])),
213
+ )
214
+ px, py = (p1[0], p2[0], p3[0], p4[0]), (p1[1], p2[1], p3[1], p4[1])
215
+ if contain_x:
216
+ if width is None:
217
+ dx = dx - min(*px)
218
+ width = max(*px) - min(*px)
219
+ else:
220
+ dx = dx - min(min(*px), 0.0)
221
+ width = max(width, max(*px) - min(min(*px), 0.0))
222
+ if contain_y:
223
+ if height is None:
224
+ dy = dy - min(*py)
225
+ height = max(*py) - min(*py)
226
+ else:
227
+ dy = dy - min(min(*py), 0.0)
228
+ height = max(height, max(*py) - min(min(*py), 0.0))
229
+ transform = Transform(*transform[:4], dx, dy)
230
+ width, height = math.ceil(width), math.ceil(height)
231
+ buf = ctypes.create_string_buffer(width * height)
232
+ bitmap = FT_Bitmap(
233
+ (ctypes.c_int)(height),
234
+ (ctypes.c_int)(width),
235
+ (ctypes.c_int)(width),
236
+ (ctypes.POINTER(ctypes.c_ubyte))(buf),
237
+ (ctypes.c_short)(256),
238
+ (ctypes.c_ubyte)(FT_PIXEL_MODE_GRAY),
239
+ (ctypes.c_char)(0),
240
+ (ctypes.c_void_p)(None),
241
+ )
242
+ outline = self.outline(transform=transform, evenOdd=evenOdd)
243
+ err = FT_Outline_Get_Bitmap(
244
+ freetype.get_handle(), ctypes.byref(outline), ctypes.byref(bitmap)
245
+ )
246
+ if err != 0:
247
+ raise FT_Exception(err)
248
+ return buf.raw, (width, height)
249
+
250
+ def array(
251
+ self, width=None, height=None, transform=None, contain=False, evenOdd=False
252
+ ):
253
+ """Returns the rendered contours as a numpy array. Requires `numpy`.
254
+
255
+ Args:
256
+ width: Image width of the bitmap in pixels. If omitted, it
257
+ automatically fits to the bounding box of the contours.
258
+ height: Image height of the bitmap in pixels. If omitted, it
259
+ automatically fits to the bounding box of the contours.
260
+ transform: An optional 6-tuple containing an affine transformation,
261
+ or a ``Transform`` object from the ``fontTools.misc.transform``
262
+ module. The bitmap size is not affected by this matrix.
263
+ contain: If ``True``, the image size will be automatically expanded
264
+ so that it fits to the bounding box of the paths. Useful for
265
+ rendering glyphs with negative sidebearings without clipping.
266
+ evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
267
+
268
+ Returns:
269
+ A ``numpy.ndarray`` object with a shape of ``(height, width)``.
270
+ Each element takes a value in the range of ``[0.0, 1.0]``.
271
+
272
+ Notes:
273
+ The image size should always be given explicitly if you need to get
274
+ a proper glyph image. When ``width`` and ``height`` are omitted, it
275
+ forcifully fits to the bounding box and the side bearings get
276
+ cropped. If you pass ``0`` to both ``width`` and ``height`` and set
277
+ ``contain`` to ``True``, it expands to the bounding box while
278
+ maintaining the origin of the contours, meaning that LSB will be
279
+ maintained but RSB won’t. The difference between the two becomes
280
+ more obvious when rotate or skew transformation is applied.
281
+
282
+ Example:
283
+ .. code-block:: pycon
284
+
285
+ >>>
286
+ >> pen = FreeTypePen(None)
287
+ >> glyph.draw(pen)
288
+ >> arr = pen.array(width=500, height=1000)
289
+ >> type(a), a.shape
290
+ (<class 'numpy.ndarray'>, (1000, 500))
291
+ """
292
+
293
+ import numpy as np
294
+
295
+ buf, size = self.buffer(
296
+ width=width,
297
+ height=height,
298
+ transform=transform,
299
+ contain=contain,
300
+ evenOdd=evenOdd,
301
+ )
302
+ return np.frombuffer(buf, "B").reshape((size[1], size[0])) / 255.0
303
+
304
+ def show(
305
+ self, width=None, height=None, transform=None, contain=False, evenOdd=False
306
+ ):
307
+ """Plots the rendered contours with `pyplot`. Requires `numpy` and
308
+ `matplotlib`.
309
+
310
+ Args:
311
+ width: Image width of the bitmap in pixels. If omitted, it
312
+ automatically fits to the bounding box of the contours.
313
+ height: Image height of the bitmap in pixels. If omitted, it
314
+ automatically fits to the bounding box of the contours.
315
+ transform: An optional 6-tuple containing an affine transformation,
316
+ or a ``Transform`` object from the ``fontTools.misc.transform``
317
+ module. The bitmap size is not affected by this matrix.
318
+ contain: If ``True``, the image size will be automatically expanded
319
+ so that it fits to the bounding box of the paths. Useful for
320
+ rendering glyphs with negative sidebearings without clipping.
321
+ evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
322
+
323
+ Notes:
324
+ The image size should always be given explicitly if you need to get
325
+ a proper glyph image. When ``width`` and ``height`` are omitted, it
326
+ forcifully fits to the bounding box and the side bearings get
327
+ cropped. If you pass ``0`` to both ``width`` and ``height`` and set
328
+ ``contain`` to ``True``, it expands to the bounding box while
329
+ maintaining the origin of the contours, meaning that LSB will be
330
+ maintained but RSB won’t. The difference between the two becomes
331
+ more obvious when rotate or skew transformation is applied.
332
+
333
+ Example:
334
+ .. code-block:: pycon
335
+
336
+ >>>
337
+ >> pen = FreeTypePen(None)
338
+ >> glyph.draw(pen)
339
+ >> pen.show(width=500, height=1000)
340
+ """
341
+ from matplotlib import pyplot as plt
342
+
343
+ a = self.array(
344
+ width=width,
345
+ height=height,
346
+ transform=transform,
347
+ contain=contain,
348
+ evenOdd=evenOdd,
349
+ )
350
+ plt.imshow(a, cmap="gray_r", vmin=0, vmax=1)
351
+ plt.show()
352
+
353
+ def image(
354
+ self, width=None, height=None, transform=None, contain=False, evenOdd=False
355
+ ):
356
+ """Returns the rendered contours as a PIL image. Requires `Pillow`.
357
+ Can be used to display a glyph image in Jupyter Notebook.
358
+
359
+ Args:
360
+ width: Image width of the bitmap in pixels. If omitted, it
361
+ automatically fits to the bounding box of the contours.
362
+ height: Image height of the bitmap in pixels. If omitted, it
363
+ automatically fits to the bounding box of the contours.
364
+ transform: An optional 6-tuple containing an affine transformation,
365
+ or a ``Transform`` object from the ``fontTools.misc.transform``
366
+ module. The bitmap size is not affected by this matrix.
367
+ contain: If ``True``, the image size will be automatically expanded
368
+ so that it fits to the bounding box of the paths. Useful for
369
+ rendering glyphs with negative sidebearings without clipping.
370
+ evenOdd: Pass ``True`` for even-odd fill instead of non-zero.
371
+
372
+ Returns:
373
+ A ``PIL.image`` object. The image is filled in black with alpha
374
+ channel obtained from the rendered bitmap.
375
+
376
+ Notes:
377
+ The image size should always be given explicitly if you need to get
378
+ a proper glyph image. When ``width`` and ``height`` are omitted, it
379
+ forcifully fits to the bounding box and the side bearings get
380
+ cropped. If you pass ``0`` to both ``width`` and ``height`` and set
381
+ ``contain`` to ``True``, it expands to the bounding box while
382
+ maintaining the origin of the contours, meaning that LSB will be
383
+ maintained but RSB won’t. The difference between the two becomes
384
+ more obvious when rotate or skew transformation is applied.
385
+
386
+ Example:
387
+ .. code-block:: pycon
388
+
389
+ >>>
390
+ >> pen = FreeTypePen(None)
391
+ >> glyph.draw(pen)
392
+ >> img = pen.image(width=500, height=1000)
393
+ >> type(img), img.size
394
+ (<class 'PIL.Image.Image'>, (500, 1000))
395
+ """
396
+ from PIL import Image
397
+
398
+ buf, size = self.buffer(
399
+ width=width,
400
+ height=height,
401
+ transform=transform,
402
+ contain=contain,
403
+ evenOdd=evenOdd,
404
+ )
405
+ img = Image.new("L", size, 0)
406
+ img.putalpha(Image.frombuffer("L", size, buf))
407
+ return img
408
+
409
+ @property
410
+ def bbox(self):
411
+ """Computes the exact bounding box of an outline.
412
+
413
+ Returns:
414
+ A tuple of ``(xMin, yMin, xMax, yMax)``.
415
+ """
416
+ bbox = FT_BBox()
417
+ outline = self.outline()
418
+ FT_Outline_Get_BBox(ctypes.byref(outline), ctypes.byref(bbox))
419
+ return (bbox.xMin / 64.0, bbox.yMin / 64.0, bbox.xMax / 64.0, bbox.yMax / 64.0)
420
+
421
+ @property
422
+ def cbox(self):
423
+ """Returns an outline's ‘control box’.
424
+
425
+ Returns:
426
+ A tuple of ``(xMin, yMin, xMax, yMax)``.
427
+ """
428
+ cbox = FT_BBox()
429
+ outline = self.outline()
430
+ FT_Outline_Get_CBox(ctypes.byref(outline), ctypes.byref(cbox))
431
+ return (cbox.xMin / 64.0, cbox.yMin / 64.0, cbox.xMax / 64.0, cbox.yMax / 64.0)
432
+
433
+ def _moveTo(self, pt):
434
+ contour = Contour([], [])
435
+ self.contours.append(contour)
436
+ contour.points.append(pt)
437
+ contour.tags.append(FT_CURVE_TAG_ON)
438
+
439
+ def _lineTo(self, pt):
440
+ if not (self.contours and len(self.contours[-1].points) > 0):
441
+ raise PenError("Contour missing required initial moveTo")
442
+ contour = self.contours[-1]
443
+ contour.points.append(pt)
444
+ contour.tags.append(FT_CURVE_TAG_ON)
445
+
446
+ def _curveToOne(self, p1, p2, p3):
447
+ if not (self.contours and len(self.contours[-1].points) > 0):
448
+ raise PenError("Contour missing required initial moveTo")
449
+ t1, t2, t3 = FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_CUBIC, FT_CURVE_TAG_ON
450
+ contour = self.contours[-1]
451
+ for p, t in ((p1, t1), (p2, t2), (p3, t3)):
452
+ contour.points.append(p)
453
+ contour.tags.append(t)
454
+
455
+ def _qCurveToOne(self, p1, p2):
456
+ if not (self.contours and len(self.contours[-1].points) > 0):
457
+ raise PenError("Contour missing required initial moveTo")
458
+ t1, t2 = FT_CURVE_TAG_CONIC, FT_CURVE_TAG_ON
459
+ contour = self.contours[-1]
460
+ for p, t in ((p1, t1), (p2, t2)):
461
+ contour.points.append(p)
462
+ contour.tags.append(t)
venv/Lib/site-packages/fontTools/pens/hashPointPen.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Modified from https://github.com/adobe-type-tools/psautohint/blob/08b346865710ed3c172f1eb581d6ef243b203f99/python/psautohint/ufoFont.py#L800-L838
2
+ import hashlib
3
+
4
+ from fontTools.pens.basePen import MissingComponentError
5
+ from fontTools.pens.pointPen import AbstractPointPen
6
+
7
+
8
+ class HashPointPen(AbstractPointPen):
9
+ """
10
+ This pen can be used to check if a glyph's contents (outlines plus
11
+ components) have changed.
12
+
13
+ Components are added as the original outline plus each composite's
14
+ transformation.
15
+
16
+ Example: You have some TrueType hinting code for a glyph which you want to
17
+ compile. The hinting code specifies a hash value computed with HashPointPen
18
+ that was valid for the glyph's outlines at the time the hinting code was
19
+ written. Now you can calculate the hash for the glyph's current outlines to
20
+ check if the outlines have changed, which would probably make the hinting
21
+ code invalid.
22
+
23
+ > glyph = ufo[name]
24
+ > hash_pen = HashPointPen(glyph.width, ufo)
25
+ > glyph.drawPoints(hash_pen)
26
+ > ttdata = glyph.lib.get("public.truetype.instructions", None)
27
+ > stored_hash = ttdata.get("id", None) # The hash is stored in the "id" key
28
+ > if stored_hash is None or stored_hash != hash_pen.hash:
29
+ > logger.error(f"Glyph hash mismatch, glyph '{name}' will have no instructions in font.")
30
+ > else:
31
+ > # The hash values are identical, the outline has not changed.
32
+ > # Compile the hinting code ...
33
+ > pass
34
+
35
+ If you want to compare a glyph from a source format which supports floating point
36
+ coordinates and transformations against a glyph from a format which has restrictions
37
+ on the precision of floats, e.g. UFO vs. TTF, you must use an appropriate rounding
38
+ function to make the values comparable. For TTF fonts with composites, this
39
+ construct can be used to make the transform values conform to F2Dot14:
40
+
41
+ > ttf_hash_pen = HashPointPen(ttf_glyph_width, ttFont.getGlyphSet())
42
+ > ttf_round_pen = RoundingPointPen(ttf_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14))
43
+ > ufo_hash_pen = HashPointPen(ufo_glyph.width, ufo)
44
+ > ttf_glyph.drawPoints(ttf_round_pen, ttFont["glyf"])
45
+ > ufo_round_pen = RoundingPointPen(ufo_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14))
46
+ > ufo_glyph.drawPoints(ufo_round_pen)
47
+ > assert ttf_hash_pen.hash == ufo_hash_pen.hash
48
+ """
49
+
50
+ def __init__(self, glyphWidth=0, glyphSet=None):
51
+ self.glyphset = glyphSet
52
+ self.data = ["w%s" % round(glyphWidth, 9)]
53
+
54
+ @property
55
+ def hash(self):
56
+ data = "".join(self.data)
57
+ if len(data) >= 128:
58
+ data = hashlib.sha512(data.encode("ascii")).hexdigest()
59
+ return data
60
+
61
+ def beginPath(self, identifier=None, **kwargs):
62
+ pass
63
+
64
+ def endPath(self):
65
+ self.data.append("|")
66
+
67
+ def addPoint(
68
+ self,
69
+ pt,
70
+ segmentType=None,
71
+ smooth=False,
72
+ name=None,
73
+ identifier=None,
74
+ **kwargs,
75
+ ):
76
+ if segmentType is None:
77
+ pt_type = "o" # offcurve
78
+ else:
79
+ pt_type = segmentType[0]
80
+ self.data.append(f"{pt_type}{pt[0]:g}{pt[1]:+g}")
81
+
82
+ def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
83
+ tr = "".join([f"{t:+}" for t in transformation])
84
+ self.data.append("[")
85
+ try:
86
+ self.glyphset[baseGlyphName].drawPoints(self)
87
+ except KeyError:
88
+ raise MissingComponentError(baseGlyphName)
89
+ self.data.append(f"({tr})]")
venv/Lib/site-packages/fontTools/pens/momentsPen.c ADDED
The diff for this file is too large to render. See raw diff
 
venv/Lib/site-packages/fontTools/pens/momentsPen.cp311-win_amd64.pyd ADDED
Binary file (80.9 kB). View file
 
venv/Lib/site-packages/fontTools/pens/momentsPen.py ADDED
@@ -0,0 +1,879 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.pens.basePen import BasePen, OpenContourError
2
+
3
+ try:
4
+ import cython
5
+ except (AttributeError, ImportError):
6
+ # if cython not installed, use mock module with no-op decorators and types
7
+ from fontTools.misc import cython
8
+ COMPILED = cython.compiled
9
+
10
+
11
+ __all__ = ["MomentsPen"]
12
+
13
+
14
+ class MomentsPen(BasePen):
15
+
16
+ def __init__(self, glyphset=None):
17
+ BasePen.__init__(self, glyphset)
18
+
19
+ self.area = 0
20
+ self.momentX = 0
21
+ self.momentY = 0
22
+ self.momentXX = 0
23
+ self.momentXY = 0
24
+ self.momentYY = 0
25
+
26
+ def _moveTo(self, p0):
27
+ self._startPoint = p0
28
+
29
+ def _closePath(self):
30
+ p0 = self._getCurrentPoint()
31
+ if p0 != self._startPoint:
32
+ self._lineTo(self._startPoint)
33
+
34
+ def _endPath(self):
35
+ p0 = self._getCurrentPoint()
36
+ if p0 != self._startPoint:
37
+ raise OpenContourError("Glyph statistics is not defined on open contours.")
38
+
39
+ @cython.locals(r0=cython.double)
40
+ @cython.locals(r1=cython.double)
41
+ @cython.locals(r2=cython.double)
42
+ @cython.locals(r3=cython.double)
43
+ @cython.locals(r4=cython.double)
44
+ @cython.locals(r5=cython.double)
45
+ @cython.locals(r6=cython.double)
46
+ @cython.locals(r7=cython.double)
47
+ @cython.locals(r8=cython.double)
48
+ @cython.locals(r9=cython.double)
49
+ @cython.locals(r10=cython.double)
50
+ @cython.locals(r11=cython.double)
51
+ @cython.locals(r12=cython.double)
52
+ @cython.locals(x0=cython.double, y0=cython.double)
53
+ @cython.locals(x1=cython.double, y1=cython.double)
54
+ def _lineTo(self, p1):
55
+ x0, y0 = self._getCurrentPoint()
56
+ x1, y1 = p1
57
+
58
+ r0 = x1 * y0
59
+ r1 = x1 * y1
60
+ r2 = x1**2
61
+ r3 = r2 * y1
62
+ r4 = y0 - y1
63
+ r5 = r4 * x0
64
+ r6 = x0**2
65
+ r7 = 2 * y0
66
+ r8 = y0**2
67
+ r9 = y1**2
68
+ r10 = x1**3
69
+ r11 = y0**3
70
+ r12 = y1**3
71
+
72
+ self.area += -r0 / 2 - r1 / 2 + x0 * (y0 + y1) / 2
73
+ self.momentX += -r2 * y0 / 6 - r3 / 3 - r5 * x1 / 6 + r6 * (r7 + y1) / 6
74
+ self.momentY += (
75
+ -r0 * y1 / 6 - r8 * x1 / 6 - r9 * x1 / 6 + x0 * (r8 + r9 + y0 * y1) / 6
76
+ )
77
+ self.momentXX += (
78
+ -r10 * y0 / 12
79
+ - r10 * y1 / 4
80
+ - r2 * r5 / 12
81
+ - r4 * r6 * x1 / 12
82
+ + x0**3 * (3 * y0 + y1) / 12
83
+ )
84
+ self.momentXY += (
85
+ -r2 * r8 / 24
86
+ - r2 * r9 / 8
87
+ - r3 * r7 / 24
88
+ + r6 * (r7 * y1 + 3 * r8 + r9) / 24
89
+ - x0 * x1 * (r8 - r9) / 12
90
+ )
91
+ self.momentYY += (
92
+ -r0 * r9 / 12
93
+ - r1 * r8 / 12
94
+ - r11 * x1 / 12
95
+ - r12 * x1 / 12
96
+ + x0 * (r11 + r12 + r8 * y1 + r9 * y0) / 12
97
+ )
98
+
99
+ @cython.locals(r0=cython.double)
100
+ @cython.locals(r1=cython.double)
101
+ @cython.locals(r2=cython.double)
102
+ @cython.locals(r3=cython.double)
103
+ @cython.locals(r4=cython.double)
104
+ @cython.locals(r5=cython.double)
105
+ @cython.locals(r6=cython.double)
106
+ @cython.locals(r7=cython.double)
107
+ @cython.locals(r8=cython.double)
108
+ @cython.locals(r9=cython.double)
109
+ @cython.locals(r10=cython.double)
110
+ @cython.locals(r11=cython.double)
111
+ @cython.locals(r12=cython.double)
112
+ @cython.locals(r13=cython.double)
113
+ @cython.locals(r14=cython.double)
114
+ @cython.locals(r15=cython.double)
115
+ @cython.locals(r16=cython.double)
116
+ @cython.locals(r17=cython.double)
117
+ @cython.locals(r18=cython.double)
118
+ @cython.locals(r19=cython.double)
119
+ @cython.locals(r20=cython.double)
120
+ @cython.locals(r21=cython.double)
121
+ @cython.locals(r22=cython.double)
122
+ @cython.locals(r23=cython.double)
123
+ @cython.locals(r24=cython.double)
124
+ @cython.locals(r25=cython.double)
125
+ @cython.locals(r26=cython.double)
126
+ @cython.locals(r27=cython.double)
127
+ @cython.locals(r28=cython.double)
128
+ @cython.locals(r29=cython.double)
129
+ @cython.locals(r30=cython.double)
130
+ @cython.locals(r31=cython.double)
131
+ @cython.locals(r32=cython.double)
132
+ @cython.locals(r33=cython.double)
133
+ @cython.locals(r34=cython.double)
134
+ @cython.locals(r35=cython.double)
135
+ @cython.locals(r36=cython.double)
136
+ @cython.locals(r37=cython.double)
137
+ @cython.locals(r38=cython.double)
138
+ @cython.locals(r39=cython.double)
139
+ @cython.locals(r40=cython.double)
140
+ @cython.locals(r41=cython.double)
141
+ @cython.locals(r42=cython.double)
142
+ @cython.locals(r43=cython.double)
143
+ @cython.locals(r44=cython.double)
144
+ @cython.locals(r45=cython.double)
145
+ @cython.locals(r46=cython.double)
146
+ @cython.locals(r47=cython.double)
147
+ @cython.locals(r48=cython.double)
148
+ @cython.locals(r49=cython.double)
149
+ @cython.locals(r50=cython.double)
150
+ @cython.locals(r51=cython.double)
151
+ @cython.locals(r52=cython.double)
152
+ @cython.locals(r53=cython.double)
153
+ @cython.locals(x0=cython.double, y0=cython.double)
154
+ @cython.locals(x1=cython.double, y1=cython.double)
155
+ @cython.locals(x2=cython.double, y2=cython.double)
156
+ def _qCurveToOne(self, p1, p2):
157
+ x0, y0 = self._getCurrentPoint()
158
+ x1, y1 = p1
159
+ x2, y2 = p2
160
+
161
+ r0 = 2 * y1
162
+ r1 = r0 * x2
163
+ r2 = x2 * y2
164
+ r3 = 3 * r2
165
+ r4 = 2 * x1
166
+ r5 = 3 * y0
167
+ r6 = x1**2
168
+ r7 = x2**2
169
+ r8 = 4 * y1
170
+ r9 = 10 * y2
171
+ r10 = 2 * y2
172
+ r11 = r4 * x2
173
+ r12 = x0**2
174
+ r13 = 10 * y0
175
+ r14 = r4 * y2
176
+ r15 = x2 * y0
177
+ r16 = 4 * x1
178
+ r17 = r0 * x1 + r2
179
+ r18 = r2 * r8
180
+ r19 = y1**2
181
+ r20 = 2 * r19
182
+ r21 = y2**2
183
+ r22 = r21 * x2
184
+ r23 = 5 * r22
185
+ r24 = y0**2
186
+ r25 = y0 * y2
187
+ r26 = 5 * r24
188
+ r27 = x1**3
189
+ r28 = x2**3
190
+ r29 = 30 * y1
191
+ r30 = 6 * y1
192
+ r31 = 10 * r7 * x1
193
+ r32 = 5 * y2
194
+ r33 = 12 * r6
195
+ r34 = 30 * x1
196
+ r35 = x1 * y1
197
+ r36 = r3 + 20 * r35
198
+ r37 = 12 * x1
199
+ r38 = 20 * r6
200
+ r39 = 8 * r6 * y1
201
+ r40 = r32 * r7
202
+ r41 = 60 * y1
203
+ r42 = 20 * r19
204
+ r43 = 4 * r19
205
+ r44 = 15 * r21
206
+ r45 = 12 * x2
207
+ r46 = 12 * y2
208
+ r47 = 6 * x1
209
+ r48 = 8 * r19 * x1 + r23
210
+ r49 = 8 * y1**3
211
+ r50 = y2**3
212
+ r51 = y0**3
213
+ r52 = 10 * y1
214
+ r53 = 12 * y1
215
+
216
+ self.area += (
217
+ -r1 / 6
218
+ - r3 / 6
219
+ + x0 * (r0 + r5 + y2) / 6
220
+ + x1 * y2 / 3
221
+ - y0 * (r4 + x2) / 6
222
+ )
223
+ self.momentX += (
224
+ -r11 * (-r10 + y1) / 30
225
+ + r12 * (r13 + r8 + y2) / 30
226
+ + r6 * y2 / 15
227
+ - r7 * r8 / 30
228
+ - r7 * r9 / 30
229
+ + x0 * (r14 - r15 - r16 * y0 + r17) / 30
230
+ - y0 * (r11 + 2 * r6 + r7) / 30
231
+ )
232
+ self.momentY += (
233
+ -r18 / 30
234
+ - r20 * x2 / 30
235
+ - r23 / 30
236
+ - r24 * (r16 + x2) / 30
237
+ + x0 * (r0 * y2 + r20 + r21 + r25 + r26 + r8 * y0) / 30
238
+ + x1 * y2 * (r10 + y1) / 15
239
+ - y0 * (r1 + r17) / 30
240
+ )
241
+ self.momentXX += (
242
+ r12 * (r1 - 5 * r15 - r34 * y0 + r36 + r9 * x1) / 420
243
+ + 2 * r27 * y2 / 105
244
+ - r28 * r29 / 420
245
+ - r28 * y2 / 4
246
+ - r31 * (r0 - 3 * y2) / 420
247
+ - r6 * x2 * (r0 - r32) / 105
248
+ + x0**3 * (r30 + 21 * y0 + y2) / 84
249
+ - x0
250
+ * (
251
+ r0 * r7
252
+ + r15 * r37
253
+ - r2 * r37
254
+ - r33 * y2
255
+ + r38 * y0
256
+ - r39
257
+ - r40
258
+ + r5 * r7
259
+ )
260
+ / 420
261
+ - y0 * (8 * r27 + 5 * r28 + r31 + r33 * x2) / 420
262
+ )
263
+ self.momentXY += (
264
+ r12 * (r13 * y2 + 3 * r21 + 105 * r24 + r41 * y0 + r42 + r46 * y1) / 840
265
+ - r16 * x2 * (r43 - r44) / 840
266
+ - r21 * r7 / 8
267
+ - r24 * (r38 + r45 * x1 + 3 * r7) / 840
268
+ - r41 * r7 * y2 / 840
269
+ - r42 * r7 / 840
270
+ + r6 * y2 * (r32 + r8) / 210
271
+ + x0
272
+ * (
273
+ -r15 * r8
274
+ + r16 * r25
275
+ + r18
276
+ + r21 * r47
277
+ - r24 * r34
278
+ - r26 * x2
279
+ + r35 * r46
280
+ + r48
281
+ )
282
+ / 420
283
+ - y0 * (r16 * r2 + r30 * r7 + r35 * r45 + r39 + r40) / 420
284
+ )
285
+ self.momentYY += (
286
+ -r2 * r42 / 420
287
+ - r22 * r29 / 420
288
+ - r24 * (r14 + r36 + r52 * x2) / 420
289
+ - r49 * x2 / 420
290
+ - r50 * x2 / 12
291
+ - r51 * (r47 + x2) / 84
292
+ + x0
293
+ * (
294
+ r19 * r46
295
+ + r21 * r5
296
+ + r21 * r52
297
+ + r24 * r29
298
+ + r25 * r53
299
+ + r26 * y2
300
+ + r42 * y0
301
+ + r49
302
+ + 5 * r50
303
+ + 35 * r51
304
+ )
305
+ / 420
306
+ + x1 * y2 * (r43 + r44 + r9 * y1) / 210
307
+ - y0 * (r19 * r45 + r2 * r53 - r21 * r4 + r48) / 420
308
+ )
309
+
310
+ @cython.locals(r0=cython.double)
311
+ @cython.locals(r1=cython.double)
312
+ @cython.locals(r2=cython.double)
313
+ @cython.locals(r3=cython.double)
314
+ @cython.locals(r4=cython.double)
315
+ @cython.locals(r5=cython.double)
316
+ @cython.locals(r6=cython.double)
317
+ @cython.locals(r7=cython.double)
318
+ @cython.locals(r8=cython.double)
319
+ @cython.locals(r9=cython.double)
320
+ @cython.locals(r10=cython.double)
321
+ @cython.locals(r11=cython.double)
322
+ @cython.locals(r12=cython.double)
323
+ @cython.locals(r13=cython.double)
324
+ @cython.locals(r14=cython.double)
325
+ @cython.locals(r15=cython.double)
326
+ @cython.locals(r16=cython.double)
327
+ @cython.locals(r17=cython.double)
328
+ @cython.locals(r18=cython.double)
329
+ @cython.locals(r19=cython.double)
330
+ @cython.locals(r20=cython.double)
331
+ @cython.locals(r21=cython.double)
332
+ @cython.locals(r22=cython.double)
333
+ @cython.locals(r23=cython.double)
334
+ @cython.locals(r24=cython.double)
335
+ @cython.locals(r25=cython.double)
336
+ @cython.locals(r26=cython.double)
337
+ @cython.locals(r27=cython.double)
338
+ @cython.locals(r28=cython.double)
339
+ @cython.locals(r29=cython.double)
340
+ @cython.locals(r30=cython.double)
341
+ @cython.locals(r31=cython.double)
342
+ @cython.locals(r32=cython.double)
343
+ @cython.locals(r33=cython.double)
344
+ @cython.locals(r34=cython.double)
345
+ @cython.locals(r35=cython.double)
346
+ @cython.locals(r36=cython.double)
347
+ @cython.locals(r37=cython.double)
348
+ @cython.locals(r38=cython.double)
349
+ @cython.locals(r39=cython.double)
350
+ @cython.locals(r40=cython.double)
351
+ @cython.locals(r41=cython.double)
352
+ @cython.locals(r42=cython.double)
353
+ @cython.locals(r43=cython.double)
354
+ @cython.locals(r44=cython.double)
355
+ @cython.locals(r45=cython.double)
356
+ @cython.locals(r46=cython.double)
357
+ @cython.locals(r47=cython.double)
358
+ @cython.locals(r48=cython.double)
359
+ @cython.locals(r49=cython.double)
360
+ @cython.locals(r50=cython.double)
361
+ @cython.locals(r51=cython.double)
362
+ @cython.locals(r52=cython.double)
363
+ @cython.locals(r53=cython.double)
364
+ @cython.locals(r54=cython.double)
365
+ @cython.locals(r55=cython.double)
366
+ @cython.locals(r56=cython.double)
367
+ @cython.locals(r57=cython.double)
368
+ @cython.locals(r58=cython.double)
369
+ @cython.locals(r59=cython.double)
370
+ @cython.locals(r60=cython.double)
371
+ @cython.locals(r61=cython.double)
372
+ @cython.locals(r62=cython.double)
373
+ @cython.locals(r63=cython.double)
374
+ @cython.locals(r64=cython.double)
375
+ @cython.locals(r65=cython.double)
376
+ @cython.locals(r66=cython.double)
377
+ @cython.locals(r67=cython.double)
378
+ @cython.locals(r68=cython.double)
379
+ @cython.locals(r69=cython.double)
380
+ @cython.locals(r70=cython.double)
381
+ @cython.locals(r71=cython.double)
382
+ @cython.locals(r72=cython.double)
383
+ @cython.locals(r73=cython.double)
384
+ @cython.locals(r74=cython.double)
385
+ @cython.locals(r75=cython.double)
386
+ @cython.locals(r76=cython.double)
387
+ @cython.locals(r77=cython.double)
388
+ @cython.locals(r78=cython.double)
389
+ @cython.locals(r79=cython.double)
390
+ @cython.locals(r80=cython.double)
391
+ @cython.locals(r81=cython.double)
392
+ @cython.locals(r82=cython.double)
393
+ @cython.locals(r83=cython.double)
394
+ @cython.locals(r84=cython.double)
395
+ @cython.locals(r85=cython.double)
396
+ @cython.locals(r86=cython.double)
397
+ @cython.locals(r87=cython.double)
398
+ @cython.locals(r88=cython.double)
399
+ @cython.locals(r89=cython.double)
400
+ @cython.locals(r90=cython.double)
401
+ @cython.locals(r91=cython.double)
402
+ @cython.locals(r92=cython.double)
403
+ @cython.locals(r93=cython.double)
404
+ @cython.locals(r94=cython.double)
405
+ @cython.locals(r95=cython.double)
406
+ @cython.locals(r96=cython.double)
407
+ @cython.locals(r97=cython.double)
408
+ @cython.locals(r98=cython.double)
409
+ @cython.locals(r99=cython.double)
410
+ @cython.locals(r100=cython.double)
411
+ @cython.locals(r101=cython.double)
412
+ @cython.locals(r102=cython.double)
413
+ @cython.locals(r103=cython.double)
414
+ @cython.locals(r104=cython.double)
415
+ @cython.locals(r105=cython.double)
416
+ @cython.locals(r106=cython.double)
417
+ @cython.locals(r107=cython.double)
418
+ @cython.locals(r108=cython.double)
419
+ @cython.locals(r109=cython.double)
420
+ @cython.locals(r110=cython.double)
421
+ @cython.locals(r111=cython.double)
422
+ @cython.locals(r112=cython.double)
423
+ @cython.locals(r113=cython.double)
424
+ @cython.locals(r114=cython.double)
425
+ @cython.locals(r115=cython.double)
426
+ @cython.locals(r116=cython.double)
427
+ @cython.locals(r117=cython.double)
428
+ @cython.locals(r118=cython.double)
429
+ @cython.locals(r119=cython.double)
430
+ @cython.locals(r120=cython.double)
431
+ @cython.locals(r121=cython.double)
432
+ @cython.locals(r122=cython.double)
433
+ @cython.locals(r123=cython.double)
434
+ @cython.locals(r124=cython.double)
435
+ @cython.locals(r125=cython.double)
436
+ @cython.locals(r126=cython.double)
437
+ @cython.locals(r127=cython.double)
438
+ @cython.locals(r128=cython.double)
439
+ @cython.locals(r129=cython.double)
440
+ @cython.locals(r130=cython.double)
441
+ @cython.locals(r131=cython.double)
442
+ @cython.locals(r132=cython.double)
443
+ @cython.locals(x0=cython.double, y0=cython.double)
444
+ @cython.locals(x1=cython.double, y1=cython.double)
445
+ @cython.locals(x2=cython.double, y2=cython.double)
446
+ @cython.locals(x3=cython.double, y3=cython.double)
447
+ def _curveToOne(self, p1, p2, p3):
448
+ x0, y0 = self._getCurrentPoint()
449
+ x1, y1 = p1
450
+ x2, y2 = p2
451
+ x3, y3 = p3
452
+
453
+ r0 = 6 * y2
454
+ r1 = r0 * x3
455
+ r2 = 10 * y3
456
+ r3 = r2 * x3
457
+ r4 = 3 * y1
458
+ r5 = 6 * x1
459
+ r6 = 3 * x2
460
+ r7 = 6 * y1
461
+ r8 = 3 * y2
462
+ r9 = x2**2
463
+ r10 = 45 * r9
464
+ r11 = r10 * y3
465
+ r12 = x3**2
466
+ r13 = r12 * y2
467
+ r14 = r12 * y3
468
+ r15 = 7 * y3
469
+ r16 = 15 * x3
470
+ r17 = r16 * x2
471
+ r18 = x1**2
472
+ r19 = 9 * r18
473
+ r20 = x0**2
474
+ r21 = 21 * y1
475
+ r22 = 9 * r9
476
+ r23 = r7 * x3
477
+ r24 = 9 * y2
478
+ r25 = r24 * x2 + r3
479
+ r26 = 9 * x2
480
+ r27 = x2 * y3
481
+ r28 = -r26 * y1 + 15 * r27
482
+ r29 = 3 * x1
483
+ r30 = 45 * x1
484
+ r31 = 12 * x3
485
+ r32 = 45 * r18
486
+ r33 = 5 * r12
487
+ r34 = r8 * x3
488
+ r35 = 105 * y0
489
+ r36 = 30 * y0
490
+ r37 = r36 * x2
491
+ r38 = 5 * x3
492
+ r39 = 15 * y3
493
+ r40 = 5 * y3
494
+ r41 = r40 * x3
495
+ r42 = x2 * y2
496
+ r43 = 18 * r42
497
+ r44 = 45 * y1
498
+ r45 = r41 + r43 + r44 * x1
499
+ r46 = y2 * y3
500
+ r47 = r46 * x3
501
+ r48 = y2**2
502
+ r49 = 45 * r48
503
+ r50 = r49 * x3
504
+ r51 = y3**2
505
+ r52 = r51 * x3
506
+ r53 = y1**2
507
+ r54 = 9 * r53
508
+ r55 = y0**2
509
+ r56 = 21 * x1
510
+ r57 = 6 * x2
511
+ r58 = r16 * y2
512
+ r59 = r39 * y2
513
+ r60 = 9 * r48
514
+ r61 = r6 * y3
515
+ r62 = 3 * y3
516
+ r63 = r36 * y2
517
+ r64 = y1 * y3
518
+ r65 = 45 * r53
519
+ r66 = 5 * r51
520
+ r67 = x2**3
521
+ r68 = x3**3
522
+ r69 = 630 * y2
523
+ r70 = 126 * x3
524
+ r71 = x1**3
525
+ r72 = 126 * x2
526
+ r73 = 63 * r9
527
+ r74 = r73 * x3
528
+ r75 = r15 * x3 + 15 * r42
529
+ r76 = 630 * x1
530
+ r77 = 14 * x3
531
+ r78 = 21 * r27
532
+ r79 = 42 * x1
533
+ r80 = 42 * x2
534
+ r81 = x1 * y2
535
+ r82 = 63 * r42
536
+ r83 = x1 * y1
537
+ r84 = r41 + r82 + 378 * r83
538
+ r85 = x2 * x3
539
+ r86 = r85 * y1
540
+ r87 = r27 * x3
541
+ r88 = 27 * r9
542
+ r89 = r88 * y2
543
+ r90 = 42 * r14
544
+ r91 = 90 * x1
545
+ r92 = 189 * r18
546
+ r93 = 378 * r18
547
+ r94 = r12 * y1
548
+ r95 = 252 * x1 * x2
549
+ r96 = r79 * x3
550
+ r97 = 30 * r85
551
+ r98 = r83 * x3
552
+ r99 = 30 * x3
553
+ r100 = 42 * x3
554
+ r101 = r42 * x1
555
+ r102 = r10 * y2 + 14 * r14 + 126 * r18 * y1 + r81 * r99
556
+ r103 = 378 * r48
557
+ r104 = 18 * y1
558
+ r105 = r104 * y2
559
+ r106 = y0 * y1
560
+ r107 = 252 * y2
561
+ r108 = r107 * y0
562
+ r109 = y0 * y3
563
+ r110 = 42 * r64
564
+ r111 = 378 * r53
565
+ r112 = 63 * r48
566
+ r113 = 27 * x2
567
+ r114 = r27 * y2
568
+ r115 = r113 * r48 + 42 * r52
569
+ r116 = x3 * y3
570
+ r117 = 54 * r42
571
+ r118 = r51 * x1
572
+ r119 = r51 * x2
573
+ r120 = r48 * x1
574
+ r121 = 21 * x3
575
+ r122 = r64 * x1
576
+ r123 = r81 * y3
577
+ r124 = 30 * r27 * y1 + r49 * x2 + 14 * r52 + 126 * r53 * x1
578
+ r125 = y2**3
579
+ r126 = y3**3
580
+ r127 = y1**3
581
+ r128 = y0**3
582
+ r129 = r51 * y2
583
+ r130 = r112 * y3 + r21 * r51
584
+ r131 = 189 * r53
585
+ r132 = 90 * y2
586
+
587
+ self.area += (
588
+ -r1 / 20
589
+ - r3 / 20
590
+ - r4 * (x2 + x3) / 20
591
+ + x0 * (r7 + r8 + 10 * y0 + y3) / 20
592
+ + 3 * x1 * (y2 + y3) / 20
593
+ + 3 * x2 * y3 / 10
594
+ - y0 * (r5 + r6 + x3) / 20
595
+ )
596
+ self.momentX += (
597
+ r11 / 840
598
+ - r13 / 8
599
+ - r14 / 3
600
+ - r17 * (-r15 + r8) / 840
601
+ + r19 * (r8 + 2 * y3) / 840
602
+ + r20 * (r0 + r21 + 56 * y0 + y3) / 168
603
+ + r29 * (-r23 + r25 + r28) / 840
604
+ - r4 * (10 * r12 + r17 + r22) / 840
605
+ + x0
606
+ * (
607
+ 12 * r27
608
+ + r30 * y2
609
+ + r34
610
+ - r35 * x1
611
+ - r37
612
+ - r38 * y0
613
+ + r39 * x1
614
+ - r4 * x3
615
+ + r45
616
+ )
617
+ / 840
618
+ - y0 * (r17 + r30 * x2 + r31 * x1 + r32 + r33 + 18 * r9) / 840
619
+ )
620
+ self.momentY += (
621
+ -r4 * (r25 + r58) / 840
622
+ - r47 / 8
623
+ - r50 / 840
624
+ - r52 / 6
625
+ - r54 * (r6 + 2 * x3) / 840
626
+ - r55 * (r56 + r57 + x3) / 168
627
+ + x0
628
+ * (
629
+ r35 * y1
630
+ + r40 * y0
631
+ + r44 * y2
632
+ + 18 * r48
633
+ + 140 * r55
634
+ + r59
635
+ + r63
636
+ + 12 * r64
637
+ + r65
638
+ + r66
639
+ )
640
+ / 840
641
+ + x1 * (r24 * y1 + 10 * r51 + r59 + r60 + r7 * y3) / 280
642
+ + x2 * y3 * (r15 + r8) / 56
643
+ - y0 * (r16 * y1 + r31 * y2 + r44 * x2 + r45 + r61 - r62 * x1) / 840
644
+ )
645
+ self.momentXX += (
646
+ -r12 * r72 * (-r40 + r8) / 9240
647
+ + 3 * r18 * (r28 + r34 - r38 * y1 + r75) / 3080
648
+ + r20
649
+ * (
650
+ r24 * x3
651
+ - r72 * y0
652
+ - r76 * y0
653
+ - r77 * y0
654
+ + r78
655
+ + r79 * y3
656
+ + r80 * y1
657
+ + 210 * r81
658
+ + r84
659
+ )
660
+ / 9240
661
+ - r29
662
+ * (
663
+ r12 * r21
664
+ + 14 * r13
665
+ + r44 * r9
666
+ - r73 * y3
667
+ + 54 * r86
668
+ - 84 * r87
669
+ - r89
670
+ - r90
671
+ )
672
+ / 9240
673
+ - r4 * (70 * r12 * x2 + 27 * r67 + 42 * r68 + r74) / 9240
674
+ + 3 * r67 * y3 / 220
675
+ - r68 * r69 / 9240
676
+ - r68 * y3 / 4
677
+ - r70 * r9 * (-r62 + y2) / 9240
678
+ + 3 * r71 * (r24 + r40) / 3080
679
+ + x0**3 * (r24 + r44 + 165 * y0 + y3) / 660
680
+ + x0
681
+ * (
682
+ r100 * r27
683
+ + 162 * r101
684
+ + r102
685
+ + r11
686
+ + 63 * r18 * y3
687
+ + r27 * r91
688
+ - r33 * y0
689
+ - r37 * x3
690
+ + r43 * x3
691
+ - r73 * y0
692
+ - r88 * y1
693
+ + r92 * y2
694
+ - r93 * y0
695
+ - 9 * r94
696
+ - r95 * y0
697
+ - r96 * y0
698
+ - r97 * y1
699
+ - 18 * r98
700
+ + r99 * x1 * y3
701
+ )
702
+ / 9240
703
+ - y0
704
+ * (
705
+ r12 * r56
706
+ + r12 * r80
707
+ + r32 * x3
708
+ + 45 * r67
709
+ + 14 * r68
710
+ + 126 * r71
711
+ + r74
712
+ + r85 * r91
713
+ + 135 * r9 * x1
714
+ + r92 * x2
715
+ )
716
+ / 9240
717
+ )
718
+ self.momentXY += (
719
+ -r103 * r12 / 18480
720
+ - r12 * r51 / 8
721
+ - 3 * r14 * y2 / 44
722
+ + 3 * r18 * (r105 + r2 * y1 + 18 * r46 + 15 * r48 + 7 * r51) / 6160
723
+ + r20
724
+ * (
725
+ 1260 * r106
726
+ + r107 * y1
727
+ + r108
728
+ + 28 * r109
729
+ + r110
730
+ + r111
731
+ + r112
732
+ + 30 * r46
733
+ + 2310 * r55
734
+ + r66
735
+ )
736
+ / 18480
737
+ - r54 * (7 * r12 + 18 * r85 + 15 * r9) / 18480
738
+ - r55 * (r33 + r73 + r93 + r95 + r96 + r97) / 18480
739
+ - r7 * (42 * r13 + r82 * x3 + 28 * r87 + r89 + r90) / 18480
740
+ - 3 * r85 * (r48 - r66) / 220
741
+ + 3 * r9 * y3 * (r62 + 2 * y2) / 440
742
+ + x0
743
+ * (
744
+ -r1 * y0
745
+ - 84 * r106 * x2
746
+ + r109 * r56
747
+ + 54 * r114
748
+ + r117 * y1
749
+ + 15 * r118
750
+ + 21 * r119
751
+ + 81 * r120
752
+ + r121 * r46
753
+ + 54 * r122
754
+ + 60 * r123
755
+ + r124
756
+ - r21 * x3 * y0
757
+ + r23 * y3
758
+ - r54 * x3
759
+ - r55 * r72
760
+ - r55 * r76
761
+ - r55 * r77
762
+ + r57 * y0 * y3
763
+ + r60 * x3
764
+ + 84 * r81 * y0
765
+ + 189 * r81 * y1
766
+ )
767
+ / 9240
768
+ + x1
769
+ * (
770
+ r104 * r27
771
+ - r105 * x3
772
+ - r113 * r53
773
+ + 63 * r114
774
+ + r115
775
+ - r16 * r53
776
+ + 28 * r47
777
+ + r51 * r80
778
+ )
779
+ / 3080
780
+ - y0
781
+ * (
782
+ 54 * r101
783
+ + r102
784
+ + r116 * r5
785
+ + r117 * x3
786
+ + 21 * r13
787
+ - r19 * y3
788
+ + r22 * y3
789
+ + r78 * x3
790
+ + 189 * r83 * x2
791
+ + 60 * r86
792
+ + 81 * r9 * y1
793
+ + 15 * r94
794
+ + 54 * r98
795
+ )
796
+ / 9240
797
+ )
798
+ self.momentYY += (
799
+ -r103 * r116 / 9240
800
+ - r125 * r70 / 9240
801
+ - r126 * x3 / 12
802
+ - 3 * r127 * (r26 + r38) / 3080
803
+ - r128 * (r26 + r30 + x3) / 660
804
+ - r4 * (r112 * x3 + r115 - 14 * r119 + 84 * r47) / 9240
805
+ - r52 * r69 / 9240
806
+ - r54 * (r58 + r61 + r75) / 9240
807
+ - r55
808
+ * (r100 * y1 + r121 * y2 + r26 * y3 + r79 * y2 + r84 + 210 * x2 * y1)
809
+ / 9240
810
+ + x0
811
+ * (
812
+ r108 * y1
813
+ + r110 * y0
814
+ + r111 * y0
815
+ + r112 * y0
816
+ + 45 * r125
817
+ + 14 * r126
818
+ + 126 * r127
819
+ + 770 * r128
820
+ + 42 * r129
821
+ + r130
822
+ + r131 * y2
823
+ + r132 * r64
824
+ + 135 * r48 * y1
825
+ + 630 * r55 * y1
826
+ + 126 * r55 * y2
827
+ + 14 * r55 * y3
828
+ + r63 * y3
829
+ + r65 * y3
830
+ + r66 * y0
831
+ )
832
+ / 9240
833
+ + x1
834
+ * (
835
+ 27 * r125
836
+ + 42 * r126
837
+ + 70 * r129
838
+ + r130
839
+ + r39 * r53
840
+ + r44 * r48
841
+ + 27 * r53 * y2
842
+ + 54 * r64 * y2
843
+ )
844
+ / 3080
845
+ + 3 * x2 * y3 * (r48 + r66 + r8 * y3) / 220
846
+ - y0
847
+ * (
848
+ r100 * r46
849
+ + 18 * r114
850
+ - 9 * r118
851
+ - 27 * r120
852
+ - 18 * r122
853
+ - 30 * r123
854
+ + r124
855
+ + r131 * x2
856
+ + r132 * x3 * y1
857
+ + 162 * r42 * y1
858
+ + r50
859
+ + 63 * r53 * x3
860
+ + r64 * r99
861
+ )
862
+ / 9240
863
+ )
864
+
865
+
866
+ if __name__ == "__main__":
867
+ from fontTools.misc.symfont import x, y, printGreenPen
868
+
869
+ printGreenPen(
870
+ "MomentsPen",
871
+ [
872
+ ("area", 1),
873
+ ("momentX", x),
874
+ ("momentY", y),
875
+ ("momentXX", x**2),
876
+ ("momentXY", x * y),
877
+ ("momentYY", y**2),
878
+ ],
879
+ )
venv/Lib/site-packages/fontTools/pens/perimeterPen.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """Calculate the perimeter of a glyph."""
3
+
4
+ from fontTools.pens.basePen import BasePen
5
+ from fontTools.misc.bezierTools import (
6
+ approximateQuadraticArcLengthC,
7
+ calcQuadraticArcLengthC,
8
+ approximateCubicArcLengthC,
9
+ calcCubicArcLengthC,
10
+ )
11
+ import math
12
+
13
+
14
+ __all__ = ["PerimeterPen"]
15
+
16
+
17
+ def _distance(p0, p1):
18
+ return math.hypot(p0[0] - p1[0], p0[1] - p1[1])
19
+
20
+
21
+ class PerimeterPen(BasePen):
22
+ def __init__(self, glyphset=None, tolerance=0.005):
23
+ BasePen.__init__(self, glyphset)
24
+ self.value = 0
25
+ self.tolerance = tolerance
26
+
27
+ # Choose which algorithm to use for quadratic and for cubic.
28
+ # Quadrature is faster but has fixed error characteristic with no strong
29
+ # error bound. The cutoff points are derived empirically.
30
+ self._addCubic = (
31
+ self._addCubicQuadrature if tolerance >= 0.0015 else self._addCubicRecursive
32
+ )
33
+ self._addQuadratic = (
34
+ self._addQuadraticQuadrature
35
+ if tolerance >= 0.00075
36
+ else self._addQuadraticExact
37
+ )
38
+
39
+ def _moveTo(self, p0):
40
+ self.__startPoint = p0
41
+
42
+ def _closePath(self):
43
+ p0 = self._getCurrentPoint()
44
+ if p0 != self.__startPoint:
45
+ self._lineTo(self.__startPoint)
46
+
47
+ def _lineTo(self, p1):
48
+ p0 = self._getCurrentPoint()
49
+ self.value += _distance(p0, p1)
50
+
51
+ def _addQuadraticExact(self, c0, c1, c2):
52
+ self.value += calcQuadraticArcLengthC(c0, c1, c2)
53
+
54
+ def _addQuadraticQuadrature(self, c0, c1, c2):
55
+ self.value += approximateQuadraticArcLengthC(c0, c1, c2)
56
+
57
+ def _qCurveToOne(self, p1, p2):
58
+ p0 = self._getCurrentPoint()
59
+ self._addQuadratic(complex(*p0), complex(*p1), complex(*p2))
60
+
61
+ def _addCubicRecursive(self, c0, c1, c2, c3):
62
+ self.value += calcCubicArcLengthC(c0, c1, c2, c3, self.tolerance)
63
+
64
+ def _addCubicQuadrature(self, c0, c1, c2, c3):
65
+ self.value += approximateCubicArcLengthC(c0, c1, c2, c3)
66
+
67
+ def _curveToOne(self, p1, p2, p3):
68
+ p0 = self._getCurrentPoint()
69
+ self._addCubic(complex(*p0), complex(*p1), complex(*p2), complex(*p3))
venv/Lib/site-packages/fontTools/pens/pointInsidePen.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """fontTools.pens.pointInsidePen -- Pen implementing "point inside" testing
2
+ for shapes.
3
+ """
4
+
5
+ from fontTools.pens.basePen import BasePen
6
+ from fontTools.misc.bezierTools import solveQuadratic, solveCubic
7
+
8
+
9
+ __all__ = ["PointInsidePen"]
10
+
11
+
12
+ class PointInsidePen(BasePen):
13
+ """This pen implements "point inside" testing: to test whether
14
+ a given point lies inside the shape (black) or outside (white).
15
+ Instances of this class can be recycled, as long as the
16
+ setTestPoint() method is used to set the new point to test.
17
+
18
+ :Example:
19
+ .. code-block::
20
+
21
+ pen = PointInsidePen(glyphSet, (100, 200))
22
+ outline.draw(pen)
23
+ isInside = pen.getResult()
24
+
25
+ Both the even-odd algorithm and the non-zero-winding-rule
26
+ algorithm are implemented. The latter is the default, specify
27
+ True for the evenOdd argument of __init__ or setTestPoint
28
+ to use the even-odd algorithm.
29
+ """
30
+
31
+ # This class implements the classical "shoot a ray from the test point
32
+ # to infinity and count how many times it intersects the outline" (as well
33
+ # as the non-zero variant, where the counter is incremented if the outline
34
+ # intersects the ray in one direction and decremented if it intersects in
35
+ # the other direction).
36
+ # I found an amazingly clear explanation of the subtleties involved in
37
+ # implementing this correctly for polygons here:
38
+ # http://graphics.cs.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html
39
+ # I extended the principles outlined on that page to curves.
40
+
41
+ def __init__(self, glyphSet, testPoint, evenOdd=False):
42
+ BasePen.__init__(self, glyphSet)
43
+ self.setTestPoint(testPoint, evenOdd)
44
+
45
+ def setTestPoint(self, testPoint, evenOdd=False):
46
+ """Set the point to test. Call this _before_ the outline gets drawn."""
47
+ self.testPoint = testPoint
48
+ self.evenOdd = evenOdd
49
+ self.firstPoint = None
50
+ self.intersectionCount = 0
51
+
52
+ def getWinding(self):
53
+ if self.firstPoint is not None:
54
+ # always make sure the sub paths are closed; the algorithm only works
55
+ # for closed paths.
56
+ self.closePath()
57
+ return self.intersectionCount
58
+
59
+ def getResult(self):
60
+ """After the shape has been drawn, getResult() returns True if the test
61
+ point lies within the (black) shape, and False if it doesn't.
62
+ """
63
+ winding = self.getWinding()
64
+ if self.evenOdd:
65
+ result = winding % 2
66
+ else: # non-zero
67
+ result = self.intersectionCount != 0
68
+ return not not result
69
+
70
+ def _addIntersection(self, goingUp):
71
+ if self.evenOdd or goingUp:
72
+ self.intersectionCount += 1
73
+ else:
74
+ self.intersectionCount -= 1
75
+
76
+ def _moveTo(self, point):
77
+ if self.firstPoint is not None:
78
+ # always make sure the sub paths are closed; the algorithm only works
79
+ # for closed paths.
80
+ self.closePath()
81
+ self.firstPoint = point
82
+
83
+ def _lineTo(self, point):
84
+ x, y = self.testPoint
85
+ x1, y1 = self._getCurrentPoint()
86
+ x2, y2 = point
87
+
88
+ if x1 < x and x2 < x:
89
+ return
90
+ if y1 < y and y2 < y:
91
+ return
92
+ if y1 >= y and y2 >= y:
93
+ return
94
+
95
+ dx = x2 - x1
96
+ dy = y2 - y1
97
+ t = (y - y1) / dy
98
+ ix = dx * t + x1
99
+ if ix < x:
100
+ return
101
+ self._addIntersection(y2 > y1)
102
+
103
+ def _curveToOne(self, bcp1, bcp2, point):
104
+ x, y = self.testPoint
105
+ x1, y1 = self._getCurrentPoint()
106
+ x2, y2 = bcp1
107
+ x3, y3 = bcp2
108
+ x4, y4 = point
109
+
110
+ if x1 < x and x2 < x and x3 < x and x4 < x:
111
+ return
112
+ if y1 < y and y2 < y and y3 < y and y4 < y:
113
+ return
114
+ if y1 >= y and y2 >= y and y3 >= y and y4 >= y:
115
+ return
116
+
117
+ dy = y1
118
+ cy = (y2 - dy) * 3.0
119
+ by = (y3 - y2) * 3.0 - cy
120
+ ay = y4 - dy - cy - by
121
+ solutions = sorted(solveCubic(ay, by, cy, dy - y))
122
+ solutions = [t for t in solutions if -0.0 <= t <= 1.0]
123
+ if not solutions:
124
+ return
125
+
126
+ dx = x1
127
+ cx = (x2 - dx) * 3.0
128
+ bx = (x3 - x2) * 3.0 - cx
129
+ ax = x4 - dx - cx - bx
130
+
131
+ above = y1 >= y
132
+ lastT = None
133
+ for t in solutions:
134
+ if t == lastT:
135
+ continue
136
+ lastT = t
137
+ t2 = t * t
138
+ t3 = t2 * t
139
+
140
+ direction = 3 * ay * t2 + 2 * by * t + cy
141
+ incomingGoingUp = outgoingGoingUp = direction > 0.0
142
+ if direction == 0.0:
143
+ direction = 6 * ay * t + 2 * by
144
+ outgoingGoingUp = direction > 0.0
145
+ incomingGoingUp = not outgoingGoingUp
146
+ if direction == 0.0:
147
+ direction = ay
148
+ incomingGoingUp = outgoingGoingUp = direction > 0.0
149
+
150
+ xt = ax * t3 + bx * t2 + cx * t + dx
151
+ if xt < x:
152
+ continue
153
+
154
+ if t in (0.0, -0.0):
155
+ if not outgoingGoingUp:
156
+ self._addIntersection(outgoingGoingUp)
157
+ elif t == 1.0:
158
+ if incomingGoingUp:
159
+ self._addIntersection(incomingGoingUp)
160
+ else:
161
+ if incomingGoingUp == outgoingGoingUp:
162
+ self._addIntersection(outgoingGoingUp)
163
+ # else:
164
+ # we're not really intersecting, merely touching
165
+
166
+ def _qCurveToOne_unfinished(self, bcp, point):
167
+ # XXX need to finish this, for now doing it through a cubic
168
+ # (BasePen implements _qCurveTo in terms of a cubic) will
169
+ # have to do.
170
+ x, y = self.testPoint
171
+ x1, y1 = self._getCurrentPoint()
172
+ x2, y2 = bcp
173
+ x3, y3 = point
174
+ c = y1
175
+ b = (y2 - c) * 2.0
176
+ a = y3 - c - b
177
+ solutions = sorted(solveQuadratic(a, b, c - y))
178
+ solutions = [
179
+ t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON
180
+ ]
181
+ if not solutions:
182
+ return
183
+ # XXX
184
+
185
+ def _closePath(self):
186
+ if self._getCurrentPoint() != self.firstPoint:
187
+ self.lineTo(self.firstPoint)
188
+ self.firstPoint = None
189
+
190
+ def _endPath(self):
191
+ """Insideness is not defined for open contours."""
192
+ raise NotImplementedError
venv/Lib/site-packages/fontTools/pens/pointPen.py ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ =========
3
+ PointPens
4
+ =========
5
+
6
+ Where **SegmentPens** have an intuitive approach to drawing
7
+ (if you're familiar with postscript anyway), the **PointPen**
8
+ is geared towards accessing all the data in the contours of
9
+ the glyph. A PointPen has a very simple interface, it just
10
+ steps through all the points in a call from glyph.drawPoints().
11
+ This allows the caller to provide more data for each point.
12
+ For instance, whether or not a point is smooth, and its name.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import Any, Dict, List, Optional, Tuple
19
+
20
+ from fontTools.misc.enumTools import StrEnum
21
+ from fontTools.misc.loggingTools import LogMixin
22
+ from fontTools.misc.transform import DecomposedTransform, Identity
23
+ from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError
24
+
25
+ __all__ = [
26
+ "AbstractPointPen",
27
+ "BasePointToSegmentPen",
28
+ "PointToSegmentPen",
29
+ "SegmentToPointPen",
30
+ "GuessSmoothPointPen",
31
+ "ReverseContourPointPen",
32
+ "ReverseFlipped",
33
+ ]
34
+
35
+ # Some type aliases to make it easier below
36
+ Point = Tuple[float, float]
37
+ PointName = Optional[str]
38
+ # [(pt, smooth, name, kwargs)]
39
+ SegmentPointList = List[Tuple[Optional[Point], bool, PointName, Any]]
40
+ SegmentType = Optional[str]
41
+ SegmentList = List[Tuple[SegmentType, SegmentPointList]]
42
+
43
+
44
+ class ReverseFlipped(StrEnum):
45
+ """How to handle flipped components during decomposition.
46
+
47
+ NO: Don't reverse flipped components
48
+ KEEP_START: Reverse flipped components, keeping original starting point
49
+ ON_CURVE_FIRST: Reverse flipped components, ensuring first point is on-curve
50
+ """
51
+
52
+ NO = "no"
53
+ KEEP_START = "keep_start"
54
+ ON_CURVE_FIRST = "on_curve_first"
55
+
56
+
57
+ class AbstractPointPen:
58
+ """Baseclass for all PointPens."""
59
+
60
+ def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
61
+ """Start a new sub path."""
62
+ raise NotImplementedError
63
+
64
+ def endPath(self) -> None:
65
+ """End the current sub path."""
66
+ raise NotImplementedError
67
+
68
+ def addPoint(
69
+ self,
70
+ pt: Tuple[float, float],
71
+ segmentType: Optional[str] = None,
72
+ smooth: bool = False,
73
+ name: Optional[str] = None,
74
+ identifier: Optional[str] = None,
75
+ **kwargs: Any,
76
+ ) -> None:
77
+ """Add a point to the current sub path."""
78
+ raise NotImplementedError
79
+
80
+ def addComponent(
81
+ self,
82
+ baseGlyphName: str,
83
+ transformation: Tuple[float, float, float, float, float, float],
84
+ identifier: Optional[str] = None,
85
+ **kwargs: Any,
86
+ ) -> None:
87
+ """Add a sub glyph."""
88
+ raise NotImplementedError
89
+
90
+ def addVarComponent(
91
+ self,
92
+ glyphName: str,
93
+ transformation: DecomposedTransform,
94
+ location: Dict[str, float],
95
+ identifier: Optional[str] = None,
96
+ **kwargs: Any,
97
+ ) -> None:
98
+ """Add a VarComponent sub glyph. The 'transformation' argument
99
+ must be a DecomposedTransform from the fontTools.misc.transform module,
100
+ and the 'location' argument must be a dictionary mapping axis tags
101
+ to their locations.
102
+ """
103
+ # ttGlyphSet decomposes for us
104
+ raise AttributeError
105
+
106
+
107
+ class BasePointToSegmentPen(AbstractPointPen):
108
+ """
109
+ Base class for retrieving the outline in a segment-oriented
110
+ way. The PointPen protocol is simple yet also a little tricky,
111
+ so when you need an outline presented as segments but you have
112
+ as points, do use this base implementation as it properly takes
113
+ care of all the edge cases.
114
+ """
115
+
116
+ def __init__(self) -> None:
117
+ self.currentPath = None
118
+
119
+ def beginPath(self, identifier=None, **kwargs):
120
+ if self.currentPath is not None:
121
+ raise PenError("Path already begun.")
122
+ self.currentPath = []
123
+
124
+ def _flushContour(self, segments: SegmentList) -> None:
125
+ """Override this method.
126
+
127
+ It will be called for each non-empty sub path with a list
128
+ of segments: the 'segments' argument.
129
+
130
+ The segments list contains tuples of length 2:
131
+ (segmentType, points)
132
+
133
+ segmentType is one of "move", "line", "curve" or "qcurve".
134
+ "move" may only occur as the first segment, and it signifies
135
+ an OPEN path. A CLOSED path does NOT start with a "move", in
136
+ fact it will not contain a "move" at ALL.
137
+
138
+ The 'points' field in the 2-tuple is a list of point info
139
+ tuples. The list has 1 or more items, a point tuple has
140
+ four items:
141
+ (point, smooth, name, kwargs)
142
+ 'point' is an (x, y) coordinate pair.
143
+
144
+ For a closed path, the initial moveTo point is defined as
145
+ the last point of the last segment.
146
+
147
+ The 'points' list of "move" and "line" segments always contains
148
+ exactly one point tuple.
149
+ """
150
+ raise NotImplementedError
151
+
152
+ def endPath(self) -> None:
153
+ if self.currentPath is None:
154
+ raise PenError("Path not begun.")
155
+ points = self.currentPath
156
+ self.currentPath = None
157
+ if not points:
158
+ return
159
+ if len(points) == 1:
160
+ # Not much more we can do than output a single move segment.
161
+ pt, segmentType, smooth, name, kwargs = points[0]
162
+ segments: SegmentList = [("move", [(pt, smooth, name, kwargs)])]
163
+ self._flushContour(segments)
164
+ return
165
+ segments = []
166
+ if points[0][1] == "move":
167
+ # It's an open contour, insert a "move" segment for the first
168
+ # point and remove that first point from the point list.
169
+ pt, segmentType, smooth, name, kwargs = points[0]
170
+ segments.append(("move", [(pt, smooth, name, kwargs)]))
171
+ points.pop(0)
172
+ else:
173
+ # It's a closed contour. Locate the first on-curve point, and
174
+ # rotate the point list so that it _ends_ with an on-curve
175
+ # point.
176
+ firstOnCurve = None
177
+ for i in range(len(points)):
178
+ segmentType = points[i][1]
179
+ if segmentType is not None:
180
+ firstOnCurve = i
181
+ break
182
+ if firstOnCurve is None:
183
+ # Special case for quadratics: a contour with no on-curve
184
+ # points. Add a "None" point. (See also the Pen protocol's
185
+ # qCurveTo() method and fontTools.pens.basePen.py.)
186
+ points.append((None, "qcurve", None, None, None))
187
+ else:
188
+ points = points[firstOnCurve + 1 :] + points[: firstOnCurve + 1]
189
+
190
+ currentSegment: SegmentPointList = []
191
+ for pt, segmentType, smooth, name, kwargs in points:
192
+ currentSegment.append((pt, smooth, name, kwargs))
193
+ if segmentType is None:
194
+ continue
195
+ segments.append((segmentType, currentSegment))
196
+ currentSegment = []
197
+
198
+ self._flushContour(segments)
199
+
200
+ def addPoint(
201
+ self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
202
+ ):
203
+ if self.currentPath is None:
204
+ raise PenError("Path not begun")
205
+ self.currentPath.append((pt, segmentType, smooth, name, kwargs))
206
+
207
+
208
+ class PointToSegmentPen(BasePointToSegmentPen):
209
+ """
210
+ Adapter class that converts the PointPen protocol to the
211
+ (Segment)Pen protocol.
212
+
213
+ NOTE: The segment pen does not support and will drop point names, identifiers
214
+ and kwargs.
215
+ """
216
+
217
+ def __init__(self, segmentPen, outputImpliedClosingLine: bool = False) -> None:
218
+ BasePointToSegmentPen.__init__(self)
219
+ self.pen = segmentPen
220
+ self.outputImpliedClosingLine = outputImpliedClosingLine
221
+
222
+ def _flushContour(self, segments):
223
+ if not segments:
224
+ raise PenError("Must have at least one segment.")
225
+ pen = self.pen
226
+ if segments[0][0] == "move":
227
+ # It's an open path.
228
+ closed = False
229
+ points = segments[0][1]
230
+ if len(points) != 1:
231
+ raise PenError(f"Illegal move segment point count: {len(points)}")
232
+ movePt, _, _, _ = points[0]
233
+ del segments[0]
234
+ else:
235
+ # It's a closed path, do a moveTo to the last
236
+ # point of the last segment.
237
+ closed = True
238
+ segmentType, points = segments[-1]
239
+ movePt, _, _, _ = points[-1]
240
+ if movePt is None:
241
+ # quad special case: a contour with no on-curve points contains
242
+ # one "qcurve" segment that ends with a point that's None. We
243
+ # must not output a moveTo() in that case.
244
+ pass
245
+ else:
246
+ pen.moveTo(movePt)
247
+ outputImpliedClosingLine = self.outputImpliedClosingLine
248
+ nSegments = len(segments)
249
+ lastPt = movePt
250
+ for i in range(nSegments):
251
+ segmentType, points = segments[i]
252
+ points = [pt for pt, _, _, _ in points]
253
+ if segmentType == "line":
254
+ if len(points) != 1:
255
+ raise PenError(f"Illegal line segment point count: {len(points)}")
256
+ pt = points[0]
257
+ # For closed contours, a 'lineTo' is always implied from the last oncurve
258
+ # point to the starting point, thus we can omit it when the last and
259
+ # starting point don't overlap.
260
+ # However, when the last oncurve point is a "line" segment and has same
261
+ # coordinates as the starting point of a closed contour, we need to output
262
+ # the closing 'lineTo' explicitly (regardless of the value of the
263
+ # 'outputImpliedClosingLine' option) in order to disambiguate this case from
264
+ # the implied closing 'lineTo', otherwise the duplicate point would be lost.
265
+ # See https://github.com/googlefonts/fontmake/issues/572.
266
+ if (
267
+ i + 1 != nSegments
268
+ or outputImpliedClosingLine
269
+ or not closed
270
+ or pt == lastPt
271
+ ):
272
+ pen.lineTo(pt)
273
+ lastPt = pt
274
+ elif segmentType == "curve":
275
+ pen.curveTo(*points)
276
+ lastPt = points[-1]
277
+ elif segmentType == "qcurve":
278
+ pen.qCurveTo(*points)
279
+ lastPt = points[-1]
280
+ else:
281
+ raise PenError(f"Illegal segmentType: {segmentType}")
282
+ if closed:
283
+ pen.closePath()
284
+ else:
285
+ pen.endPath()
286
+
287
+ def addComponent(self, glyphName, transform, identifier=None, **kwargs):
288
+ del identifier # unused
289
+ del kwargs # unused
290
+ self.pen.addComponent(glyphName, transform)
291
+
292
+
293
+ class SegmentToPointPen(AbstractPen):
294
+ """
295
+ Adapter class that converts the (Segment)Pen protocol to the
296
+ PointPen protocol.
297
+ """
298
+
299
+ def __init__(self, pointPen, guessSmooth=True) -> None:
300
+ if guessSmooth:
301
+ self.pen = GuessSmoothPointPen(pointPen)
302
+ else:
303
+ self.pen = pointPen
304
+ self.contour: Optional[List[Tuple[Point, SegmentType]]] = None
305
+
306
+ def _flushContour(self) -> None:
307
+ pen = self.pen
308
+ pen.beginPath()
309
+ for pt, segmentType in self.contour:
310
+ pen.addPoint(pt, segmentType=segmentType)
311
+ pen.endPath()
312
+
313
+ def moveTo(self, pt):
314
+ self.contour = []
315
+ self.contour.append((pt, "move"))
316
+
317
+ def lineTo(self, pt):
318
+ if self.contour is None:
319
+ raise PenError("Contour missing required initial moveTo")
320
+ self.contour.append((pt, "line"))
321
+
322
+ def curveTo(self, *pts):
323
+ if not pts:
324
+ raise TypeError("Must pass in at least one point")
325
+ if self.contour is None:
326
+ raise PenError("Contour missing required initial moveTo")
327
+ for pt in pts[:-1]:
328
+ self.contour.append((pt, None))
329
+ self.contour.append((pts[-1], "curve"))
330
+
331
+ def qCurveTo(self, *pts):
332
+ if not pts:
333
+ raise TypeError("Must pass in at least one point")
334
+ if pts[-1] is None:
335
+ self.contour = []
336
+ else:
337
+ if self.contour is None:
338
+ raise PenError("Contour missing required initial moveTo")
339
+ for pt in pts[:-1]:
340
+ self.contour.append((pt, None))
341
+ if pts[-1] is not None:
342
+ self.contour.append((pts[-1], "qcurve"))
343
+
344
+ def closePath(self):
345
+ if self.contour is None:
346
+ raise PenError("Contour missing required initial moveTo")
347
+ if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]:
348
+ self.contour[0] = self.contour[-1]
349
+ del self.contour[-1]
350
+ else:
351
+ # There's an implied line at the end, replace "move" with "line"
352
+ # for the first point
353
+ pt, tp = self.contour[0]
354
+ if tp == "move":
355
+ self.contour[0] = pt, "line"
356
+ self._flushContour()
357
+ self.contour = None
358
+
359
+ def endPath(self):
360
+ if self.contour is None:
361
+ raise PenError("Contour missing required initial moveTo")
362
+ self._flushContour()
363
+ self.contour = None
364
+
365
+ def addComponent(self, glyphName, transform):
366
+ if self.contour is not None:
367
+ raise PenError("Components must be added before or after contours")
368
+ self.pen.addComponent(glyphName, transform)
369
+
370
+
371
+ class GuessSmoothPointPen(AbstractPointPen):
372
+ """
373
+ Filtering PointPen that tries to determine whether an on-curve point
374
+ should be "smooth", ie. that it's a "tangent" point or a "curve" point.
375
+ """
376
+
377
+ def __init__(self, outPen, error=0.05):
378
+ self._outPen = outPen
379
+ self._error = error
380
+ self._points = None
381
+
382
+ def _flushContour(self):
383
+ if self._points is None:
384
+ raise PenError("Path not begun")
385
+ points = self._points
386
+ nPoints = len(points)
387
+ if not nPoints:
388
+ return
389
+ if points[0][1] == "move":
390
+ # Open path.
391
+ indices = range(1, nPoints - 1)
392
+ elif nPoints > 1:
393
+ # Closed path. To avoid having to mod the contour index, we
394
+ # simply abuse Python's negative index feature, and start at -1
395
+ indices = range(-1, nPoints - 1)
396
+ else:
397
+ # closed path containing 1 point (!), ignore.
398
+ indices = []
399
+ for i in indices:
400
+ pt, segmentType, _, name, kwargs = points[i]
401
+ if segmentType is None:
402
+ continue
403
+ prev = i - 1
404
+ next = i + 1
405
+ if points[prev][1] is not None and points[next][1] is not None:
406
+ continue
407
+ # At least one of our neighbors is an off-curve point
408
+ pt = points[i][0]
409
+ prevPt = points[prev][0]
410
+ nextPt = points[next][0]
411
+ if pt != prevPt and pt != nextPt:
412
+ dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1]
413
+ dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1]
414
+ a1 = math.atan2(dy1, dx1)
415
+ a2 = math.atan2(dy2, dx2)
416
+ if abs(a1 - a2) < self._error:
417
+ points[i] = pt, segmentType, True, name, kwargs
418
+
419
+ for pt, segmentType, smooth, name, kwargs in points:
420
+ self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
421
+
422
+ def beginPath(self, identifier=None, **kwargs):
423
+ if self._points is not None:
424
+ raise PenError("Path already begun")
425
+ self._points = []
426
+ if identifier is not None:
427
+ kwargs["identifier"] = identifier
428
+ self._outPen.beginPath(**kwargs)
429
+
430
+ def endPath(self):
431
+ self._flushContour()
432
+ self._outPen.endPath()
433
+ self._points = None
434
+
435
+ def addPoint(
436
+ self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
437
+ ):
438
+ if self._points is None:
439
+ raise PenError("Path not begun")
440
+ if identifier is not None:
441
+ kwargs["identifier"] = identifier
442
+ self._points.append((pt, segmentType, False, name, kwargs))
443
+
444
+ def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
445
+ if self._points is not None:
446
+ raise PenError("Components must be added before or after contours")
447
+ if identifier is not None:
448
+ kwargs["identifier"] = identifier
449
+ self._outPen.addComponent(glyphName, transformation, **kwargs)
450
+
451
+ def addVarComponent(
452
+ self, glyphName, transformation, location, identifier=None, **kwargs
453
+ ):
454
+ if self._points is not None:
455
+ raise PenError("VarComponents must be added before or after contours")
456
+ if identifier is not None:
457
+ kwargs["identifier"] = identifier
458
+ self._outPen.addVarComponent(glyphName, transformation, location, **kwargs)
459
+
460
+
461
+ class ReverseContourPointPen(AbstractPointPen):
462
+ """
463
+ This is a PointPen that passes outline data to another PointPen, but
464
+ reversing the winding direction of all contours. Components are simply
465
+ passed through unchanged.
466
+
467
+ Closed contours are reversed in such a way that the first point remains
468
+ the first point.
469
+ """
470
+
471
+ def __init__(self, outputPointPen):
472
+ self.pen = outputPointPen
473
+ # a place to store the points for the current sub path
474
+ self.currentContour = None
475
+
476
+ def _flushContour(self):
477
+ pen = self.pen
478
+ contour = self.currentContour
479
+ if not contour:
480
+ pen.beginPath(identifier=self.currentContourIdentifier)
481
+ pen.endPath()
482
+ return
483
+
484
+ closed = contour[0][1] != "move"
485
+ if not closed:
486
+ lastSegmentType = "move"
487
+ else:
488
+ # Remove the first point and insert it at the end. When
489
+ # the list of points gets reversed, this point will then
490
+ # again be at the start. In other words, the following
491
+ # will hold:
492
+ # for N in range(len(originalContour)):
493
+ # originalContour[N] == reversedContour[-N]
494
+ contour.append(contour.pop(0))
495
+ # Find the first on-curve point.
496
+ firstOnCurve = None
497
+ for i in range(len(contour)):
498
+ if contour[i][1] is not None:
499
+ firstOnCurve = i
500
+ break
501
+ if firstOnCurve is None:
502
+ # There are no on-curve points, be basically have to
503
+ # do nothing but contour.reverse().
504
+ lastSegmentType = None
505
+ else:
506
+ lastSegmentType = contour[firstOnCurve][1]
507
+
508
+ contour.reverse()
509
+ if not closed:
510
+ # Open paths must start with a move, so we simply dump
511
+ # all off-curve points leading up to the first on-curve.
512
+ while contour[0][1] is None:
513
+ contour.pop(0)
514
+ pen.beginPath(identifier=self.currentContourIdentifier)
515
+ for pt, nextSegmentType, smooth, name, kwargs in contour:
516
+ if nextSegmentType is not None:
517
+ segmentType = lastSegmentType
518
+ lastSegmentType = nextSegmentType
519
+ else:
520
+ segmentType = None
521
+ pen.addPoint(
522
+ pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs
523
+ )
524
+ pen.endPath()
525
+
526
+ def beginPath(self, identifier=None, **kwargs):
527
+ if self.currentContour is not None:
528
+ raise PenError("Path already begun")
529
+ self.currentContour = []
530
+ self.currentContourIdentifier = identifier
531
+ self.onCurve = []
532
+
533
+ def endPath(self):
534
+ if self.currentContour is None:
535
+ raise PenError("Path not begun")
536
+ self._flushContour()
537
+ self.currentContour = None
538
+
539
+ def addPoint(
540
+ self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
541
+ ):
542
+ if self.currentContour is None:
543
+ raise PenError("Path not begun")
544
+ if identifier is not None:
545
+ kwargs["identifier"] = identifier
546
+ self.currentContour.append((pt, segmentType, smooth, name, kwargs))
547
+
548
+ def addComponent(self, glyphName, transform, identifier=None, **kwargs):
549
+ if self.currentContour is not None:
550
+ raise PenError("Components must be added before or after contours")
551
+ self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs)
552
+
553
+
554
+ class DecomposingPointPen(LogMixin, AbstractPointPen):
555
+ """Implements a 'addComponent' method that decomposes components
556
+ (i.e. draws them onto self as simple contours).
557
+ It can also be used as a mixin class (e.g. see DecomposingRecordingPointPen).
558
+
559
+ You must override beginPath, addPoint, endPath. You may
560
+ additionally override addVarComponent and addComponent.
561
+
562
+ By default a warning message is logged when a base glyph is missing;
563
+ set the class variable ``skipMissingComponents`` to False if you want
564
+ all instances of a sub-class to raise a :class:`MissingComponentError`
565
+ exception by default.
566
+ """
567
+
568
+ skipMissingComponents = True
569
+ # alias error for convenience
570
+ MissingComponentError = MissingComponentError
571
+
572
+ def __init__(
573
+ self,
574
+ glyphSet,
575
+ *args,
576
+ skipMissingComponents=None,
577
+ reverseFlipped: bool | ReverseFlipped = False,
578
+ **kwargs,
579
+ ):
580
+ """Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
581
+ as components are looked up by their name.
582
+
583
+ If the optional 'reverseFlipped' argument is True or a ReverseFlipped enum value,
584
+ components whose transformation matrix has a negative determinant will be decomposed
585
+ with a reversed path direction to compensate for the flip.
586
+
587
+ The reverseFlipped parameter can be:
588
+ - False or ReverseFlipped.NO: Don't reverse flipped components
589
+ - True or ReverseFlipped.KEEP_START: Reverse, keeping original starting point
590
+ - ReverseFlipped.ON_CURVE_FIRST: Reverse, ensuring first point is on-curve
591
+
592
+ The optional 'skipMissingComponents' argument can be set to True/False to
593
+ override the homonymous class attribute for a given pen instance.
594
+ """
595
+ super().__init__(*args, **kwargs)
596
+ self.glyphSet = glyphSet
597
+ self.skipMissingComponents = (
598
+ self.__class__.skipMissingComponents
599
+ if skipMissingComponents is None
600
+ else skipMissingComponents
601
+ )
602
+ # Handle backward compatibility and validate string inputs
603
+ if reverseFlipped is False:
604
+ self.reverseFlipped = ReverseFlipped.NO
605
+ elif reverseFlipped is True:
606
+ self.reverseFlipped = ReverseFlipped.KEEP_START
607
+ else:
608
+ self.reverseFlipped = ReverseFlipped(reverseFlipped)
609
+
610
+ def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
611
+ """Transform the points of the base glyph and draw it onto self.
612
+
613
+ The `identifier` parameter and any extra kwargs are ignored.
614
+ """
615
+ from fontTools.pens.transformPen import TransformPointPen
616
+
617
+ try:
618
+ glyph = self.glyphSet[baseGlyphName]
619
+ except KeyError:
620
+ if not self.skipMissingComponents:
621
+ raise MissingComponentError(baseGlyphName)
622
+ self.log.warning(
623
+ "glyph '%s' is missing from glyphSet; skipped" % baseGlyphName
624
+ )
625
+ else:
626
+ pen = self
627
+ if transformation != Identity:
628
+ pen = TransformPointPen(pen, transformation)
629
+ if self.reverseFlipped != ReverseFlipped.NO:
630
+ # if the transformation has a negative determinant, it will
631
+ # reverse the contour direction of the component
632
+ a, b, c, d = transformation[:4]
633
+ if a * d - b * c < 0:
634
+ pen = ReverseContourPointPen(pen)
635
+
636
+ if self.reverseFlipped == ReverseFlipped.ON_CURVE_FIRST:
637
+ from fontTools.pens.filterPen import OnCurveFirstPointPen
638
+
639
+ # Ensure the starting point is an on-curve.
640
+ # Wrap last so this filter runs first during drawPoints
641
+ pen = OnCurveFirstPointPen(pen)
642
+
643
+ glyph.drawPoints(pen)
venv/Lib/site-packages/fontTools/pens/qtPen.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.pens.basePen import BasePen
2
+
3
+
4
+ __all__ = ["QtPen"]
5
+
6
+
7
+ class QtPen(BasePen):
8
+ def __init__(self, glyphSet, path=None):
9
+ BasePen.__init__(self, glyphSet)
10
+ if path is None:
11
+ from PyQt5.QtGui import QPainterPath
12
+
13
+ path = QPainterPath()
14
+ self.path = path
15
+
16
+ def _moveTo(self, p):
17
+ self.path.moveTo(*p)
18
+
19
+ def _lineTo(self, p):
20
+ self.path.lineTo(*p)
21
+
22
+ def _curveToOne(self, p1, p2, p3):
23
+ self.path.cubicTo(*p1, *p2, *p3)
24
+
25
+ def _qCurveToOne(self, p1, p2):
26
+ self.path.quadTo(*p1, *p2)
27
+
28
+ def _closePath(self):
29
+ self.path.closeSubpath()
venv/Lib/site-packages/fontTools/pens/qu2cuPen.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2016 Google Inc. All Rights Reserved.
2
+ # Copyright 2023 Behdad Esfahbod. All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from fontTools.qu2cu import quadratic_to_curves
17
+ from fontTools.pens.filterPen import ContourFilterPen
18
+ from fontTools.pens.reverseContourPen import ReverseContourPen
19
+ import math
20
+
21
+
22
+ class Qu2CuPen(ContourFilterPen):
23
+ """A filter pen to convert quadratic bezier splines to cubic curves
24
+ using the FontTools SegmentPen protocol.
25
+
26
+ Args:
27
+
28
+ other_pen: another SegmentPen used to draw the transformed outline.
29
+ max_err: maximum approximation error in font units. For optimal results,
30
+ if you know the UPEM of the font, we recommend setting this to a
31
+ value equal, or close to UPEM / 1000.
32
+ reverse_direction: flip the contours' direction but keep starting point.
33
+ stats: a dictionary counting the point numbers of cubic segments.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ other_pen,
39
+ max_err,
40
+ all_cubic=False,
41
+ reverse_direction=False,
42
+ stats=None,
43
+ ):
44
+ if reverse_direction:
45
+ other_pen = ReverseContourPen(other_pen)
46
+ super().__init__(other_pen)
47
+ self.all_cubic = all_cubic
48
+ self.max_err = max_err
49
+ self.stats = stats
50
+
51
+ def _quadratics_to_curve(self, q):
52
+ curves = quadratic_to_curves(q, self.max_err, all_cubic=self.all_cubic)
53
+ if self.stats is not None:
54
+ for curve in curves:
55
+ n = str(len(curve) - 2)
56
+ self.stats[n] = self.stats.get(n, 0) + 1
57
+ for curve in curves:
58
+ if len(curve) == 4:
59
+ yield ("curveTo", curve[1:])
60
+ else:
61
+ yield ("qCurveTo", curve[1:])
62
+
63
+ def filterContour(self, contour):
64
+ quadratics = []
65
+ currentPt = None
66
+ newContour = []
67
+ for op, args in contour:
68
+ if op == "qCurveTo" and (
69
+ self.all_cubic or (len(args) > 2 and args[-1] is not None)
70
+ ):
71
+ if args[-1] is None:
72
+ raise NotImplementedError(
73
+ "oncurve-less contours with all_cubic not implemented"
74
+ )
75
+ quadratics.append((currentPt,) + args)
76
+ else:
77
+ if quadratics:
78
+ newContour.extend(self._quadratics_to_curve(quadratics))
79
+ quadratics = []
80
+ newContour.append((op, args))
81
+ currentPt = args[-1] if args else None
82
+ if quadratics:
83
+ newContour.extend(self._quadratics_to_curve(quadratics))
84
+
85
+ if not self.all_cubic:
86
+ # Add back implicit oncurve points
87
+ contour = newContour
88
+ newContour = []
89
+ for op, args in contour:
90
+ if op == "qCurveTo" and newContour and newContour[-1][0] == "qCurveTo":
91
+ pt0 = newContour[-1][1][-2]
92
+ pt1 = newContour[-1][1][-1]
93
+ pt2 = args[0]
94
+ if (
95
+ pt1 is not None
96
+ and math.isclose(pt2[0] - pt1[0], pt1[0] - pt0[0])
97
+ and math.isclose(pt2[1] - pt1[1], pt1[1] - pt0[1])
98
+ ):
99
+ newArgs = newContour[-1][1][:-1] + args
100
+ newContour[-1] = (op, newArgs)
101
+ continue
102
+
103
+ newContour.append((op, args))
104
+
105
+ return newContour
venv/Lib/site-packages/fontTools/pens/quartzPen.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.pens.basePen import BasePen
2
+
3
+ from Quartz.CoreGraphics import CGPathCreateMutable, CGPathMoveToPoint
4
+ from Quartz.CoreGraphics import CGPathAddLineToPoint, CGPathAddCurveToPoint
5
+ from Quartz.CoreGraphics import CGPathAddQuadCurveToPoint, CGPathCloseSubpath
6
+
7
+
8
+ __all__ = ["QuartzPen"]
9
+
10
+
11
+ class QuartzPen(BasePen):
12
+ """A pen that creates a CGPath
13
+
14
+ Parameters
15
+ - path: an optional CGPath to add to
16
+ - xform: an optional CGAffineTransform to apply to the path
17
+ """
18
+
19
+ def __init__(self, glyphSet, path=None, xform=None):
20
+ BasePen.__init__(self, glyphSet)
21
+ if path is None:
22
+ path = CGPathCreateMutable()
23
+ self.path = path
24
+ self.xform = xform
25
+
26
+ def _moveTo(self, pt):
27
+ x, y = pt
28
+ CGPathMoveToPoint(self.path, self.xform, x, y)
29
+
30
+ def _lineTo(self, pt):
31
+ x, y = pt
32
+ CGPathAddLineToPoint(self.path, self.xform, x, y)
33
+
34
+ def _curveToOne(self, p1, p2, p3):
35
+ (x1, y1), (x2, y2), (x3, y3) = p1, p2, p3
36
+ CGPathAddCurveToPoint(self.path, self.xform, x1, y1, x2, y2, x3, y3)
37
+
38
+ def _qCurveToOne(self, p1, p2):
39
+ (x1, y1), (x2, y2) = p1, p2
40
+ CGPathAddQuadCurveToPoint(self.path, self.xform, x1, y1, x2, y2)
41
+
42
+ def _closePath(self):
43
+ CGPathCloseSubpath(self.path)
venv/Lib/site-packages/fontTools/pens/recordingPen.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pen recording operations that can be accessed or replayed."""
2
+
3
+ from fontTools.pens.basePen import AbstractPen, DecomposingPen
4
+ from fontTools.pens.pointPen import AbstractPointPen, DecomposingPointPen
5
+
6
+
7
+ __all__ = [
8
+ "replayRecording",
9
+ "RecordingPen",
10
+ "DecomposingRecordingPen",
11
+ "DecomposingRecordingPointPen",
12
+ "RecordingPointPen",
13
+ "lerpRecordings",
14
+ ]
15
+
16
+
17
+ def replayRecording(recording, pen):
18
+ """Replay a recording, as produced by RecordingPen or DecomposingRecordingPen,
19
+ to a pen.
20
+
21
+ Note that recording does not have to be produced by those pens.
22
+ It can be any iterable of tuples of method name and tuple-of-arguments.
23
+ Likewise, pen can be any objects receiving those method calls.
24
+ """
25
+ for operator, operands in recording:
26
+ getattr(pen, operator)(*operands)
27
+
28
+
29
+ class RecordingPen(AbstractPen):
30
+ """Pen recording operations that can be accessed or replayed.
31
+
32
+ The recording can be accessed as pen.value; or replayed using
33
+ pen.replay(otherPen).
34
+
35
+ :Example:
36
+ .. code-block::
37
+
38
+ from fontTools.ttLib import TTFont
39
+ from fontTools.pens.recordingPen import RecordingPen
40
+
41
+ glyph_name = 'dollar'
42
+ font_path = 'MyFont.otf'
43
+
44
+ font = TTFont(font_path)
45
+ glyphset = font.getGlyphSet()
46
+ glyph = glyphset[glyph_name]
47
+
48
+ pen = RecordingPen()
49
+ glyph.draw(pen)
50
+ print(pen.value)
51
+ """
52
+
53
+ def __init__(self):
54
+ self.value = []
55
+
56
+ def moveTo(self, p0):
57
+ self.value.append(("moveTo", (p0,)))
58
+
59
+ def lineTo(self, p1):
60
+ self.value.append(("lineTo", (p1,)))
61
+
62
+ def qCurveTo(self, *points):
63
+ self.value.append(("qCurveTo", points))
64
+
65
+ def curveTo(self, *points):
66
+ self.value.append(("curveTo", points))
67
+
68
+ def closePath(self):
69
+ self.value.append(("closePath", ()))
70
+
71
+ def endPath(self):
72
+ self.value.append(("endPath", ()))
73
+
74
+ def addComponent(self, glyphName, transformation):
75
+ self.value.append(("addComponent", (glyphName, transformation)))
76
+
77
+ def addVarComponent(self, glyphName, transformation, location):
78
+ self.value.append(("addVarComponent", (glyphName, transformation, location)))
79
+
80
+ def replay(self, pen):
81
+ replayRecording(self.value, pen)
82
+
83
+ draw = replay
84
+
85
+
86
+ class DecomposingRecordingPen(DecomposingPen, RecordingPen):
87
+ """Same as RecordingPen, except that it doesn't keep components
88
+ as references, but draws them decomposed as regular contours.
89
+
90
+ The constructor takes a required 'glyphSet' positional argument,
91
+ a dictionary of glyph objects (i.e. with a 'draw' method) keyed
92
+ by thir name; other arguments are forwarded to the DecomposingPen's
93
+ constructor::
94
+
95
+ >>> class SimpleGlyph(object):
96
+ ... def draw(self, pen):
97
+ ... pen.moveTo((0, 0))
98
+ ... pen.curveTo((1, 1), (2, 2), (3, 3))
99
+ ... pen.closePath()
100
+ >>> class CompositeGlyph(object):
101
+ ... def draw(self, pen):
102
+ ... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
103
+ >>> class MissingComponent(object):
104
+ ... def draw(self, pen):
105
+ ... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0))
106
+ >>> class FlippedComponent(object):
107
+ ... def draw(self, pen):
108
+ ... pen.addComponent('a', (-1, 0, 0, 1, 0, 0))
109
+ >>> glyphSet = {
110
+ ... 'a': SimpleGlyph(),
111
+ ... 'b': CompositeGlyph(),
112
+ ... 'c': MissingComponent(),
113
+ ... 'd': FlippedComponent(),
114
+ ... }
115
+ >>> for name, glyph in sorted(glyphSet.items()):
116
+ ... pen = DecomposingRecordingPen(glyphSet)
117
+ ... try:
118
+ ... glyph.draw(pen)
119
+ ... except pen.MissingComponentError:
120
+ ... pass
121
+ ... print("{}: {}".format(name, pen.value))
122
+ a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
123
+ b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
124
+ c: []
125
+ d: [('moveTo', ((0, 0),)), ('curveTo', ((-1, 1), (-2, 2), (-3, 3))), ('closePath', ())]
126
+
127
+ >>> for name, glyph in sorted(glyphSet.items()):
128
+ ... pen = DecomposingRecordingPen(
129
+ ... glyphSet, skipMissingComponents=True, reverseFlipped=True,
130
+ ... )
131
+ ... glyph.draw(pen)
132
+ ... print("{}: {}".format(name, pen.value))
133
+ a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
134
+ b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
135
+ c: []
136
+ d: [('moveTo', ((0, 0),)), ('lineTo', ((-3, 3),)), ('curveTo', ((-2, 2), (-1, 1), (0, 0))), ('closePath', ())]
137
+ """
138
+
139
+ # raises MissingComponentError(KeyError) if base glyph is not found in glyphSet
140
+ skipMissingComponents = False
141
+
142
+
143
+ class RecordingPointPen(AbstractPointPen):
144
+ """PointPen recording operations that can be accessed or replayed.
145
+
146
+ The recording can be accessed as pen.value; or replayed using
147
+ pointPen.replay(otherPointPen).
148
+
149
+ :Example:
150
+ .. code-block::
151
+
152
+ from defcon import Font
153
+ from fontTools.pens.recordingPen import RecordingPointPen
154
+
155
+ glyph_name = 'a'
156
+ font_path = 'MyFont.ufo'
157
+
158
+ font = Font(font_path)
159
+ glyph = font[glyph_name]
160
+
161
+ pen = RecordingPointPen()
162
+ glyph.drawPoints(pen)
163
+ print(pen.value)
164
+
165
+ new_glyph = font.newGlyph('b')
166
+ pen.replay(new_glyph.getPointPen())
167
+ """
168
+
169
+ def __init__(self):
170
+ self.value = []
171
+
172
+ def beginPath(self, identifier=None, **kwargs):
173
+ if identifier is not None:
174
+ kwargs["identifier"] = identifier
175
+ self.value.append(("beginPath", (), kwargs))
176
+
177
+ def endPath(self):
178
+ self.value.append(("endPath", (), {}))
179
+
180
+ def addPoint(
181
+ self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
182
+ ):
183
+ if identifier is not None:
184
+ kwargs["identifier"] = identifier
185
+ self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs))
186
+
187
+ def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
188
+ if identifier is not None:
189
+ kwargs["identifier"] = identifier
190
+ self.value.append(("addComponent", (baseGlyphName, transformation), kwargs))
191
+
192
+ def addVarComponent(
193
+ self, baseGlyphName, transformation, location, identifier=None, **kwargs
194
+ ):
195
+ if identifier is not None:
196
+ kwargs["identifier"] = identifier
197
+ self.value.append(
198
+ ("addVarComponent", (baseGlyphName, transformation, location), kwargs)
199
+ )
200
+
201
+ def replay(self, pointPen):
202
+ for operator, args, kwargs in self.value:
203
+ getattr(pointPen, operator)(*args, **kwargs)
204
+
205
+ drawPoints = replay
206
+
207
+
208
+ class DecomposingRecordingPointPen(DecomposingPointPen, RecordingPointPen):
209
+ """Same as RecordingPointPen, except that it doesn't keep components
210
+ as references, but draws them decomposed as regular contours.
211
+
212
+ The constructor takes a required 'glyphSet' positional argument,
213
+ a dictionary of pointPen-drawable glyph objects (i.e. with a 'drawPoints' method)
214
+ keyed by thir name; other arguments are forwarded to the DecomposingPointPen's
215
+ constructor::
216
+
217
+ >>> from pprint import pprint
218
+ >>> class SimpleGlyph(object):
219
+ ... def drawPoints(self, pen):
220
+ ... pen.beginPath()
221
+ ... pen.addPoint((0, 0), "line")
222
+ ... pen.addPoint((1, 1))
223
+ ... pen.addPoint((2, 2))
224
+ ... pen.addPoint((3, 3), "curve")
225
+ ... pen.endPath()
226
+ >>> class CompositeGlyph(object):
227
+ ... def drawPoints(self, pen):
228
+ ... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
229
+ >>> class MissingComponent(object):
230
+ ... def drawPoints(self, pen):
231
+ ... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0))
232
+ >>> class FlippedComponent(object):
233
+ ... def drawPoints(self, pen):
234
+ ... pen.addComponent('a', (-1, 0, 0, 1, 0, 0))
235
+ >>> glyphSet = {
236
+ ... 'a': SimpleGlyph(),
237
+ ... 'b': CompositeGlyph(),
238
+ ... 'c': MissingComponent(),
239
+ ... 'd': FlippedComponent(),
240
+ ... }
241
+ >>> for name, glyph in sorted(glyphSet.items()):
242
+ ... pen = DecomposingRecordingPointPen(glyphSet)
243
+ ... try:
244
+ ... glyph.drawPoints(pen)
245
+ ... except pen.MissingComponentError:
246
+ ... pass
247
+ ... pprint({name: pen.value})
248
+ {'a': [('beginPath', (), {}),
249
+ ('addPoint', ((0, 0), 'line', False, None), {}),
250
+ ('addPoint', ((1, 1), None, False, None), {}),
251
+ ('addPoint', ((2, 2), None, False, None), {}),
252
+ ('addPoint', ((3, 3), 'curve', False, None), {}),
253
+ ('endPath', (), {})]}
254
+ {'b': [('beginPath', (), {}),
255
+ ('addPoint', ((-1, 1), 'line', False, None), {}),
256
+ ('addPoint', ((0, 2), None, False, None), {}),
257
+ ('addPoint', ((1, 3), None, False, None), {}),
258
+ ('addPoint', ((2, 4), 'curve', False, None), {}),
259
+ ('endPath', (), {})]}
260
+ {'c': []}
261
+ {'d': [('beginPath', (), {}),
262
+ ('addPoint', ((0, 0), 'line', False, None), {}),
263
+ ('addPoint', ((-1, 1), None, False, None), {}),
264
+ ('addPoint', ((-2, 2), None, False, None), {}),
265
+ ('addPoint', ((-3, 3), 'curve', False, None), {}),
266
+ ('endPath', (), {})]}
267
+
268
+ >>> for name, glyph in sorted(glyphSet.items()):
269
+ ... pen = DecomposingRecordingPointPen(
270
+ ... glyphSet, skipMissingComponents=True, reverseFlipped=True,
271
+ ... )
272
+ ... glyph.drawPoints(pen)
273
+ ... pprint({name: pen.value})
274
+ {'a': [('beginPath', (), {}),
275
+ ('addPoint', ((0, 0), 'line', False, None), {}),
276
+ ('addPoint', ((1, 1), None, False, None), {}),
277
+ ('addPoint', ((2, 2), None, False, None), {}),
278
+ ('addPoint', ((3, 3), 'curve', False, None), {}),
279
+ ('endPath', (), {})]}
280
+ {'b': [('beginPath', (), {}),
281
+ ('addPoint', ((-1, 1), 'line', False, None), {}),
282
+ ('addPoint', ((0, 2), None, False, None), {}),
283
+ ('addPoint', ((1, 3), None, False, None), {}),
284
+ ('addPoint', ((2, 4), 'curve', False, None), {}),
285
+ ('endPath', (), {})]}
286
+ {'c': []}
287
+ {'d': [('beginPath', (), {}),
288
+ ('addPoint', ((0, 0), 'curve', False, None), {}),
289
+ ('addPoint', ((-3, 3), 'line', False, None), {}),
290
+ ('addPoint', ((-2, 2), None, False, None), {}),
291
+ ('addPoint', ((-1, 1), None, False, None), {}),
292
+ ('endPath', (), {})]}
293
+ """
294
+
295
+ # raises MissingComponentError(KeyError) if base glyph is not found in glyphSet
296
+ skipMissingComponents = False
297
+
298
+
299
+ def lerpRecordings(recording1, recording2, factor=0.5):
300
+ """Linearly interpolate between two recordings. The recordings
301
+ must be decomposed, i.e. they must not contain any components.
302
+
303
+ Factor is typically between 0 and 1. 0 means the first recording,
304
+ 1 means the second recording, and 0.5 means the average of the
305
+ two recordings. Other values are possible, and can be useful to
306
+ extrapolate. Defaults to 0.5.
307
+
308
+ Returns a generator with the new recording.
309
+ """
310
+ if len(recording1) != len(recording2):
311
+ raise ValueError(
312
+ "Mismatched lengths: %d and %d" % (len(recording1), len(recording2))
313
+ )
314
+ for (op1, args1), (op2, args2) in zip(recording1, recording2):
315
+ if op1 != op2:
316
+ raise ValueError("Mismatched operations: %s, %s" % (op1, op2))
317
+ if op1 == "addComponent":
318
+ raise ValueError("Cannot interpolate components")
319
+ else:
320
+ mid_args = [
321
+ (x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
322
+ for (x1, y1), (x2, y2) in zip(args1, args2)
323
+ ]
324
+ yield (op1, mid_args)
325
+
326
+
327
+ if __name__ == "__main__":
328
+ pen = RecordingPen()
329
+ pen.moveTo((0, 0))
330
+ pen.lineTo((0, 100))
331
+ pen.curveTo((50, 75), (60, 50), (50, 25))
332
+ pen.closePath()
333
+ from pprint import pprint
334
+
335
+ pprint(pen.value)
venv/Lib/site-packages/fontTools/pens/reportLabPen.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.pens.basePen import BasePen
2
+ from reportlab.graphics.shapes import Path
3
+
4
+
5
+ __all__ = ["ReportLabPen"]
6
+
7
+
8
+ class ReportLabPen(BasePen):
9
+ """A pen for drawing onto a ``reportlab.graphics.shapes.Path`` object."""
10
+
11
+ def __init__(self, glyphSet, path=None):
12
+ BasePen.__init__(self, glyphSet)
13
+ if path is None:
14
+ path = Path()
15
+ self.path = path
16
+
17
+ def _moveTo(self, p):
18
+ (x, y) = p
19
+ self.path.moveTo(x, y)
20
+
21
+ def _lineTo(self, p):
22
+ (x, y) = p
23
+ self.path.lineTo(x, y)
24
+
25
+ def _curveToOne(self, p1, p2, p3):
26
+ (x1, y1) = p1
27
+ (x2, y2) = p2
28
+ (x3, y3) = p3
29
+ self.path.curveTo(x1, y1, x2, y2, x3, y3)
30
+
31
+ def _closePath(self):
32
+ self.path.closePath()
33
+
34
+
35
+ if __name__ == "__main__":
36
+ import sys
37
+
38
+ if len(sys.argv) < 3:
39
+ print(
40
+ "Usage: reportLabPen.py <OTF/TTF font> <glyphname> [<image file to create>]"
41
+ )
42
+ print(
43
+ " If no image file name is created, by default <glyphname>.png is created."
44
+ )
45
+ print(" example: reportLabPen.py Arial.TTF R test.png")
46
+ print(
47
+ " (The file format will be PNG, regardless of the image file name supplied)"
48
+ )
49
+ sys.exit(0)
50
+
51
+ from fontTools.ttLib import TTFont
52
+ from reportlab.lib import colors
53
+
54
+ path = sys.argv[1]
55
+ glyphName = sys.argv[2]
56
+ if len(sys.argv) > 3:
57
+ imageFile = sys.argv[3]
58
+ else:
59
+ imageFile = "%s.png" % glyphName
60
+
61
+ font = TTFont(path) # it would work just as well with fontTools.t1Lib.T1Font
62
+ gs = font.getGlyphSet()
63
+ pen = ReportLabPen(gs, Path(fillColor=colors.red, strokeWidth=5))
64
+ g = gs[glyphName]
65
+ g.draw(pen)
66
+
67
+ w, h = g.width, 1000
68
+ from reportlab.graphics import renderPM
69
+ from reportlab.graphics.shapes import Group, Drawing, scale
70
+
71
+ # Everything is wrapped in a group to allow transformations.
72
+ g = Group(pen.path)
73
+ g.translate(0, 200)
74
+ g.scale(0.3, 0.3)
75
+
76
+ d = Drawing(w, h)
77
+ d.add(g)
78
+
79
+ renderPM.drawToFile(d, imageFile, fmt="PNG")
venv/Lib/site-packages/fontTools/pens/reverseContourPen.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.misc.arrayTools import pairwise
2
+ from fontTools.pens.filterPen import ContourFilterPen
3
+
4
+
5
+ __all__ = ["reversedContour", "ReverseContourPen"]
6
+
7
+
8
+ class ReverseContourPen(ContourFilterPen):
9
+ """Filter pen that passes outline data to another pen, but reversing
10
+ the winding direction of all contours. Components are simply passed
11
+ through unchanged.
12
+
13
+ Closed contours are reversed in such a way that the first point remains
14
+ the first point.
15
+ """
16
+
17
+ def __init__(self, outPen, outputImpliedClosingLine=False):
18
+ super().__init__(outPen)
19
+ self.outputImpliedClosingLine = outputImpliedClosingLine
20
+
21
+ def filterContour(self, contour):
22
+ return reversedContour(contour, self.outputImpliedClosingLine)
23
+
24
+
25
+ def reversedContour(contour, outputImpliedClosingLine=False):
26
+ """Generator that takes a list of pen's (operator, operands) tuples,
27
+ and yields them with the winding direction reversed.
28
+ """
29
+ if not contour:
30
+ return # nothing to do, stop iteration
31
+
32
+ # valid contours must have at least a starting and ending command,
33
+ # can't have one without the other
34
+ assert len(contour) > 1, "invalid contour"
35
+
36
+ # the type of the last command determines if the contour is closed
37
+ contourType = contour.pop()[0]
38
+ assert contourType in ("endPath", "closePath")
39
+ closed = contourType == "closePath"
40
+
41
+ firstType, firstPts = contour.pop(0)
42
+ assert firstType in ("moveTo", "qCurveTo"), (
43
+ "invalid initial segment type: %r" % firstType
44
+ )
45
+ firstOnCurve = firstPts[-1]
46
+ if firstType == "qCurveTo":
47
+ # special case for TrueType paths contaning only off-curve points
48
+ assert firstOnCurve is None, "off-curve only paths must end with 'None'"
49
+ assert not contour, "only one qCurveTo allowed per off-curve path"
50
+ firstPts = (firstPts[0],) + tuple(reversed(firstPts[1:-1])) + (None,)
51
+
52
+ if not contour:
53
+ # contour contains only one segment, nothing to reverse
54
+ if firstType == "moveTo":
55
+ closed = False # single-point paths can't be closed
56
+ else:
57
+ closed = True # off-curve paths are closed by definition
58
+ yield firstType, firstPts
59
+ else:
60
+ lastType, lastPts = contour[-1]
61
+ lastOnCurve = lastPts[-1]
62
+ if closed:
63
+ # for closed paths, we keep the starting point
64
+ yield firstType, firstPts
65
+ if firstOnCurve != lastOnCurve:
66
+ # emit an implied line between the last and first points
67
+ yield "lineTo", (lastOnCurve,)
68
+ contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,))
69
+
70
+ if len(contour) > 1:
71
+ secondType, secondPts = contour[0]
72
+ else:
73
+ # contour has only two points, the second and last are the same
74
+ secondType, secondPts = lastType, lastPts
75
+
76
+ if not outputImpliedClosingLine:
77
+ # if a lineTo follows the initial moveTo, after reversing it
78
+ # will be implied by the closePath, so we don't emit one;
79
+ # unless the lineTo and moveTo overlap, in which case we keep the
80
+ # duplicate points
81
+ if secondType == "lineTo" and firstPts != secondPts:
82
+ del contour[0]
83
+ if contour:
84
+ contour[-1] = (lastType, tuple(lastPts[:-1]) + secondPts)
85
+ else:
86
+ # for open paths, the last point will become the first
87
+ yield firstType, (lastOnCurve,)
88
+ contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,))
89
+
90
+ # we iterate over all segment pairs in reverse order, and yield
91
+ # each one with the off-curve points reversed (if any), and
92
+ # with the on-curve point of the following segment
93
+ for (curType, curPts), (_, nextPts) in pairwise(contour, reverse=True):
94
+ yield curType, tuple(reversed(curPts[:-1])) + (nextPts[-1],)
95
+
96
+ yield "closePath" if closed else "endPath", ()
venv/Lib/site-packages/fontTools/pens/roundingPen.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.misc.roundTools import noRound, otRound
2
+ from fontTools.misc.transform import Transform
3
+ from fontTools.pens.filterPen import FilterPen, FilterPointPen
4
+
5
+
6
+ __all__ = ["RoundingPen", "RoundingPointPen"]
7
+
8
+
9
+ class RoundingPen(FilterPen):
10
+ """
11
+ Filter pen that rounds point coordinates and component XY offsets to integer. For
12
+ rounding the component transform values, a separate round function can be passed to
13
+ the pen.
14
+
15
+ >>> from fontTools.pens.recordingPen import RecordingPen
16
+ >>> recpen = RecordingPen()
17
+ >>> roundpen = RoundingPen(recpen)
18
+ >>> roundpen.moveTo((0.4, 0.6))
19
+ >>> roundpen.lineTo((1.6, 2.5))
20
+ >>> roundpen.qCurveTo((2.4, 4.6), (3.3, 5.7), (4.9, 6.1))
21
+ >>> roundpen.curveTo((6.4, 8.6), (7.3, 9.7), (8.9, 10.1))
22
+ >>> roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5))
23
+ >>> recpen.value == [
24
+ ... ('moveTo', ((0, 1),)),
25
+ ... ('lineTo', ((2, 3),)),
26
+ ... ('qCurveTo', ((2, 5), (3, 6), (5, 6))),
27
+ ... ('curveTo', ((6, 9), (7, 10), (9, 10))),
28
+ ... ('addComponent', ('a', (1.5, 0, 0, 1.5, 11, -10))),
29
+ ... ]
30
+ True
31
+ """
32
+
33
+ def __init__(self, outPen, roundFunc=otRound, transformRoundFunc=noRound):
34
+ super().__init__(outPen)
35
+ self.roundFunc = roundFunc
36
+ self.transformRoundFunc = transformRoundFunc
37
+
38
+ def moveTo(self, pt):
39
+ self._outPen.moveTo((self.roundFunc(pt[0]), self.roundFunc(pt[1])))
40
+
41
+ def lineTo(self, pt):
42
+ self._outPen.lineTo((self.roundFunc(pt[0]), self.roundFunc(pt[1])))
43
+
44
+ def curveTo(self, *points):
45
+ self._outPen.curveTo(
46
+ *((self.roundFunc(x), self.roundFunc(y)) for x, y in points)
47
+ )
48
+
49
+ def qCurveTo(self, *points):
50
+ self._outPen.qCurveTo(
51
+ *((self.roundFunc(x), self.roundFunc(y)) for x, y in points)
52
+ )
53
+
54
+ def addComponent(self, glyphName, transformation):
55
+ xx, xy, yx, yy, dx, dy = transformation
56
+ self._outPen.addComponent(
57
+ glyphName,
58
+ Transform(
59
+ self.transformRoundFunc(xx),
60
+ self.transformRoundFunc(xy),
61
+ self.transformRoundFunc(yx),
62
+ self.transformRoundFunc(yy),
63
+ self.roundFunc(dx),
64
+ self.roundFunc(dy),
65
+ ),
66
+ )
67
+
68
+
69
+ class RoundingPointPen(FilterPointPen):
70
+ """
71
+ Filter point pen that rounds point coordinates and component XY offsets to integer.
72
+ For rounding the component scale values, a separate round function can be passed to
73
+ the pen.
74
+
75
+ >>> from fontTools.pens.recordingPen import RecordingPointPen
76
+ >>> recpen = RecordingPointPen()
77
+ >>> roundpen = RoundingPointPen(recpen)
78
+ >>> roundpen.beginPath()
79
+ >>> roundpen.addPoint((0.4, 0.6), 'line')
80
+ >>> roundpen.addPoint((1.6, 2.5), 'line')
81
+ >>> roundpen.addPoint((2.4, 4.6))
82
+ >>> roundpen.addPoint((3.3, 5.7))
83
+ >>> roundpen.addPoint((4.9, 6.1), 'qcurve')
84
+ >>> roundpen.endPath()
85
+ >>> roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5))
86
+ >>> recpen.value == [
87
+ ... ('beginPath', (), {}),
88
+ ... ('addPoint', ((0, 1), 'line', False, None), {}),
89
+ ... ('addPoint', ((2, 3), 'line', False, None), {}),
90
+ ... ('addPoint', ((2, 5), None, False, None), {}),
91
+ ... ('addPoint', ((3, 6), None, False, None), {}),
92
+ ... ('addPoint', ((5, 6), 'qcurve', False, None), {}),
93
+ ... ('endPath', (), {}),
94
+ ... ('addComponent', ('a', (1.5, 0, 0, 1.5, 11, -10)), {}),
95
+ ... ]
96
+ True
97
+ """
98
+
99
+ def __init__(self, outPen, roundFunc=otRound, transformRoundFunc=noRound):
100
+ super().__init__(outPen)
101
+ self.roundFunc = roundFunc
102
+ self.transformRoundFunc = transformRoundFunc
103
+
104
+ def addPoint(
105
+ self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
106
+ ):
107
+ self._outPen.addPoint(
108
+ (self.roundFunc(pt[0]), self.roundFunc(pt[1])),
109
+ segmentType=segmentType,
110
+ smooth=smooth,
111
+ name=name,
112
+ identifier=identifier,
113
+ **kwargs,
114
+ )
115
+
116
+ def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
117
+ xx, xy, yx, yy, dx, dy = transformation
118
+ self._outPen.addComponent(
119
+ baseGlyphName,
120
+ Transform(
121
+ self.transformRoundFunc(xx),
122
+ self.transformRoundFunc(xy),
123
+ self.transformRoundFunc(yx),
124
+ self.transformRoundFunc(yy),
125
+ self.roundFunc(dx),
126
+ self.roundFunc(dy),
127
+ ),
128
+ identifier=identifier,
129
+ **kwargs,
130
+ )
venv/Lib/site-packages/fontTools/pens/statisticsPen.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pen calculating area, center of mass, variance and standard-deviation,
2
+ covariance and correlation, and slant, of glyph shapes."""
3
+
4
+ from math import sqrt, degrees, atan
5
+ from fontTools.pens.basePen import BasePen, OpenContourError
6
+ from fontTools.pens.momentsPen import MomentsPen
7
+
8
+ __all__ = ["StatisticsPen", "StatisticsControlPen"]
9
+
10
+
11
+ class StatisticsBase:
12
+ def __init__(self):
13
+ self._zero()
14
+
15
+ def _zero(self):
16
+ self.area = 0
17
+ self.meanX = 0
18
+ self.meanY = 0
19
+ self.varianceX = 0
20
+ self.varianceY = 0
21
+ self.stddevX = 0
22
+ self.stddevY = 0
23
+ self.covariance = 0
24
+ self.correlation = 0
25
+ self.slant = 0
26
+
27
+ def _update(self):
28
+ # XXX The variance formulas should never produce a negative value,
29
+ # but due to reasons I don't understand, both of our pens do.
30
+ # So we take the absolute value here.
31
+ self.varianceX = abs(self.varianceX)
32
+ self.varianceY = abs(self.varianceY)
33
+
34
+ self.stddevX = stddevX = sqrt(self.varianceX)
35
+ self.stddevY = stddevY = sqrt(self.varianceY)
36
+
37
+ # Correlation(X,Y) = Covariance(X,Y) / ( stddev(X) * stddev(Y) )
38
+ # https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient
39
+ if stddevX * stddevY == 0:
40
+ correlation = float("NaN")
41
+ else:
42
+ # XXX The above formula should never produce a value outside
43
+ # the range [-1, 1], but due to reasons I don't understand,
44
+ # (probably the same issue as above), it does. So we clamp.
45
+ correlation = self.covariance / (stddevX * stddevY)
46
+ correlation = max(-1, min(1, correlation))
47
+ self.correlation = correlation if abs(correlation) > 1e-3 else 0
48
+
49
+ slant = (
50
+ self.covariance / self.varianceY if self.varianceY != 0 else float("NaN")
51
+ )
52
+ self.slant = slant if abs(slant) > 1e-3 else 0
53
+
54
+
55
+ class StatisticsPen(StatisticsBase, MomentsPen):
56
+ """Pen calculating area, center of mass, variance and
57
+ standard-deviation, covariance and correlation, and slant,
58
+ of glyph shapes.
59
+
60
+ Note that if the glyph shape is self-intersecting, the values
61
+ are not correct (but well-defined). Moreover, area will be
62
+ negative if contour directions are clockwise."""
63
+
64
+ def __init__(self, glyphset=None):
65
+ MomentsPen.__init__(self, glyphset=glyphset)
66
+ StatisticsBase.__init__(self)
67
+
68
+ def _closePath(self):
69
+ MomentsPen._closePath(self)
70
+ self._update()
71
+
72
+ def _update(self):
73
+ area = self.area
74
+ if not area:
75
+ self._zero()
76
+ return
77
+
78
+ # Center of mass
79
+ # https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume
80
+ self.meanX = meanX = self.momentX / area
81
+ self.meanY = meanY = self.momentY / area
82
+
83
+ # Var(X) = E[X^2] - E[X]^2
84
+ self.varianceX = self.momentXX / area - meanX * meanX
85
+ self.varianceY = self.momentYY / area - meanY * meanY
86
+
87
+ # Covariance(X,Y) = (E[X.Y] - E[X]E[Y])
88
+ self.covariance = self.momentXY / area - meanX * meanY
89
+
90
+ StatisticsBase._update(self)
91
+
92
+
93
+ class StatisticsControlPen(StatisticsBase, BasePen):
94
+ """Pen calculating area, center of mass, variance and
95
+ standard-deviation, covariance and correlation, and slant,
96
+ of glyph shapes, using the control polygon only.
97
+
98
+ Note that if the glyph shape is self-intersecting, the values
99
+ are not correct (but well-defined). Moreover, area will be
100
+ negative if contour directions are clockwise."""
101
+
102
+ def __init__(self, glyphset=None):
103
+ BasePen.__init__(self, glyphset)
104
+ StatisticsBase.__init__(self)
105
+ self._nodes = []
106
+
107
+ def _moveTo(self, pt):
108
+ self._nodes.append(complex(*pt))
109
+ self._startPoint = pt
110
+
111
+ def _lineTo(self, pt):
112
+ self._nodes.append(complex(*pt))
113
+
114
+ def _qCurveToOne(self, pt1, pt2):
115
+ for pt in (pt1, pt2):
116
+ self._nodes.append(complex(*pt))
117
+
118
+ def _curveToOne(self, pt1, pt2, pt3):
119
+ for pt in (pt1, pt2, pt3):
120
+ self._nodes.append(complex(*pt))
121
+
122
+ def _closePath(self):
123
+ p0 = self._getCurrentPoint()
124
+ if p0 != self._startPoint:
125
+ self._lineTo(self._startPoint)
126
+ self._update()
127
+
128
+ def _endPath(self):
129
+ p0 = self._getCurrentPoint()
130
+ if p0 != self._startPoint:
131
+ raise OpenContourError("Glyph statistics not defined on open contours.")
132
+ self._update()
133
+
134
+ def _update(self):
135
+ nodes = self._nodes
136
+ n = len(nodes)
137
+
138
+ # Triangle formula
139
+ self.area = (
140
+ sum(
141
+ (p0.real * p1.imag - p1.real * p0.imag)
142
+ for p0, p1 in zip(nodes, nodes[1:] + nodes[:1])
143
+ )
144
+ / 2
145
+ )
146
+
147
+ # Center of mass
148
+ # https://en.wikipedia.org/wiki/Center_of_mass#A_system_of_particles
149
+ sumNodes = sum(nodes)
150
+ self.meanX = meanX = sumNodes.real / n
151
+ self.meanY = meanY = sumNodes.imag / n
152
+
153
+ if n > 1:
154
+ # Var(X) = (sum[X^2] - sum[X]^2 / n) / (n - 1)
155
+ # https://www.statisticshowto.com/probability-and-statistics/descriptive-statistics/sample-variance/
156
+ self.varianceX = varianceX = (
157
+ sum(p.real * p.real for p in nodes)
158
+ - (sumNodes.real * sumNodes.real) / n
159
+ ) / (n - 1)
160
+ self.varianceY = varianceY = (
161
+ sum(p.imag * p.imag for p in nodes)
162
+ - (sumNodes.imag * sumNodes.imag) / n
163
+ ) / (n - 1)
164
+
165
+ # Covariance(X,Y) = (sum[X.Y] - sum[X].sum[Y] / n) / (n - 1)
166
+ self.covariance = covariance = (
167
+ sum(p.real * p.imag for p in nodes)
168
+ - (sumNodes.real * sumNodes.imag) / n
169
+ ) / (n - 1)
170
+ else:
171
+ self.varianceX = varianceX = 0
172
+ self.varianceY = varianceY = 0
173
+ self.covariance = covariance = 0
174
+
175
+ StatisticsBase._update(self)
176
+
177
+
178
+ def _test(glyphset, upem, glyphs, quiet=False, *, control=False):
179
+ from fontTools.pens.transformPen import TransformPen
180
+ from fontTools.misc.transform import Scale
181
+
182
+ wght_sum = 0
183
+ wght_sum_perceptual = 0
184
+ wdth_sum = 0
185
+ slnt_sum = 0
186
+ slnt_sum_perceptual = 0
187
+ for glyph_name in glyphs:
188
+ glyph = glyphset[glyph_name]
189
+ if control:
190
+ pen = StatisticsControlPen(glyphset=glyphset)
191
+ else:
192
+ pen = StatisticsPen(glyphset=glyphset)
193
+ transformer = TransformPen(pen, Scale(1.0 / upem))
194
+ glyph.draw(transformer)
195
+
196
+ area = abs(pen.area)
197
+ width = glyph.width
198
+ wght_sum += area
199
+ wght_sum_perceptual += pen.area * width
200
+ wdth_sum += width
201
+ slnt_sum += pen.slant
202
+ slnt_sum_perceptual += pen.slant * width
203
+
204
+ if quiet:
205
+ continue
206
+
207
+ print()
208
+ print("glyph:", glyph_name)
209
+
210
+ for item in [
211
+ "area",
212
+ "momentX",
213
+ "momentY",
214
+ "momentXX",
215
+ "momentYY",
216
+ "momentXY",
217
+ "meanX",
218
+ "meanY",
219
+ "varianceX",
220
+ "varianceY",
221
+ "stddevX",
222
+ "stddevY",
223
+ "covariance",
224
+ "correlation",
225
+ "slant",
226
+ ]:
227
+ print("%s: %g" % (item, getattr(pen, item)))
228
+
229
+ if not quiet:
230
+ print()
231
+ print("font:")
232
+
233
+ print("weight: %g" % (wght_sum * upem / wdth_sum))
234
+ print("weight (perceptual): %g" % (wght_sum_perceptual / wdth_sum))
235
+ print("width: %g" % (wdth_sum / upem / len(glyphs)))
236
+ slant = slnt_sum / len(glyphs)
237
+ print("slant: %g" % slant)
238
+ print("slant angle: %g" % -degrees(atan(slant)))
239
+ slant_perceptual = slnt_sum_perceptual / wdth_sum
240
+ print("slant (perceptual): %g" % slant_perceptual)
241
+ print("slant (perceptual) angle: %g" % -degrees(atan(slant_perceptual)))
242
+
243
+
244
+ def main(args):
245
+ """Report font glyph shape geometricsl statistics"""
246
+
247
+ if args is None:
248
+ import sys
249
+
250
+ args = sys.argv[1:]
251
+
252
+ import argparse
253
+
254
+ parser = argparse.ArgumentParser(
255
+ "fonttools pens.statisticsPen",
256
+ description="Report font glyph shape geometricsl statistics",
257
+ )
258
+ parser.add_argument("font", metavar="font.ttf", help="Font file.")
259
+ parser.add_argument("glyphs", metavar="glyph-name", help="Glyph names.", nargs="*")
260
+ parser.add_argument(
261
+ "-y",
262
+ metavar="<number>",
263
+ help="Face index into a collection to open. Zero based.",
264
+ )
265
+ parser.add_argument(
266
+ "-c",
267
+ "--control",
268
+ action="store_true",
269
+ help="Use the control-box pen instead of the Green therem.",
270
+ )
271
+ parser.add_argument(
272
+ "-q", "--quiet", action="store_true", help="Only report font-wide statistics."
273
+ )
274
+ parser.add_argument(
275
+ "--variations",
276
+ metavar="AXIS=LOC",
277
+ default="",
278
+ help="List of space separated locations. A location consist in "
279
+ "the name of a variation axis, followed by '=' and a number. E.g.: "
280
+ "wght=700 wdth=80. The default is the location of the base master.",
281
+ )
282
+
283
+ options = parser.parse_args(args)
284
+
285
+ glyphs = options.glyphs
286
+ fontNumber = int(options.y) if options.y is not None else 0
287
+
288
+ location = {}
289
+ for tag_v in options.variations.split():
290
+ fields = tag_v.split("=")
291
+ tag = fields[0].strip()
292
+ v = int(fields[1])
293
+ location[tag] = v
294
+
295
+ from fontTools.ttLib import TTFont
296
+
297
+ font = TTFont(options.font, fontNumber=fontNumber)
298
+ if not glyphs:
299
+ glyphs = font.getGlyphOrder()
300
+ _test(
301
+ font.getGlyphSet(location=location),
302
+ font["head"].unitsPerEm,
303
+ glyphs,
304
+ quiet=options.quiet,
305
+ control=options.control,
306
+ )
307
+
308
+
309
+ if __name__ == "__main__":
310
+ import sys
311
+
312
+ main(sys.argv[1:])
venv/Lib/site-packages/fontTools/pens/svgPathPen.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable
2
+ from fontTools.pens.basePen import BasePen
3
+
4
+
5
+ def pointToString(pt, ntos=str):
6
+ return " ".join(ntos(i) for i in pt)
7
+
8
+
9
+ class SVGPathPen(BasePen):
10
+ """Pen to draw SVG path d commands.
11
+
12
+ Args:
13
+ glyphSet: a dictionary of drawable glyph objects keyed by name
14
+ used to resolve component references in composite glyphs.
15
+ ntos: a callable that takes a number and returns a string, to
16
+ customize how numbers are formatted (default: str).
17
+
18
+ :Example:
19
+ .. code-block::
20
+
21
+ >>> pen = SVGPathPen(None)
22
+ >>> pen.moveTo((0, 0))
23
+ >>> pen.lineTo((1, 1))
24
+ >>> pen.curveTo((2, 2), (3, 3), (4, 4))
25
+ >>> pen.closePath()
26
+ >>> pen.getCommands()
27
+ 'M0 0 1 1C2 2 3 3 4 4Z'
28
+
29
+ Note:
30
+ Fonts have a coordinate system where Y grows up, whereas in SVG,
31
+ Y grows down. As such, rendering path data from this pen in
32
+ SVG typically results in upside-down glyphs. You can fix this
33
+ by wrapping the data from this pen in an SVG group element with
34
+ transform, or wrap this pen in a transform pen. For example:
35
+ .. code-block:: python
36
+
37
+ spen = svgPathPen.SVGPathPen(glyphset)
38
+ pen= TransformPen(spen , (1, 0, 0, -1, 0, 0))
39
+ glyphset[glyphname].draw(pen)
40
+ print(tpen.getCommands())
41
+ """
42
+
43
+ def __init__(self, glyphSet, ntos: Callable[[float], str] = str):
44
+ BasePen.__init__(self, glyphSet)
45
+ self._commands = []
46
+ self._lastCommand = None
47
+ self._lastX = None
48
+ self._lastY = None
49
+ self._ntos = ntos
50
+
51
+ def _handleAnchor(self):
52
+ """
53
+ >>> pen = SVGPathPen(None)
54
+ >>> pen.moveTo((0, 0))
55
+ >>> pen.moveTo((10, 10))
56
+ >>> pen._commands
57
+ ['M10 10']
58
+ """
59
+ if self._lastCommand == "M":
60
+ self._commands.pop(-1)
61
+
62
+ def _moveTo(self, pt):
63
+ """
64
+ >>> pen = SVGPathPen(None)
65
+ >>> pen.moveTo((0, 0))
66
+ >>> pen._commands
67
+ ['M0 0']
68
+
69
+ >>> pen = SVGPathPen(None)
70
+ >>> pen.moveTo((10, 0))
71
+ >>> pen._commands
72
+ ['M10 0']
73
+
74
+ >>> pen = SVGPathPen(None)
75
+ >>> pen.moveTo((0, 10))
76
+ >>> pen._commands
77
+ ['M0 10']
78
+ """
79
+ self._handleAnchor()
80
+ t = "M%s" % (pointToString(pt, self._ntos))
81
+ self._commands.append(t)
82
+ self._lastCommand = "M"
83
+ self._lastX, self._lastY = pt
84
+
85
+ def _lineTo(self, pt):
86
+ """
87
+ # duplicate point
88
+ >>> pen = SVGPathPen(None)
89
+ >>> pen.moveTo((10, 10))
90
+ >>> pen.lineTo((10, 10))
91
+ >>> pen._commands
92
+ ['M10 10']
93
+
94
+ # vertical line
95
+ >>> pen = SVGPathPen(None)
96
+ >>> pen.moveTo((10, 10))
97
+ >>> pen.lineTo((10, 0))
98
+ >>> pen._commands
99
+ ['M10 10', 'V0']
100
+
101
+ # horizontal line
102
+ >>> pen = SVGPathPen(None)
103
+ >>> pen.moveTo((10, 10))
104
+ >>> pen.lineTo((0, 10))
105
+ >>> pen._commands
106
+ ['M10 10', 'H0']
107
+
108
+ # basic
109
+ >>> pen = SVGPathPen(None)
110
+ >>> pen.lineTo((70, 80))
111
+ >>> pen._commands
112
+ ['L70 80']
113
+
114
+ # basic following a moveto
115
+ >>> pen = SVGPathPen(None)
116
+ >>> pen.moveTo((0, 0))
117
+ >>> pen.lineTo((10, 10))
118
+ >>> pen._commands
119
+ ['M0 0', ' 10 10']
120
+ """
121
+ x, y = pt
122
+ # duplicate point
123
+ if x == self._lastX and y == self._lastY:
124
+ return
125
+ # vertical line
126
+ elif x == self._lastX:
127
+ cmd = "V"
128
+ pts = self._ntos(y)
129
+ # horizontal line
130
+ elif y == self._lastY:
131
+ cmd = "H"
132
+ pts = self._ntos(x)
133
+ # previous was a moveto
134
+ elif self._lastCommand == "M":
135
+ cmd = None
136
+ pts = " " + pointToString(pt, self._ntos)
137
+ # basic
138
+ else:
139
+ cmd = "L"
140
+ pts = pointToString(pt, self._ntos)
141
+ # write the string
142
+ t = ""
143
+ if cmd:
144
+ t += cmd
145
+ self._lastCommand = cmd
146
+ t += pts
147
+ self._commands.append(t)
148
+ # store for future reference
149
+ self._lastX, self._lastY = pt
150
+
151
+ def _curveToOne(self, pt1, pt2, pt3):
152
+ """
153
+ >>> pen = SVGPathPen(None)
154
+ >>> pen.curveTo((10, 20), (30, 40), (50, 60))
155
+ >>> pen._commands
156
+ ['C10 20 30 40 50 60']
157
+ """
158
+ t = "C"
159
+ t += pointToString(pt1, self._ntos) + " "
160
+ t += pointToString(pt2, self._ntos) + " "
161
+ t += pointToString(pt3, self._ntos)
162
+ self._commands.append(t)
163
+ self._lastCommand = "C"
164
+ self._lastX, self._lastY = pt3
165
+
166
+ def _qCurveToOne(self, pt1, pt2):
167
+ """
168
+ >>> pen = SVGPathPen(None)
169
+ >>> pen.qCurveTo((10, 20), (30, 40))
170
+ >>> pen._commands
171
+ ['Q10 20 30 40']
172
+ >>> from fontTools.misc.roundTools import otRound
173
+ >>> pen = SVGPathPen(None, ntos=lambda v: str(otRound(v)))
174
+ >>> pen.qCurveTo((3, 3), (7, 5), (11, 4))
175
+ >>> pen._commands
176
+ ['Q3 3 5 4', 'Q7 5 11 4']
177
+ """
178
+ assert pt2 is not None
179
+ t = "Q"
180
+ t += pointToString(pt1, self._ntos) + " "
181
+ t += pointToString(pt2, self._ntos)
182
+ self._commands.append(t)
183
+ self._lastCommand = "Q"
184
+ self._lastX, self._lastY = pt2
185
+
186
+ def _closePath(self):
187
+ """
188
+ >>> pen = SVGPathPen(None)
189
+ >>> pen.closePath()
190
+ >>> pen._commands
191
+ ['Z']
192
+ """
193
+ self._commands.append("Z")
194
+ self._lastCommand = "Z"
195
+ self._lastX = self._lastY = None
196
+
197
+ def _endPath(self):
198
+ """
199
+ >>> pen = SVGPathPen(None)
200
+ >>> pen.endPath()
201
+ >>> pen._commands
202
+ []
203
+ """
204
+ self._lastCommand = None
205
+ self._lastX = self._lastY = None
206
+
207
+ def getCommands(self):
208
+ return "".join(self._commands)
209
+
210
+
211
+ def main(args=None):
212
+ """Generate per-character SVG from font and text"""
213
+
214
+ if args is None:
215
+ import sys
216
+
217
+ args = sys.argv[1:]
218
+
219
+ from fontTools.ttLib import TTFont
220
+ import argparse
221
+
222
+ parser = argparse.ArgumentParser(
223
+ "fonttools pens.svgPathPen", description="Generate SVG from text"
224
+ )
225
+ parser.add_argument("font", metavar="font.ttf", help="Font file.")
226
+ parser.add_argument("text", metavar="text", nargs="?", help="Text string.")
227
+ parser.add_argument(
228
+ "-y",
229
+ metavar="<number>",
230
+ help="Face index into a collection to open. Zero based.",
231
+ )
232
+ parser.add_argument(
233
+ "--glyphs",
234
+ metavar="whitespace-separated list of glyph names",
235
+ type=str,
236
+ help="Glyphs to show. Exclusive with text option",
237
+ )
238
+ parser.add_argument(
239
+ "--variations",
240
+ metavar="AXIS=LOC",
241
+ default="",
242
+ help="List of space separated locations. A location consist in "
243
+ "the name of a variation axis, followed by '=' and a number. E.g.: "
244
+ "wght=700 wdth=80. The default is the location of the base master.",
245
+ )
246
+
247
+ options = parser.parse_args(args)
248
+
249
+ fontNumber = int(options.y) if options.y is not None else 0
250
+
251
+ font = TTFont(options.font, fontNumber=fontNumber)
252
+ text = options.text
253
+ glyphs = options.glyphs
254
+
255
+ location = {}
256
+ for tag_v in options.variations.split():
257
+ fields = tag_v.split("=")
258
+ tag = fields[0].strip()
259
+ v = float(fields[1])
260
+ location[tag] = v
261
+
262
+ hhea = font["hhea"]
263
+ ascent, descent = hhea.ascent, hhea.descent
264
+
265
+ glyphset = font.getGlyphSet(location=location)
266
+ cmap = font["cmap"].getBestCmap()
267
+
268
+ if glyphs is not None and text is not None:
269
+ raise ValueError("Options --glyphs and --text are exclusive")
270
+
271
+ if glyphs is None:
272
+ glyphs = " ".join(cmap[ord(u)] for u in text)
273
+
274
+ glyphs = glyphs.split()
275
+
276
+ s = ""
277
+ width = 0
278
+ for g in glyphs:
279
+ glyph = glyphset[g]
280
+
281
+ pen = SVGPathPen(glyphset)
282
+ glyph.draw(pen)
283
+ commands = pen.getCommands()
284
+
285
+ s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % (
286
+ width,
287
+ ascent,
288
+ commands,
289
+ )
290
+
291
+ width += glyph.width
292
+
293
+ print('<?xml version="1.0" encoding="UTF-8"?>')
294
+ print(
295
+ '<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">'
296
+ % (width, ascent - descent)
297
+ )
298
+ print(s, end="")
299
+ print("</svg>")
300
+
301
+
302
+ if __name__ == "__main__":
303
+ import sys
304
+
305
+ if len(sys.argv) == 1:
306
+ import doctest
307
+
308
+ sys.exit(doctest.testmod().failed)
309
+
310
+ sys.exit(main())
venv/Lib/site-packages/fontTools/pens/t2CharStringPen.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2009 Type Supply LLC
2
+ # Author: Tal Leming
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, List, Tuple
7
+
8
+ from fontTools.cffLib.specializer import commandsToProgram, specializeCommands
9
+ from fontTools.misc.psCharStrings import T2CharString
10
+ from fontTools.misc.roundTools import otRound, roundFunc
11
+ from fontTools.pens.basePen import BasePen
12
+
13
+
14
+ class T2CharStringPen(BasePen):
15
+ """Pen to draw Type 2 CharStrings.
16
+
17
+ The 'roundTolerance' argument controls the rounding of point coordinates.
18
+ It is defined as the maximum absolute difference between the original
19
+ float and the rounded integer value.
20
+ The default tolerance of 0.5 means that all floats are rounded to integer;
21
+ a value of 0 disables rounding; values in between will only round floats
22
+ which are close to their integral part within the tolerated range.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ width: float | None,
28
+ glyphSet: Dict[str, Any] | None,
29
+ roundTolerance: float = 0.5,
30
+ CFF2: bool = False,
31
+ ) -> None:
32
+ super(T2CharStringPen, self).__init__(glyphSet)
33
+ self.round = roundFunc(roundTolerance)
34
+ self._CFF2 = CFF2
35
+ self._width = width
36
+ self._commands: List[Tuple[str | bytes, List[float]]] = []
37
+ self._p0 = (0, 0)
38
+
39
+ def _p(self, pt: Tuple[float, float]) -> List[float]:
40
+ p0 = self._p0
41
+ pt = self._p0 = (self.round(pt[0]), self.round(pt[1]))
42
+ return [pt[0] - p0[0], pt[1] - p0[1]]
43
+
44
+ def _moveTo(self, pt: Tuple[float, float]) -> None:
45
+ self._commands.append(("rmoveto", self._p(pt)))
46
+
47
+ def _lineTo(self, pt: Tuple[float, float]) -> None:
48
+ self._commands.append(("rlineto", self._p(pt)))
49
+
50
+ def _curveToOne(
51
+ self,
52
+ pt1: Tuple[float, float],
53
+ pt2: Tuple[float, float],
54
+ pt3: Tuple[float, float],
55
+ ) -> None:
56
+ _p = self._p
57
+ self._commands.append(("rrcurveto", _p(pt1) + _p(pt2) + _p(pt3)))
58
+
59
+ def _closePath(self) -> None:
60
+ pass
61
+
62
+ def _endPath(self) -> None:
63
+ pass
64
+
65
+ def getCharString(
66
+ self,
67
+ private: Dict | None = None,
68
+ globalSubrs: List | None = None,
69
+ optimize: bool = True,
70
+ ) -> T2CharString:
71
+ commands = self._commands
72
+ if optimize:
73
+ maxstack = 48 if not self._CFF2 else 513
74
+ commands = specializeCommands(
75
+ commands, generalizeFirst=False, maxstack=maxstack
76
+ )
77
+ program = commandsToProgram(commands)
78
+ if self._width is not None:
79
+ assert (
80
+ not self._CFF2
81
+ ), "CFF2 does not allow encoding glyph width in CharString."
82
+ program.insert(0, otRound(self._width))
83
+ if not self._CFF2:
84
+ program.append("endchar")
85
+ charString = T2CharString(
86
+ program=program, private=private, globalSubrs=globalSubrs
87
+ )
88
+ return charString
venv/Lib/site-packages/fontTools/pens/teePen.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pen multiplexing drawing to one or more pens."""
2
+
3
+ from fontTools.pens.basePen import AbstractPen
4
+
5
+
6
+ __all__ = ["TeePen"]
7
+
8
+
9
+ class TeePen(AbstractPen):
10
+ """Pen multiplexing drawing to one or more pens.
11
+
12
+ Use either as TeePen(pen1, pen2, ...) or TeePen(iterableOfPens)."""
13
+
14
+ def __init__(self, *pens):
15
+ if len(pens) == 1:
16
+ pens = pens[0]
17
+ self.pens = pens
18
+
19
+ def moveTo(self, p0):
20
+ for pen in self.pens:
21
+ pen.moveTo(p0)
22
+
23
+ def lineTo(self, p1):
24
+ for pen in self.pens:
25
+ pen.lineTo(p1)
26
+
27
+ def qCurveTo(self, *points):
28
+ for pen in self.pens:
29
+ pen.qCurveTo(*points)
30
+
31
+ def curveTo(self, *points):
32
+ for pen in self.pens:
33
+ pen.curveTo(*points)
34
+
35
+ def closePath(self):
36
+ for pen in self.pens:
37
+ pen.closePath()
38
+
39
+ def endPath(self):
40
+ for pen in self.pens:
41
+ pen.endPath()
42
+
43
+ def addComponent(self, glyphName, transformation):
44
+ for pen in self.pens:
45
+ pen.addComponent(glyphName, transformation)
46
+
47
+
48
+ if __name__ == "__main__":
49
+ from fontTools.pens.basePen import _TestPen
50
+
51
+ pen = TeePen(_TestPen(), _TestPen())
52
+ pen.moveTo((0, 0))
53
+ pen.lineTo((0, 100))
54
+ pen.curveTo((50, 75), (60, 50), (50, 25))
55
+ pen.closePath()
venv/Lib/site-packages/fontTools/pens/transformPen.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.pens.filterPen import FilterPen, FilterPointPen
2
+
3
+
4
+ __all__ = ["TransformPen", "TransformPointPen"]
5
+
6
+
7
+ class TransformPen(FilterPen):
8
+ """Pen that transforms all coordinates using a Affine transformation,
9
+ and passes them to another pen.
10
+ """
11
+
12
+ def __init__(self, outPen, transformation):
13
+ """The 'outPen' argument is another pen object. It will receive the
14
+ transformed coordinates. The 'transformation' argument can either
15
+ be a six-tuple, or a fontTools.misc.transform.Transform object.
16
+ """
17
+ super(TransformPen, self).__init__(outPen)
18
+ if not hasattr(transformation, "transformPoint"):
19
+ from fontTools.misc.transform import Transform
20
+
21
+ transformation = Transform(*transformation)
22
+ self._transformation = transformation
23
+ self._transformPoint = transformation.transformPoint
24
+ self._stack = []
25
+
26
+ def moveTo(self, pt):
27
+ self._outPen.moveTo(self._transformPoint(pt))
28
+
29
+ def lineTo(self, pt):
30
+ self._outPen.lineTo(self._transformPoint(pt))
31
+
32
+ def curveTo(self, *points):
33
+ self._outPen.curveTo(*self._transformPoints(points))
34
+
35
+ def qCurveTo(self, *points):
36
+ if points[-1] is None:
37
+ points = self._transformPoints(points[:-1]) + [None]
38
+ else:
39
+ points = self._transformPoints(points)
40
+ self._outPen.qCurveTo(*points)
41
+
42
+ def _transformPoints(self, points):
43
+ transformPoint = self._transformPoint
44
+ return [transformPoint(pt) for pt in points]
45
+
46
+ def closePath(self):
47
+ self._outPen.closePath()
48
+
49
+ def endPath(self):
50
+ self._outPen.endPath()
51
+
52
+ def addComponent(self, glyphName, transformation):
53
+ transformation = self._transformation.transform(transformation)
54
+ self._outPen.addComponent(glyphName, transformation)
55
+
56
+
57
+ class TransformPointPen(FilterPointPen):
58
+ """PointPen that transforms all coordinates using a Affine transformation,
59
+ and passes them to another PointPen.
60
+
61
+ For example::
62
+
63
+ >>> from fontTools.pens.recordingPen import RecordingPointPen
64
+ >>> rec = RecordingPointPen()
65
+ >>> pen = TransformPointPen(rec, (2, 0, 0, 2, -10, 5))
66
+ >>> v = iter(rec.value)
67
+ >>> pen.beginPath(identifier="contour-0")
68
+ >>> next(v)
69
+ ('beginPath', (), {'identifier': 'contour-0'})
70
+
71
+ >>> pen.addPoint((100, 100), "line")
72
+ >>> next(v)
73
+ ('addPoint', ((190, 205), 'line', False, None), {})
74
+
75
+ >>> pen.endPath()
76
+ >>> next(v)
77
+ ('endPath', (), {})
78
+
79
+ >>> pen.addComponent("a", (1, 0, 0, 1, -10, 5), identifier="component-0")
80
+ >>> next(v)
81
+ ('addComponent', ('a', <Transform [2 0 0 2 -30 15]>), {'identifier': 'component-0'})
82
+ """
83
+
84
+ def __init__(self, outPointPen, transformation):
85
+ """The 'outPointPen' argument is another point pen object.
86
+ It will receive the transformed coordinates.
87
+ The 'transformation' argument can either be a six-tuple, or a
88
+ fontTools.misc.transform.Transform object.
89
+ """
90
+ super().__init__(outPointPen)
91
+ if not hasattr(transformation, "transformPoint"):
92
+ from fontTools.misc.transform import Transform
93
+
94
+ transformation = Transform(*transformation)
95
+ self._transformation = transformation
96
+ self._transformPoint = transformation.transformPoint
97
+
98
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
99
+ self._outPen.addPoint(
100
+ self._transformPoint(pt), segmentType, smooth, name, **kwargs
101
+ )
102
+
103
+ def addComponent(self, baseGlyphName, transformation, **kwargs):
104
+ transformation = self._transformation.transform(transformation)
105
+ self._outPen.addComponent(baseGlyphName, transformation, **kwargs)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ from fontTools.pens.basePen import _TestPen
110
+
111
+ pen = TransformPen(_TestPen(None), (2, 0, 0.5, 2, -10, 0))
112
+ pen.moveTo((0, 0))
113
+ pen.lineTo((0, 100))
114
+ pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
115
+ pen.closePath()
venv/Lib/site-packages/fontTools/pens/ttGlyphPen.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from array import array
2
+ from typing import Any, Callable, Dict, Optional, Tuple
3
+ from fontTools.misc.fixedTools import MAX_F2DOT14, floatToFixedToFloat
4
+ from fontTools.misc.loggingTools import LogMixin
5
+ from fontTools.pens.pointPen import AbstractPointPen
6
+ from fontTools.misc.roundTools import otRound
7
+ from fontTools.pens.basePen import LoggingPen, PenError
8
+ from fontTools.pens.transformPen import TransformPen, TransformPointPen
9
+ from fontTools.ttLib.tables import ttProgram
10
+ from fontTools.ttLib.tables._g_l_y_f import flagOnCurve, flagCubic
11
+ from fontTools.ttLib.tables._g_l_y_f import Glyph
12
+ from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
13
+ from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
14
+ from fontTools.ttLib.tables._g_l_y_f import dropImpliedOnCurvePoints
15
+ import math
16
+
17
+
18
+ __all__ = ["TTGlyphPen", "TTGlyphPointPen"]
19
+
20
+
21
+ class _TTGlyphBasePen:
22
+ def __init__(
23
+ self,
24
+ glyphSet: Optional[Dict[str, Any]],
25
+ handleOverflowingTransforms: bool = True,
26
+ ) -> None:
27
+ """
28
+ Construct a new pen.
29
+
30
+ Args:
31
+ glyphSet (Dict[str, Any]): A glyphset object, used to resolve components.
32
+ handleOverflowingTransforms (bool): See below.
33
+
34
+ If ``handleOverflowingTransforms`` is True, the components' transform values
35
+ are checked that they don't overflow the limits of a F2Dot14 number:
36
+ -2.0 <= v < +2.0. If any transform value exceeds these, the composite
37
+ glyph is decomposed.
38
+
39
+ An exception to this rule is done for values that are very close to +2.0
40
+ (both for consistency with the -2.0 case, and for the relative frequency
41
+ these occur in real fonts). When almost +2.0 values occur (and all other
42
+ values are within the range -2.0 <= x <= +2.0), they are clamped to the
43
+ maximum positive value that can still be encoded as an F2Dot14: i.e.
44
+ 1.99993896484375.
45
+
46
+ If False, no check is done and all components are translated unmodified
47
+ into the glyf table, followed by an inevitable ``struct.error`` once an
48
+ attempt is made to compile them.
49
+
50
+ If both contours and components are present in a glyph, the components
51
+ are decomposed.
52
+ """
53
+ self.glyphSet = glyphSet
54
+ self.handleOverflowingTransforms = handleOverflowingTransforms
55
+ self.init()
56
+
57
+ def _decompose(
58
+ self,
59
+ glyphName: str,
60
+ transformation: Tuple[float, float, float, float, float, float],
61
+ ):
62
+ tpen = self.transformPen(self, transformation)
63
+ getattr(self.glyphSet[glyphName], self.drawMethod)(tpen)
64
+
65
+ def _isClosed(self):
66
+ """
67
+ Check if the current path is closed.
68
+ """
69
+ raise NotImplementedError
70
+
71
+ def init(self) -> None:
72
+ self.points = []
73
+ self.endPts = []
74
+ self.types = []
75
+ self.components = []
76
+
77
+ def addComponent(
78
+ self,
79
+ baseGlyphName: str,
80
+ transformation: Tuple[float, float, float, float, float, float],
81
+ identifier: Optional[str] = None,
82
+ **kwargs: Any,
83
+ ) -> None:
84
+ """
85
+ Add a sub glyph.
86
+ """
87
+ self.components.append((baseGlyphName, transformation))
88
+
89
+ def _buildComponents(self, componentFlags):
90
+ if self.handleOverflowingTransforms:
91
+ # we can't encode transform values > 2 or < -2 in F2Dot14,
92
+ # so we must decompose the glyph if any transform exceeds these
93
+ overflowing = any(
94
+ s > 2 or s < -2
95
+ for (glyphName, transformation) in self.components
96
+ for s in transformation[:4]
97
+ )
98
+ components = []
99
+ for glyphName, transformation in self.components:
100
+ if glyphName not in self.glyphSet:
101
+ self.log.warning(f"skipped non-existing component '{glyphName}'")
102
+ continue
103
+ if self.points or (self.handleOverflowingTransforms and overflowing):
104
+ # can't have both coordinates and components, so decompose
105
+ self._decompose(glyphName, transformation)
106
+ continue
107
+
108
+ component = GlyphComponent()
109
+ component.glyphName = glyphName
110
+ component.x, component.y = (otRound(v) for v in transformation[4:])
111
+ # quantize floats to F2Dot14 so we get same values as when decompiled
112
+ # from a binary glyf table
113
+ transformation = tuple(
114
+ floatToFixedToFloat(v, 14) for v in transformation[:4]
115
+ )
116
+ if transformation != (1, 0, 0, 1):
117
+ if self.handleOverflowingTransforms and any(
118
+ MAX_F2DOT14 < s <= 2 for s in transformation
119
+ ):
120
+ # clamp values ~= +2.0 so we can keep the component
121
+ transformation = tuple(
122
+ MAX_F2DOT14 if MAX_F2DOT14 < s <= 2 else s
123
+ for s in transformation
124
+ )
125
+ component.transform = (transformation[:2], transformation[2:])
126
+ component.flags = componentFlags
127
+ components.append(component)
128
+ return components
129
+
130
+ def glyph(
131
+ self,
132
+ componentFlags: int = 0x04,
133
+ dropImpliedOnCurves: bool = False,
134
+ *,
135
+ round: Callable[[float], int] = otRound,
136
+ ) -> Glyph:
137
+ """
138
+ Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
139
+
140
+ Args:
141
+ componentFlags: Flags to use for component glyphs. (default: 0x04)
142
+
143
+ dropImpliedOnCurves: Whether to remove implied-oncurve points. (default: False)
144
+ """
145
+ if not self._isClosed():
146
+ raise PenError("Didn't close last contour.")
147
+ components = self._buildComponents(componentFlags)
148
+
149
+ glyph = Glyph()
150
+ glyph.coordinates = GlyphCoordinates(self.points)
151
+ glyph.endPtsOfContours = self.endPts
152
+ glyph.flags = array("B", self.types)
153
+ self.init()
154
+
155
+ if components:
156
+ # If both components and contours were present, they have by now
157
+ # been decomposed by _buildComponents.
158
+ glyph.components = components
159
+ glyph.numberOfContours = -1
160
+ else:
161
+ glyph.numberOfContours = len(glyph.endPtsOfContours)
162
+ glyph.program = ttProgram.Program()
163
+ glyph.program.fromBytecode(b"")
164
+ if dropImpliedOnCurves:
165
+ dropImpliedOnCurvePoints(glyph)
166
+ glyph.coordinates.toInt(round=round)
167
+
168
+ return glyph
169
+
170
+
171
+ class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
172
+ """
173
+ Pen used for drawing to a TrueType glyph.
174
+
175
+ This pen can be used to construct or modify glyphs in a TrueType format
176
+ font. After using the pen to draw, use the ``.glyph()`` method to retrieve
177
+ a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
178
+ """
179
+
180
+ drawMethod = "draw"
181
+ transformPen = TransformPen
182
+
183
+ def __init__(
184
+ self,
185
+ glyphSet: Optional[Dict[str, Any]] = None,
186
+ handleOverflowingTransforms: bool = True,
187
+ outputImpliedClosingLine: bool = False,
188
+ ) -> None:
189
+ super().__init__(glyphSet, handleOverflowingTransforms)
190
+ self.outputImpliedClosingLine = outputImpliedClosingLine
191
+
192
+ def _addPoint(self, pt: Tuple[float, float], tp: int) -> None:
193
+ self.points.append(pt)
194
+ self.types.append(tp)
195
+
196
+ def _popPoint(self) -> None:
197
+ self.points.pop()
198
+ self.types.pop()
199
+
200
+ def _isClosed(self) -> bool:
201
+ return (not self.points) or (
202
+ self.endPts and self.endPts[-1] == len(self.points) - 1
203
+ )
204
+
205
+ def lineTo(self, pt: Tuple[float, float]) -> None:
206
+ self._addPoint(pt, flagOnCurve)
207
+
208
+ def moveTo(self, pt: Tuple[float, float]) -> None:
209
+ if not self._isClosed():
210
+ raise PenError('"move"-type point must begin a new contour.')
211
+ self._addPoint(pt, flagOnCurve)
212
+
213
+ def curveTo(self, *points) -> None:
214
+ assert len(points) % 2 == 1
215
+ for pt in points[:-1]:
216
+ self._addPoint(pt, flagCubic)
217
+
218
+ # last point is None if there are no on-curve points
219
+ if points[-1] is not None:
220
+ self._addPoint(points[-1], 1)
221
+
222
+ def qCurveTo(self, *points) -> None:
223
+ assert len(points) >= 1
224
+ for pt in points[:-1]:
225
+ self._addPoint(pt, 0)
226
+
227
+ # last point is None if there are no on-curve points
228
+ if points[-1] is not None:
229
+ self._addPoint(points[-1], 1)
230
+
231
+ def closePath(self) -> None:
232
+ endPt = len(self.points) - 1
233
+
234
+ # ignore anchors (one-point paths)
235
+ if endPt == 0 or (self.endPts and endPt == self.endPts[-1] + 1):
236
+ self._popPoint()
237
+ return
238
+
239
+ if not self.outputImpliedClosingLine:
240
+ # if first and last point on this path are the same, remove last
241
+ startPt = 0
242
+ if self.endPts:
243
+ startPt = self.endPts[-1] + 1
244
+ if self.points[startPt] == self.points[endPt]:
245
+ self._popPoint()
246
+ endPt -= 1
247
+
248
+ self.endPts.append(endPt)
249
+
250
+ def endPath(self) -> None:
251
+ # TrueType contours are always "closed"
252
+ self.closePath()
253
+
254
+
255
+ class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen):
256
+ """
257
+ Point pen used for drawing to a TrueType glyph.
258
+
259
+ This pen can be used to construct or modify glyphs in a TrueType format
260
+ font. After using the pen to draw, use the ``.glyph()`` method to retrieve
261
+ a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
262
+ """
263
+
264
+ drawMethod = "drawPoints"
265
+ transformPen = TransformPointPen
266
+
267
+ def init(self) -> None:
268
+ super().init()
269
+ self._currentContourStartIndex = None
270
+
271
+ def _isClosed(self) -> bool:
272
+ return self._currentContourStartIndex is None
273
+
274
+ def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
275
+ """
276
+ Start a new sub path.
277
+ """
278
+ if not self._isClosed():
279
+ raise PenError("Didn't close previous contour.")
280
+ self._currentContourStartIndex = len(self.points)
281
+
282
+ def endPath(self) -> None:
283
+ """
284
+ End the current sub path.
285
+ """
286
+ # TrueType contours are always "closed"
287
+ if self._isClosed():
288
+ raise PenError("Contour is already closed.")
289
+ if self._currentContourStartIndex == len(self.points):
290
+ # ignore empty contours
291
+ self._currentContourStartIndex = None
292
+ return
293
+
294
+ contourStart = self.endPts[-1] + 1 if self.endPts else 0
295
+ self.endPts.append(len(self.points) - 1)
296
+ self._currentContourStartIndex = None
297
+
298
+ # Resolve types for any cubic segments
299
+ flags = self.types
300
+ for i in range(contourStart, len(flags)):
301
+ if flags[i] == "curve":
302
+ j = i - 1
303
+ if j < contourStart:
304
+ j = len(flags) - 1
305
+ while flags[j] == 0:
306
+ flags[j] = flagCubic
307
+ j -= 1
308
+ flags[i] = flagOnCurve
309
+
310
+ def addPoint(
311
+ self,
312
+ pt: Tuple[float, float],
313
+ segmentType: Optional[str] = None,
314
+ smooth: bool = False,
315
+ name: Optional[str] = None,
316
+ identifier: Optional[str] = None,
317
+ **kwargs: Any,
318
+ ) -> None:
319
+ """
320
+ Add a point to the current sub path.
321
+ """
322
+ if self._isClosed():
323
+ raise PenError("Can't add a point to a closed contour.")
324
+ if segmentType is None:
325
+ self.types.append(0)
326
+ elif segmentType in ("line", "move"):
327
+ self.types.append(flagOnCurve)
328
+ elif segmentType == "qcurve":
329
+ self.types.append(flagOnCurve)
330
+ elif segmentType == "curve":
331
+ self.types.append("curve")
332
+ else:
333
+ raise AssertionError(segmentType)
334
+
335
+ self.points.append(pt)
venv/Lib/site-packages/fontTools/pens/wxPen.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fontTools.pens.basePen import BasePen
2
+
3
+
4
+ __all__ = ["WxPen"]
5
+
6
+
7
+ class WxPen(BasePen):
8
+ def __init__(self, glyphSet, path=None):
9
+ BasePen.__init__(self, glyphSet)
10
+ if path is None:
11
+ import wx
12
+
13
+ path = wx.GraphicsRenderer.GetDefaultRenderer().CreatePath()
14
+ self.path = path
15
+
16
+ def _moveTo(self, p):
17
+ self.path.MoveToPoint(*p)
18
+
19
+ def _lineTo(self, p):
20
+ self.path.AddLineToPoint(*p)
21
+
22
+ def _curveToOne(self, p1, p2, p3):
23
+ self.path.AddCurveToPoint(*p1 + p2 + p3)
24
+
25
+ def _qCurveToOne(self, p1, p2):
26
+ self.path.AddQuadCurveToPoint(*p1 + p2)
27
+
28
+ def _closePath(self):
29
+ self.path.CloseSubpath()
venv/Lib/site-packages/fontTools/qu2cu/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2016 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .qu2cu import *
venv/Lib/site-packages/fontTools/qu2cu/__main__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ from .cli import _main as main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
venv/Lib/site-packages/fontTools/qu2cu/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (234 Bytes). View file
 
venv/Lib/site-packages/fontTools/qu2cu/__pycache__/__main__.cpython-311.pyc ADDED
Binary file (417 Bytes). View file
 
venv/Lib/site-packages/fontTools/qu2cu/__pycache__/benchmark.cpython-311.pyc ADDED
Binary file (3.66 kB). View file
 
venv/Lib/site-packages/fontTools/qu2cu/__pycache__/cli.cpython-311.pyc ADDED
Binary file (5.99 kB). View file
 
venv/Lib/site-packages/fontTools/qu2cu/__pycache__/qu2cu.cpython-311.pyc ADDED
Binary file (16.3 kB). View file