Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/fontTools/pens/pointPen.py: 16%
292 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:33 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:33 +0000
1"""
2=========
3PointPens
4=========
6Where **SegmentPens** have an intuitive approach to drawing
7(if you're familiar with postscript anyway), the **PointPen**
8is geared towards accessing all the data in the contours of
9the glyph. A PointPen has a very simple interface, it just
10steps through all the points in a call from glyph.drawPoints().
11This allows the caller to provide more data for each point.
12For instance, whether or not a point is smooth, and its name.
13"""
15import math
16from typing import Any, Optional, Tuple, Dict
18from fontTools.pens.basePen import AbstractPen, PenError
19from fontTools.misc.transform import DecomposedTransform
21__all__ = [
22 "AbstractPointPen",
23 "BasePointToSegmentPen",
24 "PointToSegmentPen",
25 "SegmentToPointPen",
26 "GuessSmoothPointPen",
27 "ReverseContourPointPen",
28]
31class AbstractPointPen:
32 """Baseclass for all PointPens."""
34 def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
35 """Start a new sub path."""
36 raise NotImplementedError
38 def endPath(self) -> None:
39 """End the current sub path."""
40 raise NotImplementedError
42 def addPoint(
43 self,
44 pt: Tuple[float, float],
45 segmentType: Optional[str] = None,
46 smooth: bool = False,
47 name: Optional[str] = None,
48 identifier: Optional[str] = None,
49 **kwargs: Any,
50 ) -> None:
51 """Add a point to the current sub path."""
52 raise NotImplementedError
54 def addComponent(
55 self,
56 baseGlyphName: str,
57 transformation: Tuple[float, float, float, float, float, float],
58 identifier: Optional[str] = None,
59 **kwargs: Any,
60 ) -> None:
61 """Add a sub glyph."""
62 raise NotImplementedError
64 def addVarComponent(
65 self,
66 glyphName: str,
67 transformation: DecomposedTransform,
68 location: Dict[str, float],
69 identifier: Optional[str] = None,
70 **kwargs: Any,
71 ) -> None:
72 """Add a VarComponent sub glyph. The 'transformation' argument
73 must be a DecomposedTransform from the fontTools.misc.transform module,
74 and the 'location' argument must be a dictionary mapping axis tags
75 to their locations.
76 """
77 # ttGlyphSet decomposes for us
78 raise AttributeError
81class BasePointToSegmentPen(AbstractPointPen):
82 """
83 Base class for retrieving the outline in a segment-oriented
84 way. The PointPen protocol is simple yet also a little tricky,
85 so when you need an outline presented as segments but you have
86 as points, do use this base implementation as it properly takes
87 care of all the edge cases.
88 """
90 def __init__(self):
91 self.currentPath = None
93 def beginPath(self, identifier=None, **kwargs):
94 if self.currentPath is not None:
95 raise PenError("Path already begun.")
96 self.currentPath = []
98 def _flushContour(self, segments):
99 """Override this method.
101 It will be called for each non-empty sub path with a list
102 of segments: the 'segments' argument.
104 The segments list contains tuples of length 2:
105 (segmentType, points)
107 segmentType is one of "move", "line", "curve" or "qcurve".
108 "move" may only occur as the first segment, and it signifies
109 an OPEN path. A CLOSED path does NOT start with a "move", in
110 fact it will not contain a "move" at ALL.
112 The 'points' field in the 2-tuple is a list of point info
113 tuples. The list has 1 or more items, a point tuple has
114 four items:
115 (point, smooth, name, kwargs)
116 'point' is an (x, y) coordinate pair.
118 For a closed path, the initial moveTo point is defined as
119 the last point of the last segment.
121 The 'points' list of "move" and "line" segments always contains
122 exactly one point tuple.
123 """
124 raise NotImplementedError
126 def endPath(self):
127 if self.currentPath is None:
128 raise PenError("Path not begun.")
129 points = self.currentPath
130 self.currentPath = None
131 if not points:
132 return
133 if len(points) == 1:
134 # Not much more we can do than output a single move segment.
135 pt, segmentType, smooth, name, kwargs = points[0]
136 segments = [("move", [(pt, smooth, name, kwargs)])]
137 self._flushContour(segments)
138 return
139 segments = []
140 if points[0][1] == "move":
141 # It's an open contour, insert a "move" segment for the first
142 # point and remove that first point from the point list.
143 pt, segmentType, smooth, name, kwargs = points[0]
144 segments.append(("move", [(pt, smooth, name, kwargs)]))
145 points.pop(0)
146 else:
147 # It's a closed contour. Locate the first on-curve point, and
148 # rotate the point list so that it _ends_ with an on-curve
149 # point.
150 firstOnCurve = None
151 for i in range(len(points)):
152 segmentType = points[i][1]
153 if segmentType is not None:
154 firstOnCurve = i
155 break
156 if firstOnCurve is None:
157 # Special case for quadratics: a contour with no on-curve
158 # points. Add a "None" point. (See also the Pen protocol's
159 # qCurveTo() method and fontTools.pens.basePen.py.)
160 points.append((None, "qcurve", None, None, None))
161 else:
162 points = points[firstOnCurve + 1 :] + points[: firstOnCurve + 1]
164 currentSegment = []
165 for pt, segmentType, smooth, name, kwargs in points:
166 currentSegment.append((pt, smooth, name, kwargs))
167 if segmentType is None:
168 continue
169 segments.append((segmentType, currentSegment))
170 currentSegment = []
172 self._flushContour(segments)
174 def addPoint(
175 self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
176 ):
177 if self.currentPath is None:
178 raise PenError("Path not begun")
179 self.currentPath.append((pt, segmentType, smooth, name, kwargs))
182class PointToSegmentPen(BasePointToSegmentPen):
183 """
184 Adapter class that converts the PointPen protocol to the
185 (Segment)Pen protocol.
187 NOTE: The segment pen does not support and will drop point names, identifiers
188 and kwargs.
189 """
191 def __init__(self, segmentPen, outputImpliedClosingLine=False):
192 BasePointToSegmentPen.__init__(self)
193 self.pen = segmentPen
194 self.outputImpliedClosingLine = outputImpliedClosingLine
196 def _flushContour(self, segments):
197 if not segments:
198 raise PenError("Must have at least one segment.")
199 pen = self.pen
200 if segments[0][0] == "move":
201 # It's an open path.
202 closed = False
203 points = segments[0][1]
204 if len(points) != 1:
205 raise PenError(f"Illegal move segment point count: {len(points)}")
206 movePt, _, _, _ = points[0]
207 del segments[0]
208 else:
209 # It's a closed path, do a moveTo to the last
210 # point of the last segment.
211 closed = True
212 segmentType, points = segments[-1]
213 movePt, _, _, _ = points[-1]
214 if movePt is None:
215 # quad special case: a contour with no on-curve points contains
216 # one "qcurve" segment that ends with a point that's None. We
217 # must not output a moveTo() in that case.
218 pass
219 else:
220 pen.moveTo(movePt)
221 outputImpliedClosingLine = self.outputImpliedClosingLine
222 nSegments = len(segments)
223 lastPt = movePt
224 for i in range(nSegments):
225 segmentType, points = segments[i]
226 points = [pt for pt, _, _, _ in points]
227 if segmentType == "line":
228 if len(points) != 1:
229 raise PenError(f"Illegal line segment point count: {len(points)}")
230 pt = points[0]
231 # For closed contours, a 'lineTo' is always implied from the last oncurve
232 # point to the starting point, thus we can omit it when the last and
233 # starting point don't overlap.
234 # However, when the last oncurve point is a "line" segment and has same
235 # coordinates as the starting point of a closed contour, we need to output
236 # the closing 'lineTo' explicitly (regardless of the value of the
237 # 'outputImpliedClosingLine' option) in order to disambiguate this case from
238 # the implied closing 'lineTo', otherwise the duplicate point would be lost.
239 # See https://github.com/googlefonts/fontmake/issues/572.
240 if (
241 i + 1 != nSegments
242 or outputImpliedClosingLine
243 or not closed
244 or pt == lastPt
245 ):
246 pen.lineTo(pt)
247 lastPt = pt
248 elif segmentType == "curve":
249 pen.curveTo(*points)
250 lastPt = points[-1]
251 elif segmentType == "qcurve":
252 pen.qCurveTo(*points)
253 lastPt = points[-1]
254 else:
255 raise PenError(f"Illegal segmentType: {segmentType}")
256 if closed:
257 pen.closePath()
258 else:
259 pen.endPath()
261 def addComponent(self, glyphName, transform, identifier=None, **kwargs):
262 del identifier # unused
263 del kwargs # unused
264 self.pen.addComponent(glyphName, transform)
267class SegmentToPointPen(AbstractPen):
268 """
269 Adapter class that converts the (Segment)Pen protocol to the
270 PointPen protocol.
271 """
273 def __init__(self, pointPen, guessSmooth=True):
274 if guessSmooth:
275 self.pen = GuessSmoothPointPen(pointPen)
276 else:
277 self.pen = pointPen
278 self.contour = None
280 def _flushContour(self):
281 pen = self.pen
282 pen.beginPath()
283 for pt, segmentType in self.contour:
284 pen.addPoint(pt, segmentType=segmentType)
285 pen.endPath()
287 def moveTo(self, pt):
288 self.contour = []
289 self.contour.append((pt, "move"))
291 def lineTo(self, pt):
292 if self.contour is None:
293 raise PenError("Contour missing required initial moveTo")
294 self.contour.append((pt, "line"))
296 def curveTo(self, *pts):
297 if not pts:
298 raise TypeError("Must pass in at least one point")
299 if self.contour is None:
300 raise PenError("Contour missing required initial moveTo")
301 for pt in pts[:-1]:
302 self.contour.append((pt, None))
303 self.contour.append((pts[-1], "curve"))
305 def qCurveTo(self, *pts):
306 if not pts:
307 raise TypeError("Must pass in at least one point")
308 if pts[-1] is None:
309 self.contour = []
310 else:
311 if self.contour is None:
312 raise PenError("Contour missing required initial moveTo")
313 for pt in pts[:-1]:
314 self.contour.append((pt, None))
315 if pts[-1] is not None:
316 self.contour.append((pts[-1], "qcurve"))
318 def closePath(self):
319 if self.contour is None:
320 raise PenError("Contour missing required initial moveTo")
321 if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]:
322 self.contour[0] = self.contour[-1]
323 del self.contour[-1]
324 else:
325 # There's an implied line at the end, replace "move" with "line"
326 # for the first point
327 pt, tp = self.contour[0]
328 if tp == "move":
329 self.contour[0] = pt, "line"
330 self._flushContour()
331 self.contour = None
333 def endPath(self):
334 if self.contour is None:
335 raise PenError("Contour missing required initial moveTo")
336 self._flushContour()
337 self.contour = None
339 def addComponent(self, glyphName, transform):
340 if self.contour is not None:
341 raise PenError("Components must be added before or after contours")
342 self.pen.addComponent(glyphName, transform)
345class GuessSmoothPointPen(AbstractPointPen):
346 """
347 Filtering PointPen that tries to determine whether an on-curve point
348 should be "smooth", ie. that it's a "tangent" point or a "curve" point.
349 """
351 def __init__(self, outPen, error=0.05):
352 self._outPen = outPen
353 self._error = error
354 self._points = None
356 def _flushContour(self):
357 if self._points is None:
358 raise PenError("Path not begun")
359 points = self._points
360 nPoints = len(points)
361 if not nPoints:
362 return
363 if points[0][1] == "move":
364 # Open path.
365 indices = range(1, nPoints - 1)
366 elif nPoints > 1:
367 # Closed path. To avoid having to mod the contour index, we
368 # simply abuse Python's negative index feature, and start at -1
369 indices = range(-1, nPoints - 1)
370 else:
371 # closed path containing 1 point (!), ignore.
372 indices = []
373 for i in indices:
374 pt, segmentType, _, name, kwargs = points[i]
375 if segmentType is None:
376 continue
377 prev = i - 1
378 next = i + 1
379 if points[prev][1] is not None and points[next][1] is not None:
380 continue
381 # At least one of our neighbors is an off-curve point
382 pt = points[i][0]
383 prevPt = points[prev][0]
384 nextPt = points[next][0]
385 if pt != prevPt and pt != nextPt:
386 dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1]
387 dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1]
388 a1 = math.atan2(dy1, dx1)
389 a2 = math.atan2(dy2, dx2)
390 if abs(a1 - a2) < self._error:
391 points[i] = pt, segmentType, True, name, kwargs
393 for pt, segmentType, smooth, name, kwargs in points:
394 self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
396 def beginPath(self, identifier=None, **kwargs):
397 if self._points is not None:
398 raise PenError("Path already begun")
399 self._points = []
400 if identifier is not None:
401 kwargs["identifier"] = identifier
402 self._outPen.beginPath(**kwargs)
404 def endPath(self):
405 self._flushContour()
406 self._outPen.endPath()
407 self._points = None
409 def addPoint(
410 self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
411 ):
412 if self._points is None:
413 raise PenError("Path not begun")
414 if identifier is not None:
415 kwargs["identifier"] = identifier
416 self._points.append((pt, segmentType, False, name, kwargs))
418 def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
419 if self._points is not None:
420 raise PenError("Components must be added before or after contours")
421 if identifier is not None:
422 kwargs["identifier"] = identifier
423 self._outPen.addComponent(glyphName, transformation, **kwargs)
425 def addVarComponent(
426 self, glyphName, transformation, location, identifier=None, **kwargs
427 ):
428 if self._points is not None:
429 raise PenError("VarComponents must be added before or after contours")
430 if identifier is not None:
431 kwargs["identifier"] = identifier
432 self._outPen.addVarComponent(glyphName, transformation, location, **kwargs)
435class ReverseContourPointPen(AbstractPointPen):
436 """
437 This is a PointPen that passes outline data to another PointPen, but
438 reversing the winding direction of all contours. Components are simply
439 passed through unchanged.
441 Closed contours are reversed in such a way that the first point remains
442 the first point.
443 """
445 def __init__(self, outputPointPen):
446 self.pen = outputPointPen
447 # a place to store the points for the current sub path
448 self.currentContour = None
450 def _flushContour(self):
451 pen = self.pen
452 contour = self.currentContour
453 if not contour:
454 pen.beginPath(identifier=self.currentContourIdentifier)
455 pen.endPath()
456 return
458 closed = contour[0][1] != "move"
459 if not closed:
460 lastSegmentType = "move"
461 else:
462 # Remove the first point and insert it at the end. When
463 # the list of points gets reversed, this point will then
464 # again be at the start. In other words, the following
465 # will hold:
466 # for N in range(len(originalContour)):
467 # originalContour[N] == reversedContour[-N]
468 contour.append(contour.pop(0))
469 # Find the first on-curve point.
470 firstOnCurve = None
471 for i in range(len(contour)):
472 if contour[i][1] is not None:
473 firstOnCurve = i
474 break
475 if firstOnCurve is None:
476 # There are no on-curve points, be basically have to
477 # do nothing but contour.reverse().
478 lastSegmentType = None
479 else:
480 lastSegmentType = contour[firstOnCurve][1]
482 contour.reverse()
483 if not closed:
484 # Open paths must start with a move, so we simply dump
485 # all off-curve points leading up to the first on-curve.
486 while contour[0][1] is None:
487 contour.pop(0)
488 pen.beginPath(identifier=self.currentContourIdentifier)
489 for pt, nextSegmentType, smooth, name, kwargs in contour:
490 if nextSegmentType is not None:
491 segmentType = lastSegmentType
492 lastSegmentType = nextSegmentType
493 else:
494 segmentType = None
495 pen.addPoint(
496 pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs
497 )
498 pen.endPath()
500 def beginPath(self, identifier=None, **kwargs):
501 if self.currentContour is not None:
502 raise PenError("Path already begun")
503 self.currentContour = []
504 self.currentContourIdentifier = identifier
505 self.onCurve = []
507 def endPath(self):
508 if self.currentContour is None:
509 raise PenError("Path not begun")
510 self._flushContour()
511 self.currentContour = None
513 def addPoint(
514 self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
515 ):
516 if self.currentContour is None:
517 raise PenError("Path not begun")
518 if identifier is not None:
519 kwargs["identifier"] = identifier
520 self.currentContour.append((pt, segmentType, smooth, name, kwargs))
522 def addComponent(self, glyphName, transform, identifier=None, **kwargs):
523 if self.currentContour is not None:
524 raise PenError("Components must be added before or after contours")
525 self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs)