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