Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/fontTools/pens/basePen.py: 31%
178 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"""fontTools.pens.basePen.py -- Tools and base classes to build pen objects.
3The Pen Protocol
5A Pen is a kind of object that standardizes the way how to "draw" outlines:
6it is a middle man between an outline and a drawing. In other words:
7it is an abstraction for drawing outlines, making sure that outline objects
8don't need to know the details about how and where they're being drawn, and
9that drawings don't need to know the details of how outlines are stored.
11The most basic pattern is this::
13 outline.draw(pen) # 'outline' draws itself onto 'pen'
15Pens can be used to render outlines to the screen, but also to construct
16new outlines. Eg. an outline object can be both a drawable object (it has a
17draw() method) as well as a pen itself: you *build* an outline using pen
18methods.
20The AbstractPen class defines the Pen protocol. It implements almost
21nothing (only no-op closePath() and endPath() methods), but is useful
22for documentation purposes. Subclassing it basically tells the reader:
23"this class implements the Pen protocol.". An examples of an AbstractPen
24subclass is :py:class:`fontTools.pens.transformPen.TransformPen`.
26The BasePen class is a base implementation useful for pens that actually
27draw (for example a pen renders outlines using a native graphics engine).
28BasePen contains a lot of base functionality, making it very easy to build
29a pen that fully conforms to the pen protocol. Note that if you subclass
30BasePen, you *don't* override moveTo(), lineTo(), etc., but _moveTo(),
31_lineTo(), etc. See the BasePen doc string for details. Examples of
32BasePen subclasses are fontTools.pens.boundsPen.BoundsPen and
33fontTools.pens.cocoaPen.CocoaPen.
35Coordinates are usually expressed as (x, y) tuples, but generally any
36sequence of length 2 will do.
37"""
39from typing import Tuple, Dict
41from fontTools.misc.loggingTools import LogMixin
42from fontTools.misc.transform import DecomposedTransform
44__all__ = [
45 "AbstractPen",
46 "NullPen",
47 "BasePen",
48 "PenError",
49 "decomposeSuperBezierSegment",
50 "decomposeQuadraticSegment",
51]
54class PenError(Exception):
55 """Represents an error during penning."""
58class OpenContourError(PenError):
59 pass
62class 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
69 def lineTo(self, pt: Tuple[float, float]) -> None:
70 """Draw a straight line from the current point to 'pt'."""
71 raise NotImplementedError
73 def curveTo(self, *points: Tuple[float, float]) -> None:
74 """Draw a cubic bezier with an arbitrary number of control points.
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:
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().
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
94 def qCurveTo(self, *points: Tuple[float, float]) -> None:
95 """Draw a whole string of quadratic curve segments.
97 The last point specified is on-curve, all others are off-curve
98 points.
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().
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
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
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
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
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
150class NullPen(AbstractPen):
152 """A pen that does nothing."""
154 def moveTo(self, pt):
155 pass
157 def lineTo(self, pt):
158 pass
160 def curveTo(self, *points):
161 pass
163 def qCurveTo(self, *points):
164 pass
166 def closePath(self):
167 pass
169 def endPath(self):
170 pass
172 def addComponent(self, glyphName, transformation):
173 pass
175 def addVarComponent(self, glyphName, transformation, location):
176 pass
179class LoggingPen(LogMixin, AbstractPen):
180 """A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)"""
182 pass
185class MissingComponentError(KeyError):
186 """Indicates a component pointing to a non-existent glyph in the glyphset."""
189class DecomposingPen(LoggingPen):
191 """Implements a 'addComponent' method that decomposes components
192 (i.e. draws them onto self as simple contours).
193 It can also be used as a mixin class (e.g. see ContourRecordingPen).
195 You must override moveTo, lineTo, curveTo and qCurveTo. You may
196 additionally override closePath, endPath and addComponent.
198 By default a warning message is logged when a base glyph is missing;
199 set the class variable ``skipMissingComponents`` to False if you want
200 to raise a :class:`MissingComponentError` exception.
201 """
203 skipMissingComponents = True
205 def __init__(self, glyphSet):
206 """Takes a single 'glyphSet' argument (dict), in which the glyphs
207 that are referenced as components are looked up by their name.
208 """
209 super(DecomposingPen, self).__init__()
210 self.glyphSet = glyphSet
212 def addComponent(self, glyphName, transformation):
213 """Transform the points of the base glyph and draw it onto self."""
214 from fontTools.pens.transformPen import TransformPen
216 try:
217 glyph = self.glyphSet[glyphName]
218 except KeyError:
219 if not self.skipMissingComponents:
220 raise MissingComponentError(glyphName)
221 self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName)
222 else:
223 tPen = TransformPen(self, transformation)
224 glyph.draw(tPen)
226 def addVarComponent(self, glyphName, transformation, location):
227 # GlyphSet decomposes for us
228 raise AttributeError
231class BasePen(DecomposingPen):
233 """Base class for drawing pens. You must override _moveTo, _lineTo and
234 _curveToOne. You may additionally override _closePath, _endPath,
235 addComponent, addVarComponent, and/or _qCurveToOne. You should not
236 override any other methods.
237 """
239 def __init__(self, glyphSet=None):
240 super(BasePen, self).__init__(glyphSet)
241 self.__currentPoint = None
243 # must override
245 def _moveTo(self, pt):
246 raise NotImplementedError
248 def _lineTo(self, pt):
249 raise NotImplementedError
251 def _curveToOne(self, pt1, pt2, pt3):
252 raise NotImplementedError
254 # may override
256 def _closePath(self):
257 pass
259 def _endPath(self):
260 pass
262 def _qCurveToOne(self, pt1, pt2):
263 """This method implements the basic quadratic curve type. The
264 default implementation delegates the work to the cubic curve
265 function. Optionally override with a native implementation.
266 """
267 pt0x, pt0y = self.__currentPoint
268 pt1x, pt1y = pt1
269 pt2x, pt2y = pt2
270 mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
271 mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
272 mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
273 mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
274 self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
276 # don't override
278 def _getCurrentPoint(self):
279 """Return the current point. This is not part of the public
280 interface, yet is useful for subclasses.
281 """
282 return self.__currentPoint
284 def closePath(self):
285 self._closePath()
286 self.__currentPoint = None
288 def endPath(self):
289 self._endPath()
290 self.__currentPoint = None
292 def moveTo(self, pt):
293 self._moveTo(pt)
294 self.__currentPoint = pt
296 def lineTo(self, pt):
297 self._lineTo(pt)
298 self.__currentPoint = pt
300 def curveTo(self, *points):
301 n = len(points) - 1 # 'n' is the number of control points
302 assert n >= 0
303 if n == 2:
304 # The common case, we have exactly two BCP's, so this is a standard
305 # cubic bezier. Even though decomposeSuperBezierSegment() handles
306 # this case just fine, we special-case it anyway since it's so
307 # common.
308 self._curveToOne(*points)
309 self.__currentPoint = points[-1]
310 elif n > 2:
311 # n is the number of control points; split curve into n-1 cubic
312 # bezier segments. The algorithm used here is inspired by NURB
313 # splines and the TrueType "implied point" principle, and ensures
314 # the smoothest possible connection between two curve segments,
315 # with no disruption in the curvature. It is practical since it
316 # allows one to construct multiple bezier segments with a much
317 # smaller amount of points.
318 _curveToOne = self._curveToOne
319 for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
320 _curveToOne(pt1, pt2, pt3)
321 self.__currentPoint = pt3
322 elif n == 1:
323 self.qCurveTo(*points)
324 elif n == 0:
325 self.lineTo(points[0])
326 else:
327 raise AssertionError("can't get there from here")
329 def qCurveTo(self, *points):
330 n = len(points) - 1 # 'n' is the number of control points
331 assert n >= 0
332 if points[-1] is None:
333 # Special case for TrueType quadratics: it is possible to
334 # define a contour with NO on-curve points. BasePen supports
335 # this by allowing the final argument (the expected on-curve
336 # point) to be None. We simulate the feature by making the implied
337 # on-curve point between the last and the first off-curve points
338 # explicit.
339 x, y = points[-2] # last off-curve point
340 nx, ny = points[0] # first off-curve point
341 impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
342 self.__currentPoint = impliedStartPoint
343 self._moveTo(impliedStartPoint)
344 points = points[:-1] + (impliedStartPoint,)
345 if n > 0:
346 # Split the string of points into discrete quadratic curve
347 # segments. Between any two consecutive off-curve points
348 # there's an implied on-curve point exactly in the middle.
349 # This is where the segment splits.
350 _qCurveToOne = self._qCurveToOne
351 for pt1, pt2 in decomposeQuadraticSegment(points):
352 _qCurveToOne(pt1, pt2)
353 self.__currentPoint = pt2
354 else:
355 self.lineTo(points[0])
358def decomposeSuperBezierSegment(points):
359 """Split the SuperBezier described by 'points' into a list of regular
360 bezier segments. The 'points' argument must be a sequence with length
361 3 or greater, containing (x, y) coordinates. The last point is the
362 destination on-curve point, the rest of the points are off-curve points.
363 The start point should not be supplied.
365 This function returns a list of (pt1, pt2, pt3) tuples, which each
366 specify a regular curveto-style bezier segment.
367 """
368 n = len(points) - 1
369 assert n > 1
370 bezierSegments = []
371 pt1, pt2, pt3 = points[0], None, None
372 for i in range(2, n + 1):
373 # calculate points in between control points.
374 nDivisions = min(i, 3, n - i + 2)
375 for j in range(1, nDivisions):
376 factor = j / nDivisions
377 temp1 = points[i - 1]
378 temp2 = points[i - 2]
379 temp = (
380 temp2[0] + factor * (temp1[0] - temp2[0]),
381 temp2[1] + factor * (temp1[1] - temp2[1]),
382 )
383 if pt2 is None:
384 pt2 = temp
385 else:
386 pt3 = (0.5 * (pt2[0] + temp[0]), 0.5 * (pt2[1] + temp[1]))
387 bezierSegments.append((pt1, pt2, pt3))
388 pt1, pt2, pt3 = temp, None, None
389 bezierSegments.append((pt1, points[-2], points[-1]))
390 return bezierSegments
393def decomposeQuadraticSegment(points):
394 """Split the quadratic curve segment described by 'points' into a list
395 of "atomic" quadratic segments. The 'points' argument must be a sequence
396 with length 2 or greater, containing (x, y) coordinates. The last point
397 is the destination on-curve point, the rest of the points are off-curve
398 points. The start point should not be supplied.
400 This function returns a list of (pt1, pt2) tuples, which each specify a
401 plain quadratic bezier segment.
402 """
403 n = len(points) - 1
404 assert n > 0
405 quadSegments = []
406 for i in range(n - 1):
407 x, y = points[i]
408 nx, ny = points[i + 1]
409 impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
410 quadSegments.append((points[i], impliedPt))
411 quadSegments.append((points[-2], points[-1]))
412 return quadSegments
415class _TestPen(BasePen):
416 """Test class that prints PostScript to stdout."""
418 def _moveTo(self, pt):
419 print("%s %s moveto" % (pt[0], pt[1]))
421 def _lineTo(self, pt):
422 print("%s %s lineto" % (pt[0], pt[1]))
424 def _curveToOne(self, bcp1, bcp2, pt):
425 print(
426 "%s %s %s %s %s %s curveto"
427 % (bcp1[0], bcp1[1], bcp2[0], bcp2[1], pt[0], pt[1])
428 )
430 def _closePath(self):
431 print("closepath")
434if __name__ == "__main__":
435 pen = _TestPen(None)
436 pen.moveTo((0, 0))
437 pen.lineTo((0, 100))
438 pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
439 pen.closePath()
441 pen = _TestPen(None)
442 # testing the "no on-curve point" scenario
443 pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
444 pen.closePath()