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 39from __future__ import print_function, division, absolute_import 40from fontTools.misc.py23 import * 41 42__all__ = ["AbstractPen", "NullPen", "BasePen", 43 "decomposeSuperBezierSegment", "decomposeQuadraticSegment"] 44 45 46class 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 117class 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 144class 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 284def 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 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 340class _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 353if __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