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 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. 25 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. 37 """ 38 39 from __future__ import print_function, division, absolute_import 40 from fontTools.misc.py23 import * 41 42 __all__ = ["AbstractPen", "NullPen", "BasePen", 43 "decomposeSuperBezierSegment", "decomposeQuadraticSegment"] 44 45 46 class AbstractPen(object): 47 48 def moveTo(self, pt): 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 """ 52 raise NotImplementedError 53 54 def lineTo(self, pt): 55 """Draw a straight line from the current point to 'pt'.""" 56 raise NotImplementedError 57 58 def curveTo(self, *points): 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: 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 71 one curve. See decomposeSuperBezierSegment(). 72 73 The conversion algorithm used for n>2 is inspired by NURB 74 splines, and is conceptually equivalent to the TrueType "implied 75 points" principle. See also decomposeQuadraticSegment(). 76 """ 77 raise NotImplementedError 78 79 def qCurveTo(self, *points): 80 """Draw a whole string of quadratic curve segments. 81 82 The last point specified is on-curve, all others are off-curve 83 points. 84 85 This method implements TrueType-style curves, breaking up curves 86 using 'implied points': between each two consequtive off-curve points, 87 there is one implied point exactly in the middle between them. See 88 also decomposeQuadraticSegment(). 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). 93 """ 94 raise NotImplementedError 95 96 def closePath(self): 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 """ 106 pass 107 108 def addComponent(self, glyphName, transformation): 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 """ 114 raise NotImplementedError 115 116 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 144 class BasePen(AbstractPen): 145 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 """ 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 172 def _endPath(self): 173 pass 174 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 194 try: 195 glyph = self.glyphSet[glyphName] 196 except KeyError: 197 pass 198 else: 199 tPen = TransformPen(self, transformation) 200 glyph.draw(tPen) 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 214 def endPath(self): 215 self._endPath() 216 self.__currentPoint = None 217 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 231 # cubic bezier. Even though decomposeSuperBezierSegment() handles 232 # this case just fine, we special-case it anyway since it's so 233 # common. 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. 244 _curveToOne = self._curveToOne 245 for pt1, pt2, pt3 in decomposeSuperBezierSegment(points): 246 _curveToOne(pt1, pt2, pt3) 247 self.__currentPoint = pt3 248 elif n == 1: 249 self.qCurveTo(*points) 250 elif n == 0: 251 self.lineTo(points[0]) 252 else: 253 raise AssertionError("can't get there from here") 254 255 def qCurveTo(self, *points): 256 n = len(points) - 1 # 'n' is the number of control points 257 assert n >= 0 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,) 271 if n > 0: 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. 276 _qCurveToOne = self._qCurveToOne 277 for pt1, pt2 in decomposeQuadraticSegment(points): 278 _qCurveToOne(pt1, pt2) 279 self.__currentPoint = pt2 280 else: 281 self.lineTo(points[0]) 282 283 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) 301 for j in range(1, nDivisions): 302 factor = j / nDivisions 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 340 class _TestPen(BasePen): 341 """Test class that prints PostScript to stdout.""" 342 def _moveTo(self, pt): 343 print("%s %s moveto" % (pt[0], pt[1])) 344 def _lineTo(self, pt): 345 print("%s %s lineto" % (pt[0], pt[1])) 346 def _curveToOne(self, bcp1, bcp2, pt): 347 print("%s %s %s %s %s %s curveto" % (bcp1[0], bcp1[1], 348 bcp2[0], bcp2[1], pt[0], pt[1])) 349 def _closePath(self): 350 print("closepath") 351 352 353 if __name__ == "__main__": 354 pen = _TestPen(None) 355 pen.moveTo((0, 0)) 356 pen.lineTo((0, 100)) 357 pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0)) 358 pen.closePath() 359 360 pen = _TestPen(None) 361 # testing the "no on-curve point" scenario 362 pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None) 363 pen.closePath() 364