jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 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 | |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 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 fontTools.pens.transformPen.TransformPen. |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 25 | |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 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. |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 37 | """ |
| 38 | |
Behdad Esfahbod | 1ae2959 | 2014-01-14 15:07:50 +0800 | [diff] [blame^] | 39 | from __future__ import print_function, division, absolute_import |
Behdad Esfahbod | 7ed91ec | 2013-11-27 15:16:28 -0500 | [diff] [blame] | 40 | from fontTools.misc.py23 import * |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 41 | |
Behdad Esfahbod | 285d7b8 | 2013-09-10 20:30:47 -0400 | [diff] [blame] | 42 | __all__ = ["AbstractPen", "NullPen", "BasePen", |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 43 | "decomposeSuperBezierSegment", "decomposeQuadraticSegment"] |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 44 | |
| 45 | |
jvr | 91bca42 | 2012-10-18 12:49:22 +0000 | [diff] [blame] | 46 | class AbstractPen(object): |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 47 | |
| 48 | def moveTo(self, pt): |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 49 | """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 | """ |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 52 | raise NotImplementedError |
| 53 | |
| 54 | def lineTo(self, pt): |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 55 | """Draw a straight line from the current point to 'pt'.""" |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 56 | raise NotImplementedError |
| 57 | |
| 58 | def curveTo(self, *points): |
jvr | 934fe5f | 2003-08-28 19:30:46 +0000 | [diff] [blame] | 59 | """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: |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 65 | |
| 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 |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 71 | one curve. See decomposeSuperBezierSegment(). |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 72 | |
| 73 | The conversion algorithm used for n>2 is inspired by NURB |
| 74 | splines, and is conceptually equivalent to the TrueType "implied |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 75 | points" principle. See also decomposeQuadraticSegment(). |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 76 | """ |
| 77 | raise NotImplementedError |
| 78 | |
| 79 | def qCurveTo(self, *points): |
| 80 | """Draw a whole string of quadratic curve segments. |
| 81 | |
jvr | 934fe5f | 2003-08-28 19:30:46 +0000 | [diff] [blame] | 82 | The last point specified is on-curve, all others are off-curve |
| 83 | points. |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 84 | |
jvr | 934fe5f | 2003-08-28 19:30:46 +0000 | [diff] [blame] | 85 | This method implements TrueType-style curves, breaking up curves |
| 86 | using 'implied points': between each two consequtive off-curve points, |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 87 | there is one implied point exactly in the middle between them. See |
| 88 | also decomposeQuadraticSegment(). |
jvr | 934fe5f | 2003-08-28 19:30:46 +0000 | [diff] [blame] | 89 | |
| 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). |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 93 | """ |
| 94 | raise NotImplementedError |
| 95 | |
| 96 | def closePath(self): |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 97 | """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 | """ |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 106 | pass |
| 107 | |
| 108 | def addComponent(self, glyphName, transformation): |
jvr | 934fe5f | 2003-08-28 19:30:46 +0000 | [diff] [blame] | 109 | """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 | """ |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 114 | raise NotImplementedError |
| 115 | |
| 116 | |
Behdad Esfahbod | 285d7b8 | 2013-09-10 20:30:47 -0400 | [diff] [blame] | 117 | class 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 | |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 144 | class BasePen(AbstractPen): |
| 145 | |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 146 | """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 | """ |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 151 | |
| 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 | |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 172 | def _endPath(self): |
| 173 | pass |
| 174 | |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 175 | 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 |
jvr | a5c9298 | 2005-04-10 13:18:42 +0000 | [diff] [blame] | 194 | try: |
| 195 | glyph = self.glyphSet[glyphName] |
| 196 | except KeyError: |
| 197 | pass |
| 198 | else: |
jvr | 2e4cc02 | 2005-03-08 09:50:56 +0000 | [diff] [blame] | 199 | tPen = TransformPen(self, transformation) |
| 200 | glyph.draw(tPen) |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 201 | |
| 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 | |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 214 | def endPath(self): |
| 215 | self._endPath() |
| 216 | self.__currentPoint = None |
| 217 | |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 218 | 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 |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 231 | # cubic bezier. Even though decomposeSuperBezierSegment() handles |
| 232 | # this case just fine, we special-case it anyway since it's so |
| 233 | # common. |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 234 | 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. |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 244 | _curveToOne = self._curveToOne |
| 245 | for pt1, pt2, pt3 in decomposeSuperBezierSegment(points): |
| 246 | _curveToOne(pt1, pt2, pt3) |
| 247 | self.__currentPoint = pt3 |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 248 | elif n == 1: |
jvr | 49b5521 | 2003-09-05 14:18:26 +0000 | [diff] [blame] | 249 | self.qCurveTo(*points) |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 250 | elif n == 0: |
| 251 | self.lineTo(points[0]) |
| 252 | else: |
Behdad Esfahbod | cd5aad9 | 2013-11-27 02:42:28 -0500 | [diff] [blame] | 253 | raise AssertionError("can't get there from here") |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 254 | |
| 255 | def qCurveTo(self, *points): |
| 256 | n = len(points) - 1 # 'n' is the number of control points |
| 257 | assert n >= 0 |
jvr | 934fe5f | 2003-08-28 19:30:46 +0000 | [diff] [blame] | 258 | 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,) |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 271 | if n > 0: |
jvr | 82ef2a5 | 2003-08-23 20:24:42 +0000 | [diff] [blame] | 272 | # 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. |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 276 | _qCurveToOne = self._qCurveToOne |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 277 | for pt1, pt2 in decomposeQuadraticSegment(points): |
| 278 | _qCurveToOne(pt1, pt2) |
| 279 | self.__currentPoint = pt2 |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 280 | else: |
| 281 | self.lineTo(points[0]) |
| 282 | |
| 283 | |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 284 | def 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) |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 301 | for j in range(1, nDivisions): |
Behdad Esfahbod | 32c10ee | 2013-11-27 17:46:17 -0500 | [diff] [blame] | 302 | factor = j / nDivisions |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 303 | 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 | |
| 318 | def 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 | |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 340 | class _TestPen(BasePen): |
jvr | 40cde70 | 2003-09-06 16:00:03 +0000 | [diff] [blame] | 341 | """Test class that prints PostScript to stdout.""" |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 342 | def _moveTo(self, pt): |
Behdad Esfahbod | 3ec6a25 | 2013-11-27 04:57:33 -0500 | [diff] [blame] | 343 | print("%s %s moveto" % (pt[0], pt[1])) |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 344 | def _lineTo(self, pt): |
Behdad Esfahbod | 3ec6a25 | 2013-11-27 04:57:33 -0500 | [diff] [blame] | 345 | print("%s %s lineto" % (pt[0], pt[1])) |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 346 | def _curveToOne(self, bcp1, bcp2, pt): |
Behdad Esfahbod | 3ec6a25 | 2013-11-27 04:57:33 -0500 | [diff] [blame] | 347 | print("%s %s %s %s %s %s curveto" % (bcp1[0], bcp1[1], |
| 348 | bcp2[0], bcp2[1], pt[0], pt[1])) |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 349 | def _closePath(self): |
Behdad Esfahbod | 3ec6a25 | 2013-11-27 04:57:33 -0500 | [diff] [blame] | 350 | print("closepath") |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 351 | |
| 352 | |
| 353 | if __name__ == "__main__": |
| 354 | pen = _TestPen(None) |
| 355 | pen.moveTo((0, 0)) |
| 356 | pen.lineTo((0, 100)) |
jvr | 23cb200 | 2003-09-07 09:41:28 +0000 | [diff] [blame] | 357 | pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0)) |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 358 | pen.closePath() |
| 359 | |
| 360 | pen = _TestPen(None) |
jvr | 934fe5f | 2003-08-28 19:30:46 +0000 | [diff] [blame] | 361 | # testing the "no on-curve point" scenario |
| 362 | pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None) |
jvr | b369ef3 | 2003-08-23 20:19:33 +0000 | [diff] [blame] | 363 | pen.closePath() |