basePen.py revision 285d7b81d3a1d9d060864438580f05c2b44366ff
1"""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 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 fontTools.pens.transformPen.TransformPen. 25 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. 34 35Coordinates are usually expressed as (x, y) tuples, but generally any 36sequence of length 2 will do. 37""" 38 39 40__all__ = ["AbstractPen", "NullPen", "BasePen", 41 "decomposeSuperBezierSegment", "decomposeQuadraticSegment"] 42 43 44class AbstractPen(object): 45 46 def moveTo(self, pt): 47 """Begin a new sub path, set the current point to 'pt'. You must 48 end each sub path with a call to pen.closePath() or pen.endPath(). 49 """ 50 raise NotImplementedError 51 52 def lineTo(self, pt): 53 """Draw a straight line from the current point to 'pt'.""" 54 raise NotImplementedError 55 56 def curveTo(self, *points): 57 """Draw a cubic bezier with an arbitrary number of control points. 58 59 The last point specified is on-curve, all others are off-curve 60 (control) points. If the number of control points is > 2, the 61 segment is split into multiple bezier segments. This works 62 like this: 63 64 Let n be the number of control points (which is the number of 65 arguments to this call minus 1). If n==2, a plain vanilla cubic 66 bezier is drawn. If n==1, we fall back to a quadratic segment and 67 if n==0 we draw a straight line. It gets interesting when n>2: 68 n-1 PostScript-style cubic segments will be drawn as if it were 69 one curve. See decomposeSuperBezierSegment(). 70 71 The conversion algorithm used for n>2 is inspired by NURB 72 splines, and is conceptually equivalent to the TrueType "implied 73 points" principle. See also decomposeQuadraticSegment(). 74 """ 75 raise NotImplementedError 76 77 def qCurveTo(self, *points): 78 """Draw a whole string of quadratic curve segments. 79 80 The last point specified is on-curve, all others are off-curve 81 points. 82 83 This method implements TrueType-style curves, breaking up curves 84 using 'implied points': between each two consequtive off-curve points, 85 there is one implied point exactly in the middle between them. See 86 also decomposeQuadraticSegment(). 87 88 The last argument (normally the on-curve point) may be None. 89 This is to support contours that have NO on-curve points (a rarely 90 seen feature of TrueType outlines). 91 """ 92 raise NotImplementedError 93 94 def closePath(self): 95 """Close the current sub path. You must call either pen.closePath() 96 or pen.endPath() after each sub path. 97 """ 98 pass 99 100 def endPath(self): 101 """End the current sub path, but don't close it. You must call 102 either pen.closePath() or pen.endPath() after each sub path. 103 """ 104 pass 105 106 def addComponent(self, glyphName, transformation): 107 """Add a sub glyph. The 'transformation' argument must be a 6-tuple 108 containing an affine transformation, or a Transform object from the 109 fontTools.misc.transform module. More precisely: it should be a 110 sequence containing 6 numbers. 111 """ 112 raise NotImplementedError 113 114 115class NullPen(object): 116 117 """A pen that does nothing. 118 """ 119 120 def moveTo(self, pt): 121 pass 122 123 def lineTo(self, pt): 124 pass 125 126 def curveTo(self, *points): 127 pass 128 129 def qCurveTo(self, *points): 130 pass 131 132 def closePath(self): 133 pass 134 135 def endPath(self): 136 pass 137 138 def addComponent(self, glyphName, transformation): 139 pass 140 141 142class BasePen(AbstractPen): 143 144 """Base class for drawing pens. You must override _moveTo, _lineTo and 145 _curveToOne. You may additionally override _closePath, _endPath, 146 addComponent and/or _qCurveToOne. You should not override any other 147 methods. 148 """ 149 150 def __init__(self, glyphSet): 151 self.glyphSet = glyphSet 152 self.__currentPoint = None 153 154 # must override 155 156 def _moveTo(self, pt): 157 raise NotImplementedError 158 159 def _lineTo(self, pt): 160 raise NotImplementedError 161 162 def _curveToOne(self, pt1, pt2, pt3): 163 raise NotImplementedError 164 165 # may override 166 167 def _closePath(self): 168 pass 169 170 def _endPath(self): 171 pass 172 173 def _qCurveToOne(self, pt1, pt2): 174 """This method implements the basic quadratic curve type. The 175 default implementation delegates the work to the cubic curve 176 function. Optionally override with a native implementation. 177 """ 178 pt0x, pt0y = self.__currentPoint 179 pt1x, pt1y = pt1 180 pt2x, pt2y = pt2 181 mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x) 182 mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y) 183 mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x) 184 mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y) 185 self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2) 186 187 def addComponent(self, glyphName, transformation): 188 """This default implementation simply transforms the points 189 of the base glyph and draws it onto self. 190 """ 191 from fontTools.pens.transformPen import TransformPen 192 try: 193 glyph = self.glyphSet[glyphName] 194 except KeyError: 195 pass 196 else: 197 tPen = TransformPen(self, transformation) 198 glyph.draw(tPen) 199 200 # don't override 201 202 def _getCurrentPoint(self): 203 """Return the current point. This is not part of the public 204 interface, yet is useful for subclasses. 205 """ 206 return self.__currentPoint 207 208 def closePath(self): 209 self._closePath() 210 self.__currentPoint = None 211 212 def endPath(self): 213 self._endPath() 214 self.__currentPoint = None 215 216 def moveTo(self, pt): 217 self._moveTo(pt) 218 self.__currentPoint = pt 219 220 def lineTo(self, pt): 221 self._lineTo(pt) 222 self.__currentPoint = pt 223 224 def curveTo(self, *points): 225 n = len(points) - 1 # 'n' is the number of control points 226 assert n >= 0 227 if n == 2: 228 # The common case, we have exactly two BCP's, so this is a standard 229 # cubic bezier. Even though decomposeSuperBezierSegment() handles 230 # this case just fine, we special-case it anyway since it's so 231 # common. 232 self._curveToOne(*points) 233 self.__currentPoint = points[-1] 234 elif n > 2: 235 # n is the number of control points; split curve into n-1 cubic 236 # bezier segments. The algorithm used here is inspired by NURB 237 # splines and the TrueType "implied point" principle, and ensures 238 # the smoothest possible connection between two curve segments, 239 # with no disruption in the curvature. It is practical since it 240 # allows one to construct multiple bezier segments with a much 241 # smaller amount of points. 242 _curveToOne = self._curveToOne 243 for pt1, pt2, pt3 in decomposeSuperBezierSegment(points): 244 _curveToOne(pt1, pt2, pt3) 245 self.__currentPoint = pt3 246 elif n == 1: 247 self.qCurveTo(*points) 248 elif n == 0: 249 self.lineTo(points[0]) 250 else: 251 raise AssertionError, "can't get there from here" 252 253 def qCurveTo(self, *points): 254 n = len(points) - 1 # 'n' is the number of control points 255 assert n >= 0 256 if points[-1] is None: 257 # Special case for TrueType quadratics: it is possible to 258 # define a contour with NO on-curve points. BasePen supports 259 # this by allowing the final argument (the expected on-curve 260 # point) to be None. We simulate the feature by making the implied 261 # on-curve point between the last and the first off-curve points 262 # explicit. 263 x, y = points[-2] # last off-curve point 264 nx, ny = points[0] # first off-curve point 265 impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny)) 266 self.__currentPoint = impliedStartPoint 267 self._moveTo(impliedStartPoint) 268 points = points[:-1] + (impliedStartPoint,) 269 if n > 0: 270 # Split the string of points into discrete quadratic curve 271 # segments. Between any two consecutive off-curve points 272 # there's an implied on-curve point exactly in the middle. 273 # This is where the segment splits. 274 _qCurveToOne = self._qCurveToOne 275 for pt1, pt2 in decomposeQuadraticSegment(points): 276 _qCurveToOne(pt1, pt2) 277 self.__currentPoint = pt2 278 else: 279 self.lineTo(points[0]) 280 281 282def decomposeSuperBezierSegment(points): 283 """Split the SuperBezier described by 'points' into a list of regular 284 bezier segments. The 'points' argument must be a sequence with length 285 3 or greater, containing (x, y) coordinates. The last point is the 286 destination on-curve point, the rest of the points are off-curve points. 287 The start point should not be supplied. 288 289 This function returns a list of (pt1, pt2, pt3) tuples, which each 290 specify a regular curveto-style bezier segment. 291 """ 292 n = len(points) - 1 293 assert n > 1 294 bezierSegments = [] 295 pt1, pt2, pt3 = points[0], None, None 296 for i in range(2, n+1): 297 # calculate points in between control points. 298 nDivisions = min(i, 3, n-i+2) 299 d = float(nDivisions) 300 for j in range(1, nDivisions): 301 factor = j / d 302 temp1 = points[i-1] 303 temp2 = points[i-2] 304 temp = (temp2[0] + factor * (temp1[0] - temp2[0]), 305 temp2[1] + factor * (temp1[1] - temp2[1])) 306 if pt2 is None: 307 pt2 = temp 308 else: 309 pt3 = (0.5 * (pt2[0] + temp[0]), 310 0.5 * (pt2[1] + temp[1])) 311 bezierSegments.append((pt1, pt2, pt3)) 312 pt1, pt2, pt3 = temp, None, None 313 bezierSegments.append((pt1, points[-2], points[-1])) 314 return bezierSegments 315 316 317def decomposeQuadraticSegment(points): 318 """Split the quadratic curve segment described by 'points' into a list 319 of "atomic" quadratic segments. The 'points' argument must be a sequence 320 with length 2 or greater, containing (x, y) coordinates. The last point 321 is the destination on-curve point, the rest of the points are off-curve 322 points. The start point should not be supplied. 323 324 This function returns a list of (pt1, pt2) tuples, which each specify a 325 plain quadratic bezier segment. 326 """ 327 n = len(points) - 1 328 assert n > 0 329 quadSegments = [] 330 for i in range(n - 1): 331 x, y = points[i] 332 nx, ny = points[i+1] 333 impliedPt = (0.5 * (x + nx), 0.5 * (y + ny)) 334 quadSegments.append((points[i], impliedPt)) 335 quadSegments.append((points[-2], points[-1])) 336 return quadSegments 337 338 339class _TestPen(BasePen): 340 """Test class that prints PostScript to stdout.""" 341 def _moveTo(self, pt): 342 print "%s %s moveto" % (pt[0], pt[1]) 343 def _lineTo(self, pt): 344 print "%s %s lineto" % (pt[0], pt[1]) 345 def _curveToOne(self, bcp1, bcp2, pt): 346 print "%s %s %s %s %s %s curveto" % (bcp1[0], bcp1[1], 347 bcp2[0], bcp2[1], pt[0], pt[1]) 348 def _closePath(self): 349 print "closepath" 350 351 352if __name__ == "__main__": 353 pen = _TestPen(None) 354 pen.moveTo((0, 0)) 355 pen.lineTo((0, 100)) 356 pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0)) 357 pen.closePath() 358 359 pen = _TestPen(None) 360 # testing the "no on-curve point" scenario 361 pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None) 362 pen.closePath() 363