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