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