blob: eee4269930376ff5947ad55ef133faa40c79f856 [file] [log] [blame]
jvrb369ef32003-08-23 20:19:33 +00001"""fontTools.pens.basePen.py -- Tools and base classes to build pen objects.
2
3The Pen Protocol
4
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.
10
11The most basic pattern is this:
12
13 outline.draw(pen) # 'outline' draws itself onto 'pen'
14
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.
19
jvr40cde702003-09-06 16:00:03 +000020The 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 fontTools.pens.transformPen.TransformPen.
jvrb369ef32003-08-23 20:19:33 +000025
jvr40cde702003-09-06 16:00:03 +000026The 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.
34
35Coordinates are usually expressed as (x, y) tuples, but generally any
36sequence of length 2 will do.
jvrb369ef32003-08-23 20:19:33 +000037"""
38
Behdad Esfahbod1ae29592014-01-14 15:07:50 +080039from __future__ import print_function, division, absolute_import
Behdad Esfahbod7ed91ec2013-11-27 15:16:28 -050040from fontTools.misc.py23 import *
jvr40cde702003-09-06 16:00:03 +000041
Behdad Esfahbod285d7b82013-09-10 20:30:47 -040042__all__ = ["AbstractPen", "NullPen", "BasePen",
jvr23cb2002003-09-07 09:41:28 +000043 "decomposeSuperBezierSegment", "decomposeQuadraticSegment"]
jvrb369ef32003-08-23 20:19:33 +000044
45
jvr91bca422012-10-18 12:49:22 +000046class AbstractPen(object):
jvrb369ef32003-08-23 20:19:33 +000047
48 def moveTo(self, pt):
jvr40cde702003-09-06 16:00:03 +000049 """Begin a new sub path, set the current point to 'pt'. You must
50 end each sub path with a call to pen.closePath() or pen.endPath().
51 """
jvrb369ef32003-08-23 20:19:33 +000052 raise NotImplementedError
53
54 def lineTo(self, pt):
jvr40cde702003-09-06 16:00:03 +000055 """Draw a straight line from the current point to 'pt'."""
jvrb369ef32003-08-23 20:19:33 +000056 raise NotImplementedError
57
58 def curveTo(self, *points):
jvr934fe5f2003-08-28 19:30:46 +000059 """Draw a cubic bezier with an arbitrary number of control points.
60
61 The last point specified is on-curve, all others are off-curve
62 (control) points. If the number of control points is > 2, the
63 segment is split into multiple bezier segments. This works
64 like this:
jvrb369ef32003-08-23 20:19:33 +000065
66 Let n be the number of control points (which is the number of
67 arguments to this call minus 1). If n==2, a plain vanilla cubic
68 bezier is drawn. If n==1, we fall back to a quadratic segment and
69 if n==0 we draw a straight line. It gets interesting when n>2:
70 n-1 PostScript-style cubic segments will be drawn as if it were
jvr23cb2002003-09-07 09:41:28 +000071 one curve. See decomposeSuperBezierSegment().
jvrb369ef32003-08-23 20:19:33 +000072
73 The conversion algorithm used for n>2 is inspired by NURB
74 splines, and is conceptually equivalent to the TrueType "implied
jvr23cb2002003-09-07 09:41:28 +000075 points" principle. See also decomposeQuadraticSegment().
jvrb369ef32003-08-23 20:19:33 +000076 """
77 raise NotImplementedError
78
79 def qCurveTo(self, *points):
80 """Draw a whole string of quadratic curve segments.
81
jvr934fe5f2003-08-28 19:30:46 +000082 The last point specified is on-curve, all others are off-curve
83 points.
jvrb369ef32003-08-23 20:19:33 +000084
jvr934fe5f2003-08-28 19:30:46 +000085 This method implements TrueType-style curves, breaking up curves
86 using 'implied points': between each two consequtive off-curve points,
jvr23cb2002003-09-07 09:41:28 +000087 there is one implied point exactly in the middle between them. See
88 also decomposeQuadraticSegment().
jvr934fe5f2003-08-28 19:30:46 +000089
90 The last argument (normally the on-curve point) may be None.
91 This is to support contours that have NO on-curve points (a rarely
92 seen feature of TrueType outlines).
jvrb369ef32003-08-23 20:19:33 +000093 """
94 raise NotImplementedError
95
96 def closePath(self):
jvr40cde702003-09-06 16:00:03 +000097 """Close the current sub path. You must call either pen.closePath()
98 or pen.endPath() after each sub path.
99 """
100 pass
101
102 def endPath(self):
103 """End the current sub path, but don't close it. You must call
104 either pen.closePath() or pen.endPath() after each sub path.
105 """
jvrb369ef32003-08-23 20:19:33 +0000106 pass
107
108 def addComponent(self, glyphName, transformation):
jvr934fe5f2003-08-28 19:30:46 +0000109 """Add a sub glyph. The 'transformation' argument must be a 6-tuple
110 containing an affine transformation, or a Transform object from the
111 fontTools.misc.transform module. More precisely: it should be a
112 sequence containing 6 numbers.
113 """
jvrb369ef32003-08-23 20:19:33 +0000114 raise NotImplementedError
115
116
Behdad Esfahbod285d7b82013-09-10 20:30:47 -0400117class NullPen(object):
118
119 """A pen that does nothing.
120 """
121
122 def moveTo(self, pt):
123 pass
124
125 def lineTo(self, pt):
126 pass
127
128 def curveTo(self, *points):
129 pass
130
131 def qCurveTo(self, *points):
132 pass
133
134 def closePath(self):
135 pass
136
137 def endPath(self):
138 pass
139
140 def addComponent(self, glyphName, transformation):
141 pass
142
143
jvrb369ef32003-08-23 20:19:33 +0000144class BasePen(AbstractPen):
145
jvr40cde702003-09-06 16:00:03 +0000146 """Base class for drawing pens. You must override _moveTo, _lineTo and
147 _curveToOne. You may additionally override _closePath, _endPath,
148 addComponent and/or _qCurveToOne. You should not override any other
149 methods.
150 """
jvrb369ef32003-08-23 20:19:33 +0000151
152 def __init__(self, glyphSet):
153 self.glyphSet = glyphSet
154 self.__currentPoint = None
155
156 # must override
157
158 def _moveTo(self, pt):
159 raise NotImplementedError
160
161 def _lineTo(self, pt):
162 raise NotImplementedError
163
164 def _curveToOne(self, pt1, pt2, pt3):
165 raise NotImplementedError
166
167 # may override
168
169 def _closePath(self):
170 pass
171
jvr40cde702003-09-06 16:00:03 +0000172 def _endPath(self):
173 pass
174
jvrb369ef32003-08-23 20:19:33 +0000175 def _qCurveToOne(self, pt1, pt2):
176 """This method implements the basic quadratic curve type. The
177 default implementation delegates the work to the cubic curve
178 function. Optionally override with a native implementation.
179 """
180 pt0x, pt0y = self.__currentPoint
181 pt1x, pt1y = pt1
182 pt2x, pt2y = pt2
183 mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
184 mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
185 mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
186 mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
187 self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
188
189 def addComponent(self, glyphName, transformation):
190 """This default implementation simply transforms the points
191 of the base glyph and draws it onto self.
192 """
193 from fontTools.pens.transformPen import TransformPen
jvra5c92982005-04-10 13:18:42 +0000194 try:
195 glyph = self.glyphSet[glyphName]
196 except KeyError:
197 pass
198 else:
jvr2e4cc022005-03-08 09:50:56 +0000199 tPen = TransformPen(self, transformation)
200 glyph.draw(tPen)
jvrb369ef32003-08-23 20:19:33 +0000201
202 # don't override
203
204 def _getCurrentPoint(self):
205 """Return the current point. This is not part of the public
206 interface, yet is useful for subclasses.
207 """
208 return self.__currentPoint
209
210 def closePath(self):
211 self._closePath()
212 self.__currentPoint = None
213
jvr40cde702003-09-06 16:00:03 +0000214 def endPath(self):
215 self._endPath()
216 self.__currentPoint = None
217
jvrb369ef32003-08-23 20:19:33 +0000218 def moveTo(self, pt):
219 self._moveTo(pt)
220 self.__currentPoint = pt
221
222 def lineTo(self, pt):
223 self._lineTo(pt)
224 self.__currentPoint = pt
225
226 def curveTo(self, *points):
227 n = len(points) - 1 # 'n' is the number of control points
228 assert n >= 0
229 if n == 2:
230 # The common case, we have exactly two BCP's, so this is a standard
jvr23cb2002003-09-07 09:41:28 +0000231 # cubic bezier. Even though decomposeSuperBezierSegment() handles
232 # this case just fine, we special-case it anyway since it's so
233 # common.
jvrb369ef32003-08-23 20:19:33 +0000234 self._curveToOne(*points)
235 self.__currentPoint = points[-1]
236 elif n > 2:
237 # n is the number of control points; split curve into n-1 cubic
238 # bezier segments. The algorithm used here is inspired by NURB
239 # splines and the TrueType "implied point" principle, and ensures
240 # the smoothest possible connection between two curve segments,
241 # with no disruption in the curvature. It is practical since it
242 # allows one to construct multiple bezier segments with a much
243 # smaller amount of points.
jvr23cb2002003-09-07 09:41:28 +0000244 _curveToOne = self._curveToOne
245 for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
246 _curveToOne(pt1, pt2, pt3)
247 self.__currentPoint = pt3
jvrb369ef32003-08-23 20:19:33 +0000248 elif n == 1:
jvr49b55212003-09-05 14:18:26 +0000249 self.qCurveTo(*points)
jvrb369ef32003-08-23 20:19:33 +0000250 elif n == 0:
251 self.lineTo(points[0])
252 else:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500253 raise AssertionError("can't get there from here")
jvrb369ef32003-08-23 20:19:33 +0000254
255 def qCurveTo(self, *points):
256 n = len(points) - 1 # 'n' is the number of control points
257 assert n >= 0
jvr934fe5f2003-08-28 19:30:46 +0000258 if points[-1] is None:
259 # Special case for TrueType quadratics: it is possible to
260 # define a contour with NO on-curve points. BasePen supports
261 # this by allowing the final argument (the expected on-curve
262 # point) to be None. We simulate the feature by making the implied
263 # on-curve point between the last and the first off-curve points
264 # explicit.
265 x, y = points[-2] # last off-curve point
266 nx, ny = points[0] # first off-curve point
267 impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
268 self.__currentPoint = impliedStartPoint
269 self._moveTo(impliedStartPoint)
270 points = points[:-1] + (impliedStartPoint,)
jvrb369ef32003-08-23 20:19:33 +0000271 if n > 0:
jvr82ef2a52003-08-23 20:24:42 +0000272 # Split the string of points into discrete quadratic curve
273 # segments. Between any two consecutive off-curve points
274 # there's an implied on-curve point exactly in the middle.
275 # This is where the segment splits.
jvrb369ef32003-08-23 20:19:33 +0000276 _qCurveToOne = self._qCurveToOne
jvr23cb2002003-09-07 09:41:28 +0000277 for pt1, pt2 in decomposeQuadraticSegment(points):
278 _qCurveToOne(pt1, pt2)
279 self.__currentPoint = pt2
jvrb369ef32003-08-23 20:19:33 +0000280 else:
281 self.lineTo(points[0])
282
283
jvr23cb2002003-09-07 09:41:28 +0000284def decomposeSuperBezierSegment(points):
285 """Split the SuperBezier described by 'points' into a list of regular
286 bezier segments. The 'points' argument must be a sequence with length
287 3 or greater, containing (x, y) coordinates. The last point is the
288 destination on-curve point, the rest of the points are off-curve points.
289 The start point should not be supplied.
290
291 This function returns a list of (pt1, pt2, pt3) tuples, which each
292 specify a regular curveto-style bezier segment.
293 """
294 n = len(points) - 1
295 assert n > 1
296 bezierSegments = []
297 pt1, pt2, pt3 = points[0], None, None
298 for i in range(2, n+1):
299 # calculate points in between control points.
300 nDivisions = min(i, 3, n-i+2)
jvr23cb2002003-09-07 09:41:28 +0000301 for j in range(1, nDivisions):
Behdad Esfahbod32c10ee2013-11-27 17:46:17 -0500302 factor = j / nDivisions
jvr23cb2002003-09-07 09:41:28 +0000303 temp1 = points[i-1]
304 temp2 = points[i-2]
305 temp = (temp2[0] + factor * (temp1[0] - temp2[0]),
306 temp2[1] + factor * (temp1[1] - temp2[1]))
307 if pt2 is None:
308 pt2 = temp
309 else:
310 pt3 = (0.5 * (pt2[0] + temp[0]),
311 0.5 * (pt2[1] + temp[1]))
312 bezierSegments.append((pt1, pt2, pt3))
313 pt1, pt2, pt3 = temp, None, None
314 bezierSegments.append((pt1, points[-2], points[-1]))
315 return bezierSegments
316
317
318def decomposeQuadraticSegment(points):
319 """Split the quadratic curve segment described by 'points' into a list
320 of "atomic" quadratic segments. The 'points' argument must be a sequence
321 with length 2 or greater, containing (x, y) coordinates. The last point
322 is the destination on-curve point, the rest of the points are off-curve
323 points. The start point should not be supplied.
324
325 This function returns a list of (pt1, pt2) tuples, which each specify a
326 plain quadratic bezier segment.
327 """
328 n = len(points) - 1
329 assert n > 0
330 quadSegments = []
331 for i in range(n - 1):
332 x, y = points[i]
333 nx, ny = points[i+1]
334 impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
335 quadSegments.append((points[i], impliedPt))
336 quadSegments.append((points[-2], points[-1]))
337 return quadSegments
338
339
jvrb369ef32003-08-23 20:19:33 +0000340class _TestPen(BasePen):
jvr40cde702003-09-06 16:00:03 +0000341 """Test class that prints PostScript to stdout."""
jvrb369ef32003-08-23 20:19:33 +0000342 def _moveTo(self, pt):
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500343 print("%s %s moveto" % (pt[0], pt[1]))
jvrb369ef32003-08-23 20:19:33 +0000344 def _lineTo(self, pt):
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500345 print("%s %s lineto" % (pt[0], pt[1]))
jvrb369ef32003-08-23 20:19:33 +0000346 def _curveToOne(self, bcp1, bcp2, pt):
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500347 print("%s %s %s %s %s %s curveto" % (bcp1[0], bcp1[1],
348 bcp2[0], bcp2[1], pt[0], pt[1]))
jvrb369ef32003-08-23 20:19:33 +0000349 def _closePath(self):
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500350 print("closepath")
jvrb369ef32003-08-23 20:19:33 +0000351
352
353if __name__ == "__main__":
354 pen = _TestPen(None)
355 pen.moveTo((0, 0))
356 pen.lineTo((0, 100))
jvr23cb2002003-09-07 09:41:28 +0000357 pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
jvrb369ef32003-08-23 20:19:33 +0000358 pen.closePath()
359
360 pen = _TestPen(None)
jvr934fe5f2003-08-28 19:30:46 +0000361 # testing the "no on-curve point" scenario
362 pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
jvrb369ef32003-08-23 20:19:33 +0000363 pen.closePath()