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