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