1 """ 2 ========= 3 PointPens 4 ========= 5 6 Where **SegmentPens** have an intuitive approach to drawing 7 (if you're familiar with postscript anyway), the **PointPen** 8 is geared towards accessing all the data in the contours of 9 the glyph. A PointPen has a very simple interface, it just 10 steps through all the points in a call from glyph.drawPoints(). 11 This allows the caller to provide more data for each point. 12 For instance, whether or not a point is smooth, and its name. 13 """ 14 from __future__ import absolute_import, unicode_literals 15 from fontTools.pens.basePen import AbstractPen 16 import math 17 18 __all__ = [ 19 "AbstractPointPen", 20 "BasePointToSegmentPen", 21 "PointToSegmentPen", 22 "SegmentToPointPen", 23 "GuessSmoothPointPen", 24 "ReverseContourPointPen", 25 ] 26 27 28 class AbstractPointPen(object): 29 """ 30 Baseclass for all PointPens. 31 """ 32 33 def beginPath(self, identifier=None, **kwargs): 34 """Start a new sub path.""" 35 raise NotImplementedError 36 37 def endPath(self): 38 """End the current sub path.""" 39 raise NotImplementedError 40 41 def addPoint(self, pt, segmentType=None, smooth=False, name=None, 42 identifier=None, **kwargs): 43 """Add a point to the current sub path.""" 44 raise NotImplementedError 45 46 def addComponent(self, baseGlyphName, transformation, identifier=None, 47 **kwargs): 48 """Add a sub glyph.""" 49 raise NotImplementedError 50 51 52 class BasePointToSegmentPen(AbstractPointPen): 53 """ 54 Base class for retrieving the outline in a segment-oriented 55 way. The PointPen protocol is simple yet also a little tricky, 56 so when you need an outline presented as segments but you have 57 as points, do use this base implementation as it properly takes 58 care of all the edge cases. 59 """ 60 61 def __init__(self): 62 self.currentPath = None 63 64 def beginPath(self, identifier=None, **kwargs): 65 assert self.currentPath is None 66 self.currentPath = [] 67 68 def _flushContour(self, segments): 69 """Override this method. 70 71 It will be called for each non-empty sub path with a list 72 of segments: the 'segments' argument. 73 74 The segments list contains tuples of length 2: 75 (segmentType, points) 76 77 segmentType is one of "move", "line", "curve" or "qcurve". 78 "move" may only occur as the first segment, and it signifies 79 an OPEN path. A CLOSED path does NOT start with a "move", in 80 fact it will not contain a "move" at ALL. 81 82 The 'points' field in the 2-tuple is a list of point info 83 tuples. The list has 1 or more items, a point tuple has 84 four items: 85 (point, smooth, name, kwargs) 86 'point' is an (x, y) coordinate pair. 87 88 For a closed path, the initial moveTo point is defined as 89 the last point of the last segment. 90 91 The 'points' list of "move" and "line" segments always contains 92 exactly one point tuple. 93 """ 94 raise NotImplementedError 95 96 def endPath(self): 97 assert self.currentPath is not None 98 points = self.currentPath 99 self.currentPath = None 100 if not points: 101 return 102 if len(points) == 1: 103 # Not much more we can do than output a single move segment. 104 pt, segmentType, smooth, name, kwargs = points[0] 105 segments = [("move", [(pt, smooth, name, kwargs)])] 106 self._flushContour(segments) 107 return 108 segments = [] 109 if points[0][1] == "move": 110 # It's an open contour, insert a "move" segment for the first 111 # point and remove that first point from the point list. 112 pt, segmentType, smooth, name, kwargs = points[0] 113 segments.append(("move", [(pt, smooth, name, kwargs)])) 114 points.pop(0) 115 else: 116 # It's a closed contour. Locate the first on-curve point, and 117 # rotate the point list so that it _ends_ with an on-curve 118 # point. 119 firstOnCurve = None 120 for i in range(len(points)): 121 segmentType = points[i][1] 122 if segmentType is not None: 123 firstOnCurve = i 124 break 125 if firstOnCurve is None: 126 # Special case for quadratics: a contour with no on-curve 127 # points. Add a "None" point. (See also the Pen protocol's 128 # qCurveTo() method and fontTools.pens.basePen.py.) 129 points.append((None, "qcurve", None, None, None)) 130 else: 131 points = points[firstOnCurve+1:] + points[:firstOnCurve+1] 132 133 currentSegment = [] 134 for pt, segmentType, smooth, name, kwargs in points: 135 currentSegment.append((pt, smooth, name, kwargs)) 136 if segmentType is None: 137 continue 138 segments.append((segmentType, currentSegment)) 139 currentSegment = [] 140 141 self._flushContour(segments) 142 143 def addPoint(self, pt, segmentType=None, smooth=False, name=None, 144 identifier=None, **kwargs): 145 self.currentPath.append((pt, segmentType, smooth, name, kwargs)) 146 147 148 class PointToSegmentPen(BasePointToSegmentPen): 149 """ 150 Adapter class that converts the PointPen protocol to the 151 (Segment)Pen protocol. 152 """ 153 154 def __init__(self, segmentPen, outputImpliedClosingLine=False): 155 BasePointToSegmentPen.__init__(self) 156 self.pen = segmentPen 157 self.outputImpliedClosingLine = outputImpliedClosingLine 158 159 def _flushContour(self, segments): 160 assert len(segments) >= 1 161 pen = self.pen 162 if segments[0][0] == "move": 163 # It's an open path. 164 closed = False 165 points = segments[0][1] 166 assert len(points) == 1, "illegal move segment point count: %d" % len(points) 167 movePt, smooth, name, kwargs = points[0] 168 del segments[0] 169 else: 170 # It's a closed path, do a moveTo to the last 171 # point of the last segment. 172 closed = True 173 segmentType, points = segments[-1] 174 movePt, smooth, name, kwargs = points[-1] 175 if movePt is None: 176 # quad special case: a contour with no on-curve points contains 177 # one "qcurve" segment that ends with a point that's None. We 178 # must not output a moveTo() in that case. 179 pass 180 else: 181 pen.moveTo(movePt) 182 outputImpliedClosingLine = self.outputImpliedClosingLine 183 nSegments = len(segments) 184 for i in range(nSegments): 185 segmentType, points = segments[i] 186 points = [pt for pt, smooth, name, kwargs in points] 187 if segmentType == "line": 188 assert len(points) == 1, "illegal line segment point count: %d" % len(points) 189 pt = points[0] 190 if i + 1 != nSegments or outputImpliedClosingLine or not closed: 191 pen.lineTo(pt) 192 elif segmentType == "curve": 193 pen.curveTo(*points) 194 elif segmentType == "qcurve": 195 pen.qCurveTo(*points) 196 else: 197 assert 0, "illegal segmentType: %s" % segmentType 198 if closed: 199 pen.closePath() 200 else: 201 pen.endPath() 202 203 def addComponent(self, glyphName, transform, identifier=None, **kwargs): 204 del identifier # unused 205 self.pen.addComponent(glyphName, transform) 206 207 208 class SegmentToPointPen(AbstractPen): 209 """ 210 Adapter class that converts the (Segment)Pen protocol to the 211 PointPen protocol. 212 """ 213 214 def __init__(self, pointPen, guessSmooth=True): 215 if guessSmooth: 216 self.pen = GuessSmoothPointPen(pointPen) 217 else: 218 self.pen = pointPen 219 self.contour = None 220 221 def _flushContour(self): 222 pen = self.pen 223 pen.beginPath() 224 for pt, segmentType in self.contour: 225 pen.addPoint(pt, segmentType=segmentType) 226 pen.endPath() 227 228 def moveTo(self, pt): 229 self.contour = [] 230 self.contour.append((pt, "move")) 231 232 def lineTo(self, pt): 233 self.contour.append((pt, "line")) 234 235 def curveTo(self, *pts): 236 for pt in pts[:-1]: 237 self.contour.append((pt, None)) 238 self.contour.append((pts[-1], "curve")) 239 240 def qCurveTo(self, *pts): 241 if pts[-1] is None: 242 self.contour = [] 243 for pt in pts[:-1]: 244 self.contour.append((pt, None)) 245 if pts[-1] is not None: 246 self.contour.append((pts[-1], "qcurve")) 247 248 def closePath(self): 249 if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]: 250 self.contour[0] = self.contour[-1] 251 del self.contour[-1] 252 else: 253 # There's an implied line at the end, replace "move" with "line" 254 # for the first point 255 pt, tp = self.contour[0] 256 if tp == "move": 257 self.contour[0] = pt, "line" 258 self._flushContour() 259 self.contour = None 260 261 def endPath(self): 262 self._flushContour() 263 self.contour = None 264 265 def addComponent(self, glyphName, transform): 266 assert self.contour is None 267 self.pen.addComponent(glyphName, transform) 268 269 270 class GuessSmoothPointPen(AbstractPointPen): 271 """ 272 Filtering PointPen that tries to determine whether an on-curve point 273 should be "smooth", ie. that it's a "tangent" point or a "curve" point. 274 """ 275 276 def __init__(self, outPen): 277 self._outPen = outPen 278 self._points = None 279 280 def _flushContour(self): 281 points = self._points 282 nPoints = len(points) 283 if not nPoints: 284 return 285 if points[0][1] == "move": 286 # Open path. 287 indices = range(1, nPoints - 1) 288 elif nPoints > 1: 289 # Closed path. To avoid having to mod the contour index, we 290 # simply abuse Python's negative index feature, and start at -1 291 indices = range(-1, nPoints - 1) 292 else: 293 # closed path containing 1 point (!), ignore. 294 indices = [] 295 for i in indices: 296 pt, segmentType, dummy, name, kwargs = points[i] 297 if segmentType is None: 298 continue 299 prev = i - 1 300 next = i + 1 301 if points[prev][1] is not None and points[next][1] is not None: 302 continue 303 # At least one of our neighbors is an off-curve point 304 pt = points[i][0] 305 prevPt = points[prev][0] 306 nextPt = points[next][0] 307 if pt != prevPt and pt != nextPt: 308 dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1] 309 dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1] 310 a1 = math.atan2(dx1, dy1) 311 a2 = math.atan2(dx2, dy2) 312 if abs(a1 - a2) < 0.05: 313 points[i] = pt, segmentType, True, name, kwargs 314 315 for pt, segmentType, smooth, name, kwargs in points: 316 self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) 317 318 def beginPath(self, identifier=None, **kwargs): 319 assert self._points is None 320 self._points = [] 321 if identifier is not None: 322 kwargs["identifier"] = identifier 323 self._outPen.beginPath(**kwargs) 324 325 def endPath(self): 326 self._flushContour() 327 self._outPen.endPath() 328 self._points = None 329 330 def addPoint(self, pt, segmentType=None, smooth=False, name=None, 331 identifier=None, **kwargs): 332 if identifier is not None: 333 kwargs["identifier"] = identifier 334 self._points.append((pt, segmentType, False, name, kwargs)) 335 336 def addComponent(self, glyphName, transformation, identifier=None, **kwargs): 337 assert self._points is None 338 if identifier is not None: 339 kwargs["identifier"] = identifier 340 self._outPen.addComponent(glyphName, transformation, **kwargs) 341 342 343 class ReverseContourPointPen(AbstractPointPen): 344 """ 345 This is a PointPen that passes outline data to another PointPen, but 346 reversing the winding direction of all contours. Components are simply 347 passed through unchanged. 348 349 Closed contours are reversed in such a way that the first point remains 350 the first point. 351 """ 352 353 def __init__(self, outputPointPen): 354 self.pen = outputPointPen 355 # a place to store the points for the current sub path 356 self.currentContour = None 357 358 def _flushContour(self): 359 pen = self.pen 360 contour = self.currentContour 361 if not contour: 362 pen.beginPath(identifier=self.currentContourIdentifier) 363 pen.endPath() 364 return 365 366 closed = contour[0][1] != "move" 367 if not closed: 368 lastSegmentType = "move" 369 else: 370 # Remove the first point and insert it at the end. When 371 # the list of points gets reversed, this point will then 372 # again be at the start. In other words, the following 373 # will hold: 374 # for N in range(len(originalContour)): 375 # originalContour[N] == reversedContour[-N] 376 contour.append(contour.pop(0)) 377 # Find the first on-curve point. 378 firstOnCurve = None 379 for i in range(len(contour)): 380 if contour[i][1] is not None: 381 firstOnCurve = i 382 break 383 if firstOnCurve is None: 384 # There are no on-curve points, be basically have to 385 # do nothing but contour.reverse(). 386 lastSegmentType = None 387 else: 388 lastSegmentType = contour[firstOnCurve][1] 389 390 contour.reverse() 391 if not closed: 392 # Open paths must start with a move, so we simply dump 393 # all off-curve points leading up to the first on-curve. 394 while contour[0][1] is None: 395 contour.pop(0) 396 pen.beginPath(identifier=self.currentContourIdentifier) 397 for pt, nextSegmentType, smooth, name, kwargs in contour: 398 if nextSegmentType is not None: 399 segmentType = lastSegmentType 400 lastSegmentType = nextSegmentType 401 else: 402 segmentType = None 403 pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs) 404 pen.endPath() 405 406 def beginPath(self, identifier=None, **kwargs): 407 assert self.currentContour is None 408 self.currentContour = [] 409 self.currentContourIdentifier = identifier 410 self.onCurve = [] 411 412 def endPath(self): 413 assert self.currentContour is not None 414 self._flushContour() 415 self.currentContour = None 416 417 def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): 418 self.currentContour.append((pt, segmentType, smooth, name, kwargs)) 419 420 def addComponent(self, glyphName, transform, identifier=None, **kwargs): 421 assert self.currentContour is None 422 self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs) 423