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