1*e1fe3e4aSElliott Hughes""" 2*e1fe3e4aSElliott Hughes========= 3*e1fe3e4aSElliott HughesPointPens 4*e1fe3e4aSElliott Hughes========= 5*e1fe3e4aSElliott Hughes 6*e1fe3e4aSElliott HughesWhere **SegmentPens** have an intuitive approach to drawing 7*e1fe3e4aSElliott Hughes(if you're familiar with postscript anyway), the **PointPen** 8*e1fe3e4aSElliott Hughesis geared towards accessing all the data in the contours of 9*e1fe3e4aSElliott Hughesthe glyph. A PointPen has a very simple interface, it just 10*e1fe3e4aSElliott Hughessteps through all the points in a call from glyph.drawPoints(). 11*e1fe3e4aSElliott HughesThis allows the caller to provide more data for each point. 12*e1fe3e4aSElliott HughesFor instance, whether or not a point is smooth, and its name. 13*e1fe3e4aSElliott Hughes""" 14*e1fe3e4aSElliott Hughes 15*e1fe3e4aSElliott Hughesimport math 16*e1fe3e4aSElliott Hughesfrom typing import Any, Optional, Tuple, Dict 17*e1fe3e4aSElliott Hughes 18*e1fe3e4aSElliott Hughesfrom fontTools.pens.basePen import AbstractPen, PenError 19*e1fe3e4aSElliott Hughesfrom fontTools.misc.transform import DecomposedTransform 20*e1fe3e4aSElliott Hughes 21*e1fe3e4aSElliott Hughes__all__ = [ 22*e1fe3e4aSElliott Hughes "AbstractPointPen", 23*e1fe3e4aSElliott Hughes "BasePointToSegmentPen", 24*e1fe3e4aSElliott Hughes "PointToSegmentPen", 25*e1fe3e4aSElliott Hughes "SegmentToPointPen", 26*e1fe3e4aSElliott Hughes "GuessSmoothPointPen", 27*e1fe3e4aSElliott Hughes "ReverseContourPointPen", 28*e1fe3e4aSElliott Hughes] 29*e1fe3e4aSElliott Hughes 30*e1fe3e4aSElliott Hughes 31*e1fe3e4aSElliott Hughesclass AbstractPointPen: 32*e1fe3e4aSElliott Hughes """Baseclass for all PointPens.""" 33*e1fe3e4aSElliott Hughes 34*e1fe3e4aSElliott Hughes def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None: 35*e1fe3e4aSElliott Hughes """Start a new sub path.""" 36*e1fe3e4aSElliott Hughes raise NotImplementedError 37*e1fe3e4aSElliott Hughes 38*e1fe3e4aSElliott Hughes def endPath(self) -> None: 39*e1fe3e4aSElliott Hughes """End the current sub path.""" 40*e1fe3e4aSElliott Hughes raise NotImplementedError 41*e1fe3e4aSElliott Hughes 42*e1fe3e4aSElliott Hughes def addPoint( 43*e1fe3e4aSElliott Hughes self, 44*e1fe3e4aSElliott Hughes pt: Tuple[float, float], 45*e1fe3e4aSElliott Hughes segmentType: Optional[str] = None, 46*e1fe3e4aSElliott Hughes smooth: bool = False, 47*e1fe3e4aSElliott Hughes name: Optional[str] = None, 48*e1fe3e4aSElliott Hughes identifier: Optional[str] = None, 49*e1fe3e4aSElliott Hughes **kwargs: Any, 50*e1fe3e4aSElliott Hughes ) -> None: 51*e1fe3e4aSElliott Hughes """Add a point to the current sub path.""" 52*e1fe3e4aSElliott Hughes raise NotImplementedError 53*e1fe3e4aSElliott Hughes 54*e1fe3e4aSElliott Hughes def addComponent( 55*e1fe3e4aSElliott Hughes self, 56*e1fe3e4aSElliott Hughes baseGlyphName: str, 57*e1fe3e4aSElliott Hughes transformation: Tuple[float, float, float, float, float, float], 58*e1fe3e4aSElliott Hughes identifier: Optional[str] = None, 59*e1fe3e4aSElliott Hughes **kwargs: Any, 60*e1fe3e4aSElliott Hughes ) -> None: 61*e1fe3e4aSElliott Hughes """Add a sub glyph.""" 62*e1fe3e4aSElliott Hughes raise NotImplementedError 63*e1fe3e4aSElliott Hughes 64*e1fe3e4aSElliott Hughes def addVarComponent( 65*e1fe3e4aSElliott Hughes self, 66*e1fe3e4aSElliott Hughes glyphName: str, 67*e1fe3e4aSElliott Hughes transformation: DecomposedTransform, 68*e1fe3e4aSElliott Hughes location: Dict[str, float], 69*e1fe3e4aSElliott Hughes identifier: Optional[str] = None, 70*e1fe3e4aSElliott Hughes **kwargs: Any, 71*e1fe3e4aSElliott Hughes ) -> None: 72*e1fe3e4aSElliott Hughes """Add a VarComponent sub glyph. The 'transformation' argument 73*e1fe3e4aSElliott Hughes must be a DecomposedTransform from the fontTools.misc.transform module, 74*e1fe3e4aSElliott Hughes and the 'location' argument must be a dictionary mapping axis tags 75*e1fe3e4aSElliott Hughes to their locations. 76*e1fe3e4aSElliott Hughes """ 77*e1fe3e4aSElliott Hughes # ttGlyphSet decomposes for us 78*e1fe3e4aSElliott Hughes raise AttributeError 79*e1fe3e4aSElliott Hughes 80*e1fe3e4aSElliott Hughes 81*e1fe3e4aSElliott Hughesclass BasePointToSegmentPen(AbstractPointPen): 82*e1fe3e4aSElliott Hughes """ 83*e1fe3e4aSElliott Hughes Base class for retrieving the outline in a segment-oriented 84*e1fe3e4aSElliott Hughes way. The PointPen protocol is simple yet also a little tricky, 85*e1fe3e4aSElliott Hughes so when you need an outline presented as segments but you have 86*e1fe3e4aSElliott Hughes as points, do use this base implementation as it properly takes 87*e1fe3e4aSElliott Hughes care of all the edge cases. 88*e1fe3e4aSElliott Hughes """ 89*e1fe3e4aSElliott Hughes 90*e1fe3e4aSElliott Hughes def __init__(self): 91*e1fe3e4aSElliott Hughes self.currentPath = None 92*e1fe3e4aSElliott Hughes 93*e1fe3e4aSElliott Hughes def beginPath(self, identifier=None, **kwargs): 94*e1fe3e4aSElliott Hughes if self.currentPath is not None: 95*e1fe3e4aSElliott Hughes raise PenError("Path already begun.") 96*e1fe3e4aSElliott Hughes self.currentPath = [] 97*e1fe3e4aSElliott Hughes 98*e1fe3e4aSElliott Hughes def _flushContour(self, segments): 99*e1fe3e4aSElliott Hughes """Override this method. 100*e1fe3e4aSElliott Hughes 101*e1fe3e4aSElliott Hughes It will be called for each non-empty sub path with a list 102*e1fe3e4aSElliott Hughes of segments: the 'segments' argument. 103*e1fe3e4aSElliott Hughes 104*e1fe3e4aSElliott Hughes The segments list contains tuples of length 2: 105*e1fe3e4aSElliott Hughes (segmentType, points) 106*e1fe3e4aSElliott Hughes 107*e1fe3e4aSElliott Hughes segmentType is one of "move", "line", "curve" or "qcurve". 108*e1fe3e4aSElliott Hughes "move" may only occur as the first segment, and it signifies 109*e1fe3e4aSElliott Hughes an OPEN path. A CLOSED path does NOT start with a "move", in 110*e1fe3e4aSElliott Hughes fact it will not contain a "move" at ALL. 111*e1fe3e4aSElliott Hughes 112*e1fe3e4aSElliott Hughes The 'points' field in the 2-tuple is a list of point info 113*e1fe3e4aSElliott Hughes tuples. The list has 1 or more items, a point tuple has 114*e1fe3e4aSElliott Hughes four items: 115*e1fe3e4aSElliott Hughes (point, smooth, name, kwargs) 116*e1fe3e4aSElliott Hughes 'point' is an (x, y) coordinate pair. 117*e1fe3e4aSElliott Hughes 118*e1fe3e4aSElliott Hughes For a closed path, the initial moveTo point is defined as 119*e1fe3e4aSElliott Hughes the last point of the last segment. 120*e1fe3e4aSElliott Hughes 121*e1fe3e4aSElliott Hughes The 'points' list of "move" and "line" segments always contains 122*e1fe3e4aSElliott Hughes exactly one point tuple. 123*e1fe3e4aSElliott Hughes """ 124*e1fe3e4aSElliott Hughes raise NotImplementedError 125*e1fe3e4aSElliott Hughes 126*e1fe3e4aSElliott Hughes def endPath(self): 127*e1fe3e4aSElliott Hughes if self.currentPath is None: 128*e1fe3e4aSElliott Hughes raise PenError("Path not begun.") 129*e1fe3e4aSElliott Hughes points = self.currentPath 130*e1fe3e4aSElliott Hughes self.currentPath = None 131*e1fe3e4aSElliott Hughes if not points: 132*e1fe3e4aSElliott Hughes return 133*e1fe3e4aSElliott Hughes if len(points) == 1: 134*e1fe3e4aSElliott Hughes # Not much more we can do than output a single move segment. 135*e1fe3e4aSElliott Hughes pt, segmentType, smooth, name, kwargs = points[0] 136*e1fe3e4aSElliott Hughes segments = [("move", [(pt, smooth, name, kwargs)])] 137*e1fe3e4aSElliott Hughes self._flushContour(segments) 138*e1fe3e4aSElliott Hughes return 139*e1fe3e4aSElliott Hughes segments = [] 140*e1fe3e4aSElliott Hughes if points[0][1] == "move": 141*e1fe3e4aSElliott Hughes # It's an open contour, insert a "move" segment for the first 142*e1fe3e4aSElliott Hughes # point and remove that first point from the point list. 143*e1fe3e4aSElliott Hughes pt, segmentType, smooth, name, kwargs = points[0] 144*e1fe3e4aSElliott Hughes segments.append(("move", [(pt, smooth, name, kwargs)])) 145*e1fe3e4aSElliott Hughes points.pop(0) 146*e1fe3e4aSElliott Hughes else: 147*e1fe3e4aSElliott Hughes # It's a closed contour. Locate the first on-curve point, and 148*e1fe3e4aSElliott Hughes # rotate the point list so that it _ends_ with an on-curve 149*e1fe3e4aSElliott Hughes # point. 150*e1fe3e4aSElliott Hughes firstOnCurve = None 151*e1fe3e4aSElliott Hughes for i in range(len(points)): 152*e1fe3e4aSElliott Hughes segmentType = points[i][1] 153*e1fe3e4aSElliott Hughes if segmentType is not None: 154*e1fe3e4aSElliott Hughes firstOnCurve = i 155*e1fe3e4aSElliott Hughes break 156*e1fe3e4aSElliott Hughes if firstOnCurve is None: 157*e1fe3e4aSElliott Hughes # Special case for quadratics: a contour with no on-curve 158*e1fe3e4aSElliott Hughes # points. Add a "None" point. (See also the Pen protocol's 159*e1fe3e4aSElliott Hughes # qCurveTo() method and fontTools.pens.basePen.py.) 160*e1fe3e4aSElliott Hughes points.append((None, "qcurve", None, None, None)) 161*e1fe3e4aSElliott Hughes else: 162*e1fe3e4aSElliott Hughes points = points[firstOnCurve + 1 :] + points[: firstOnCurve + 1] 163*e1fe3e4aSElliott Hughes 164*e1fe3e4aSElliott Hughes currentSegment = [] 165*e1fe3e4aSElliott Hughes for pt, segmentType, smooth, name, kwargs in points: 166*e1fe3e4aSElliott Hughes currentSegment.append((pt, smooth, name, kwargs)) 167*e1fe3e4aSElliott Hughes if segmentType is None: 168*e1fe3e4aSElliott Hughes continue 169*e1fe3e4aSElliott Hughes segments.append((segmentType, currentSegment)) 170*e1fe3e4aSElliott Hughes currentSegment = [] 171*e1fe3e4aSElliott Hughes 172*e1fe3e4aSElliott Hughes self._flushContour(segments) 173*e1fe3e4aSElliott Hughes 174*e1fe3e4aSElliott Hughes def addPoint( 175*e1fe3e4aSElliott Hughes self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs 176*e1fe3e4aSElliott Hughes ): 177*e1fe3e4aSElliott Hughes if self.currentPath is None: 178*e1fe3e4aSElliott Hughes raise PenError("Path not begun") 179*e1fe3e4aSElliott Hughes self.currentPath.append((pt, segmentType, smooth, name, kwargs)) 180*e1fe3e4aSElliott Hughes 181*e1fe3e4aSElliott Hughes 182*e1fe3e4aSElliott Hughesclass PointToSegmentPen(BasePointToSegmentPen): 183*e1fe3e4aSElliott Hughes """ 184*e1fe3e4aSElliott Hughes Adapter class that converts the PointPen protocol to the 185*e1fe3e4aSElliott Hughes (Segment)Pen protocol. 186*e1fe3e4aSElliott Hughes 187*e1fe3e4aSElliott Hughes NOTE: The segment pen does not support and will drop point names, identifiers 188*e1fe3e4aSElliott Hughes and kwargs. 189*e1fe3e4aSElliott Hughes """ 190*e1fe3e4aSElliott Hughes 191*e1fe3e4aSElliott Hughes def __init__(self, segmentPen, outputImpliedClosingLine=False): 192*e1fe3e4aSElliott Hughes BasePointToSegmentPen.__init__(self) 193*e1fe3e4aSElliott Hughes self.pen = segmentPen 194*e1fe3e4aSElliott Hughes self.outputImpliedClosingLine = outputImpliedClosingLine 195*e1fe3e4aSElliott Hughes 196*e1fe3e4aSElliott Hughes def _flushContour(self, segments): 197*e1fe3e4aSElliott Hughes if not segments: 198*e1fe3e4aSElliott Hughes raise PenError("Must have at least one segment.") 199*e1fe3e4aSElliott Hughes pen = self.pen 200*e1fe3e4aSElliott Hughes if segments[0][0] == "move": 201*e1fe3e4aSElliott Hughes # It's an open path. 202*e1fe3e4aSElliott Hughes closed = False 203*e1fe3e4aSElliott Hughes points = segments[0][1] 204*e1fe3e4aSElliott Hughes if len(points) != 1: 205*e1fe3e4aSElliott Hughes raise PenError(f"Illegal move segment point count: {len(points)}") 206*e1fe3e4aSElliott Hughes movePt, _, _, _ = points[0] 207*e1fe3e4aSElliott Hughes del segments[0] 208*e1fe3e4aSElliott Hughes else: 209*e1fe3e4aSElliott Hughes # It's a closed path, do a moveTo to the last 210*e1fe3e4aSElliott Hughes # point of the last segment. 211*e1fe3e4aSElliott Hughes closed = True 212*e1fe3e4aSElliott Hughes segmentType, points = segments[-1] 213*e1fe3e4aSElliott Hughes movePt, _, _, _ = points[-1] 214*e1fe3e4aSElliott Hughes if movePt is None: 215*e1fe3e4aSElliott Hughes # quad special case: a contour with no on-curve points contains 216*e1fe3e4aSElliott Hughes # one "qcurve" segment that ends with a point that's None. We 217*e1fe3e4aSElliott Hughes # must not output a moveTo() in that case. 218*e1fe3e4aSElliott Hughes pass 219*e1fe3e4aSElliott Hughes else: 220*e1fe3e4aSElliott Hughes pen.moveTo(movePt) 221*e1fe3e4aSElliott Hughes outputImpliedClosingLine = self.outputImpliedClosingLine 222*e1fe3e4aSElliott Hughes nSegments = len(segments) 223*e1fe3e4aSElliott Hughes lastPt = movePt 224*e1fe3e4aSElliott Hughes for i in range(nSegments): 225*e1fe3e4aSElliott Hughes segmentType, points = segments[i] 226*e1fe3e4aSElliott Hughes points = [pt for pt, _, _, _ in points] 227*e1fe3e4aSElliott Hughes if segmentType == "line": 228*e1fe3e4aSElliott Hughes if len(points) != 1: 229*e1fe3e4aSElliott Hughes raise PenError(f"Illegal line segment point count: {len(points)}") 230*e1fe3e4aSElliott Hughes pt = points[0] 231*e1fe3e4aSElliott Hughes # For closed contours, a 'lineTo' is always implied from the last oncurve 232*e1fe3e4aSElliott Hughes # point to the starting point, thus we can omit it when the last and 233*e1fe3e4aSElliott Hughes # starting point don't overlap. 234*e1fe3e4aSElliott Hughes # However, when the last oncurve point is a "line" segment and has same 235*e1fe3e4aSElliott Hughes # coordinates as the starting point of a closed contour, we need to output 236*e1fe3e4aSElliott Hughes # the closing 'lineTo' explicitly (regardless of the value of the 237*e1fe3e4aSElliott Hughes # 'outputImpliedClosingLine' option) in order to disambiguate this case from 238*e1fe3e4aSElliott Hughes # the implied closing 'lineTo', otherwise the duplicate point would be lost. 239*e1fe3e4aSElliott Hughes # See https://github.com/googlefonts/fontmake/issues/572. 240*e1fe3e4aSElliott Hughes if ( 241*e1fe3e4aSElliott Hughes i + 1 != nSegments 242*e1fe3e4aSElliott Hughes or outputImpliedClosingLine 243*e1fe3e4aSElliott Hughes or not closed 244*e1fe3e4aSElliott Hughes or pt == lastPt 245*e1fe3e4aSElliott Hughes ): 246*e1fe3e4aSElliott Hughes pen.lineTo(pt) 247*e1fe3e4aSElliott Hughes lastPt = pt 248*e1fe3e4aSElliott Hughes elif segmentType == "curve": 249*e1fe3e4aSElliott Hughes pen.curveTo(*points) 250*e1fe3e4aSElliott Hughes lastPt = points[-1] 251*e1fe3e4aSElliott Hughes elif segmentType == "qcurve": 252*e1fe3e4aSElliott Hughes pen.qCurveTo(*points) 253*e1fe3e4aSElliott Hughes lastPt = points[-1] 254*e1fe3e4aSElliott Hughes else: 255*e1fe3e4aSElliott Hughes raise PenError(f"Illegal segmentType: {segmentType}") 256*e1fe3e4aSElliott Hughes if closed: 257*e1fe3e4aSElliott Hughes pen.closePath() 258*e1fe3e4aSElliott Hughes else: 259*e1fe3e4aSElliott Hughes pen.endPath() 260*e1fe3e4aSElliott Hughes 261*e1fe3e4aSElliott Hughes def addComponent(self, glyphName, transform, identifier=None, **kwargs): 262*e1fe3e4aSElliott Hughes del identifier # unused 263*e1fe3e4aSElliott Hughes del kwargs # unused 264*e1fe3e4aSElliott Hughes self.pen.addComponent(glyphName, transform) 265*e1fe3e4aSElliott Hughes 266*e1fe3e4aSElliott Hughes 267*e1fe3e4aSElliott Hughesclass SegmentToPointPen(AbstractPen): 268*e1fe3e4aSElliott Hughes """ 269*e1fe3e4aSElliott Hughes Adapter class that converts the (Segment)Pen protocol to the 270*e1fe3e4aSElliott Hughes PointPen protocol. 271*e1fe3e4aSElliott Hughes """ 272*e1fe3e4aSElliott Hughes 273*e1fe3e4aSElliott Hughes def __init__(self, pointPen, guessSmooth=True): 274*e1fe3e4aSElliott Hughes if guessSmooth: 275*e1fe3e4aSElliott Hughes self.pen = GuessSmoothPointPen(pointPen) 276*e1fe3e4aSElliott Hughes else: 277*e1fe3e4aSElliott Hughes self.pen = pointPen 278*e1fe3e4aSElliott Hughes self.contour = None 279*e1fe3e4aSElliott Hughes 280*e1fe3e4aSElliott Hughes def _flushContour(self): 281*e1fe3e4aSElliott Hughes pen = self.pen 282*e1fe3e4aSElliott Hughes pen.beginPath() 283*e1fe3e4aSElliott Hughes for pt, segmentType in self.contour: 284*e1fe3e4aSElliott Hughes pen.addPoint(pt, segmentType=segmentType) 285*e1fe3e4aSElliott Hughes pen.endPath() 286*e1fe3e4aSElliott Hughes 287*e1fe3e4aSElliott Hughes def moveTo(self, pt): 288*e1fe3e4aSElliott Hughes self.contour = [] 289*e1fe3e4aSElliott Hughes self.contour.append((pt, "move")) 290*e1fe3e4aSElliott Hughes 291*e1fe3e4aSElliott Hughes def lineTo(self, pt): 292*e1fe3e4aSElliott Hughes if self.contour is None: 293*e1fe3e4aSElliott Hughes raise PenError("Contour missing required initial moveTo") 294*e1fe3e4aSElliott Hughes self.contour.append((pt, "line")) 295*e1fe3e4aSElliott Hughes 296*e1fe3e4aSElliott Hughes def curveTo(self, *pts): 297*e1fe3e4aSElliott Hughes if not pts: 298*e1fe3e4aSElliott Hughes raise TypeError("Must pass in at least one point") 299*e1fe3e4aSElliott Hughes if self.contour is None: 300*e1fe3e4aSElliott Hughes raise PenError("Contour missing required initial moveTo") 301*e1fe3e4aSElliott Hughes for pt in pts[:-1]: 302*e1fe3e4aSElliott Hughes self.contour.append((pt, None)) 303*e1fe3e4aSElliott Hughes self.contour.append((pts[-1], "curve")) 304*e1fe3e4aSElliott Hughes 305*e1fe3e4aSElliott Hughes def qCurveTo(self, *pts): 306*e1fe3e4aSElliott Hughes if not pts: 307*e1fe3e4aSElliott Hughes raise TypeError("Must pass in at least one point") 308*e1fe3e4aSElliott Hughes if pts[-1] is None: 309*e1fe3e4aSElliott Hughes self.contour = [] 310*e1fe3e4aSElliott Hughes else: 311*e1fe3e4aSElliott Hughes if self.contour is None: 312*e1fe3e4aSElliott Hughes raise PenError("Contour missing required initial moveTo") 313*e1fe3e4aSElliott Hughes for pt in pts[:-1]: 314*e1fe3e4aSElliott Hughes self.contour.append((pt, None)) 315*e1fe3e4aSElliott Hughes if pts[-1] is not None: 316*e1fe3e4aSElliott Hughes self.contour.append((pts[-1], "qcurve")) 317*e1fe3e4aSElliott Hughes 318*e1fe3e4aSElliott Hughes def closePath(self): 319*e1fe3e4aSElliott Hughes if self.contour is None: 320*e1fe3e4aSElliott Hughes raise PenError("Contour missing required initial moveTo") 321*e1fe3e4aSElliott Hughes if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]: 322*e1fe3e4aSElliott Hughes self.contour[0] = self.contour[-1] 323*e1fe3e4aSElliott Hughes del self.contour[-1] 324*e1fe3e4aSElliott Hughes else: 325*e1fe3e4aSElliott Hughes # There's an implied line at the end, replace "move" with "line" 326*e1fe3e4aSElliott Hughes # for the first point 327*e1fe3e4aSElliott Hughes pt, tp = self.contour[0] 328*e1fe3e4aSElliott Hughes if tp == "move": 329*e1fe3e4aSElliott Hughes self.contour[0] = pt, "line" 330*e1fe3e4aSElliott Hughes self._flushContour() 331*e1fe3e4aSElliott Hughes self.contour = None 332*e1fe3e4aSElliott Hughes 333*e1fe3e4aSElliott Hughes def endPath(self): 334*e1fe3e4aSElliott Hughes if self.contour is None: 335*e1fe3e4aSElliott Hughes raise PenError("Contour missing required initial moveTo") 336*e1fe3e4aSElliott Hughes self._flushContour() 337*e1fe3e4aSElliott Hughes self.contour = None 338*e1fe3e4aSElliott Hughes 339*e1fe3e4aSElliott Hughes def addComponent(self, glyphName, transform): 340*e1fe3e4aSElliott Hughes if self.contour is not None: 341*e1fe3e4aSElliott Hughes raise PenError("Components must be added before or after contours") 342*e1fe3e4aSElliott Hughes self.pen.addComponent(glyphName, transform) 343*e1fe3e4aSElliott Hughes 344*e1fe3e4aSElliott Hughes 345*e1fe3e4aSElliott Hughesclass GuessSmoothPointPen(AbstractPointPen): 346*e1fe3e4aSElliott Hughes """ 347*e1fe3e4aSElliott Hughes Filtering PointPen that tries to determine whether an on-curve point 348*e1fe3e4aSElliott Hughes should be "smooth", ie. that it's a "tangent" point or a "curve" point. 349*e1fe3e4aSElliott Hughes """ 350*e1fe3e4aSElliott Hughes 351*e1fe3e4aSElliott Hughes def __init__(self, outPen, error=0.05): 352*e1fe3e4aSElliott Hughes self._outPen = outPen 353*e1fe3e4aSElliott Hughes self._error = error 354*e1fe3e4aSElliott Hughes self._points = None 355*e1fe3e4aSElliott Hughes 356*e1fe3e4aSElliott Hughes def _flushContour(self): 357*e1fe3e4aSElliott Hughes if self._points is None: 358*e1fe3e4aSElliott Hughes raise PenError("Path not begun") 359*e1fe3e4aSElliott Hughes points = self._points 360*e1fe3e4aSElliott Hughes nPoints = len(points) 361*e1fe3e4aSElliott Hughes if not nPoints: 362*e1fe3e4aSElliott Hughes return 363*e1fe3e4aSElliott Hughes if points[0][1] == "move": 364*e1fe3e4aSElliott Hughes # Open path. 365*e1fe3e4aSElliott Hughes indices = range(1, nPoints - 1) 366*e1fe3e4aSElliott Hughes elif nPoints > 1: 367*e1fe3e4aSElliott Hughes # Closed path. To avoid having to mod the contour index, we 368*e1fe3e4aSElliott Hughes # simply abuse Python's negative index feature, and start at -1 369*e1fe3e4aSElliott Hughes indices = range(-1, nPoints - 1) 370*e1fe3e4aSElliott Hughes else: 371*e1fe3e4aSElliott Hughes # closed path containing 1 point (!), ignore. 372*e1fe3e4aSElliott Hughes indices = [] 373*e1fe3e4aSElliott Hughes for i in indices: 374*e1fe3e4aSElliott Hughes pt, segmentType, _, name, kwargs = points[i] 375*e1fe3e4aSElliott Hughes if segmentType is None: 376*e1fe3e4aSElliott Hughes continue 377*e1fe3e4aSElliott Hughes prev = i - 1 378*e1fe3e4aSElliott Hughes next = i + 1 379*e1fe3e4aSElliott Hughes if points[prev][1] is not None and points[next][1] is not None: 380*e1fe3e4aSElliott Hughes continue 381*e1fe3e4aSElliott Hughes # At least one of our neighbors is an off-curve point 382*e1fe3e4aSElliott Hughes pt = points[i][0] 383*e1fe3e4aSElliott Hughes prevPt = points[prev][0] 384*e1fe3e4aSElliott Hughes nextPt = points[next][0] 385*e1fe3e4aSElliott Hughes if pt != prevPt and pt != nextPt: 386*e1fe3e4aSElliott Hughes dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1] 387*e1fe3e4aSElliott Hughes dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1] 388*e1fe3e4aSElliott Hughes a1 = math.atan2(dy1, dx1) 389*e1fe3e4aSElliott Hughes a2 = math.atan2(dy2, dx2) 390*e1fe3e4aSElliott Hughes if abs(a1 - a2) < self._error: 391*e1fe3e4aSElliott Hughes points[i] = pt, segmentType, True, name, kwargs 392*e1fe3e4aSElliott Hughes 393*e1fe3e4aSElliott Hughes for pt, segmentType, smooth, name, kwargs in points: 394*e1fe3e4aSElliott Hughes self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) 395*e1fe3e4aSElliott Hughes 396*e1fe3e4aSElliott Hughes def beginPath(self, identifier=None, **kwargs): 397*e1fe3e4aSElliott Hughes if self._points is not None: 398*e1fe3e4aSElliott Hughes raise PenError("Path already begun") 399*e1fe3e4aSElliott Hughes self._points = [] 400*e1fe3e4aSElliott Hughes if identifier is not None: 401*e1fe3e4aSElliott Hughes kwargs["identifier"] = identifier 402*e1fe3e4aSElliott Hughes self._outPen.beginPath(**kwargs) 403*e1fe3e4aSElliott Hughes 404*e1fe3e4aSElliott Hughes def endPath(self): 405*e1fe3e4aSElliott Hughes self._flushContour() 406*e1fe3e4aSElliott Hughes self._outPen.endPath() 407*e1fe3e4aSElliott Hughes self._points = None 408*e1fe3e4aSElliott Hughes 409*e1fe3e4aSElliott Hughes def addPoint( 410*e1fe3e4aSElliott Hughes self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs 411*e1fe3e4aSElliott Hughes ): 412*e1fe3e4aSElliott Hughes if self._points is None: 413*e1fe3e4aSElliott Hughes raise PenError("Path not begun") 414*e1fe3e4aSElliott Hughes if identifier is not None: 415*e1fe3e4aSElliott Hughes kwargs["identifier"] = identifier 416*e1fe3e4aSElliott Hughes self._points.append((pt, segmentType, False, name, kwargs)) 417*e1fe3e4aSElliott Hughes 418*e1fe3e4aSElliott Hughes def addComponent(self, glyphName, transformation, identifier=None, **kwargs): 419*e1fe3e4aSElliott Hughes if self._points is not None: 420*e1fe3e4aSElliott Hughes raise PenError("Components must be added before or after contours") 421*e1fe3e4aSElliott Hughes if identifier is not None: 422*e1fe3e4aSElliott Hughes kwargs["identifier"] = identifier 423*e1fe3e4aSElliott Hughes self._outPen.addComponent(glyphName, transformation, **kwargs) 424*e1fe3e4aSElliott Hughes 425*e1fe3e4aSElliott Hughes def addVarComponent( 426*e1fe3e4aSElliott Hughes self, glyphName, transformation, location, identifier=None, **kwargs 427*e1fe3e4aSElliott Hughes ): 428*e1fe3e4aSElliott Hughes if self._points is not None: 429*e1fe3e4aSElliott Hughes raise PenError("VarComponents must be added before or after contours") 430*e1fe3e4aSElliott Hughes if identifier is not None: 431*e1fe3e4aSElliott Hughes kwargs["identifier"] = identifier 432*e1fe3e4aSElliott Hughes self._outPen.addVarComponent(glyphName, transformation, location, **kwargs) 433*e1fe3e4aSElliott Hughes 434*e1fe3e4aSElliott Hughes 435*e1fe3e4aSElliott Hughesclass ReverseContourPointPen(AbstractPointPen): 436*e1fe3e4aSElliott Hughes """ 437*e1fe3e4aSElliott Hughes This is a PointPen that passes outline data to another PointPen, but 438*e1fe3e4aSElliott Hughes reversing the winding direction of all contours. Components are simply 439*e1fe3e4aSElliott Hughes passed through unchanged. 440*e1fe3e4aSElliott Hughes 441*e1fe3e4aSElliott Hughes Closed contours are reversed in such a way that the first point remains 442*e1fe3e4aSElliott Hughes the first point. 443*e1fe3e4aSElliott Hughes """ 444*e1fe3e4aSElliott Hughes 445*e1fe3e4aSElliott Hughes def __init__(self, outputPointPen): 446*e1fe3e4aSElliott Hughes self.pen = outputPointPen 447*e1fe3e4aSElliott Hughes # a place to store the points for the current sub path 448*e1fe3e4aSElliott Hughes self.currentContour = None 449*e1fe3e4aSElliott Hughes 450*e1fe3e4aSElliott Hughes def _flushContour(self): 451*e1fe3e4aSElliott Hughes pen = self.pen 452*e1fe3e4aSElliott Hughes contour = self.currentContour 453*e1fe3e4aSElliott Hughes if not contour: 454*e1fe3e4aSElliott Hughes pen.beginPath(identifier=self.currentContourIdentifier) 455*e1fe3e4aSElliott Hughes pen.endPath() 456*e1fe3e4aSElliott Hughes return 457*e1fe3e4aSElliott Hughes 458*e1fe3e4aSElliott Hughes closed = contour[0][1] != "move" 459*e1fe3e4aSElliott Hughes if not closed: 460*e1fe3e4aSElliott Hughes lastSegmentType = "move" 461*e1fe3e4aSElliott Hughes else: 462*e1fe3e4aSElliott Hughes # Remove the first point and insert it at the end. When 463*e1fe3e4aSElliott Hughes # the list of points gets reversed, this point will then 464*e1fe3e4aSElliott Hughes # again be at the start. In other words, the following 465*e1fe3e4aSElliott Hughes # will hold: 466*e1fe3e4aSElliott Hughes # for N in range(len(originalContour)): 467*e1fe3e4aSElliott Hughes # originalContour[N] == reversedContour[-N] 468*e1fe3e4aSElliott Hughes contour.append(contour.pop(0)) 469*e1fe3e4aSElliott Hughes # Find the first on-curve point. 470*e1fe3e4aSElliott Hughes firstOnCurve = None 471*e1fe3e4aSElliott Hughes for i in range(len(contour)): 472*e1fe3e4aSElliott Hughes if contour[i][1] is not None: 473*e1fe3e4aSElliott Hughes firstOnCurve = i 474*e1fe3e4aSElliott Hughes break 475*e1fe3e4aSElliott Hughes if firstOnCurve is None: 476*e1fe3e4aSElliott Hughes # There are no on-curve points, be basically have to 477*e1fe3e4aSElliott Hughes # do nothing but contour.reverse(). 478*e1fe3e4aSElliott Hughes lastSegmentType = None 479*e1fe3e4aSElliott Hughes else: 480*e1fe3e4aSElliott Hughes lastSegmentType = contour[firstOnCurve][1] 481*e1fe3e4aSElliott Hughes 482*e1fe3e4aSElliott Hughes contour.reverse() 483*e1fe3e4aSElliott Hughes if not closed: 484*e1fe3e4aSElliott Hughes # Open paths must start with a move, so we simply dump 485*e1fe3e4aSElliott Hughes # all off-curve points leading up to the first on-curve. 486*e1fe3e4aSElliott Hughes while contour[0][1] is None: 487*e1fe3e4aSElliott Hughes contour.pop(0) 488*e1fe3e4aSElliott Hughes pen.beginPath(identifier=self.currentContourIdentifier) 489*e1fe3e4aSElliott Hughes for pt, nextSegmentType, smooth, name, kwargs in contour: 490*e1fe3e4aSElliott Hughes if nextSegmentType is not None: 491*e1fe3e4aSElliott Hughes segmentType = lastSegmentType 492*e1fe3e4aSElliott Hughes lastSegmentType = nextSegmentType 493*e1fe3e4aSElliott Hughes else: 494*e1fe3e4aSElliott Hughes segmentType = None 495*e1fe3e4aSElliott Hughes pen.addPoint( 496*e1fe3e4aSElliott Hughes pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs 497*e1fe3e4aSElliott Hughes ) 498*e1fe3e4aSElliott Hughes pen.endPath() 499*e1fe3e4aSElliott Hughes 500*e1fe3e4aSElliott Hughes def beginPath(self, identifier=None, **kwargs): 501*e1fe3e4aSElliott Hughes if self.currentContour is not None: 502*e1fe3e4aSElliott Hughes raise PenError("Path already begun") 503*e1fe3e4aSElliott Hughes self.currentContour = [] 504*e1fe3e4aSElliott Hughes self.currentContourIdentifier = identifier 505*e1fe3e4aSElliott Hughes self.onCurve = [] 506*e1fe3e4aSElliott Hughes 507*e1fe3e4aSElliott Hughes def endPath(self): 508*e1fe3e4aSElliott Hughes if self.currentContour is None: 509*e1fe3e4aSElliott Hughes raise PenError("Path not begun") 510*e1fe3e4aSElliott Hughes self._flushContour() 511*e1fe3e4aSElliott Hughes self.currentContour = None 512*e1fe3e4aSElliott Hughes 513*e1fe3e4aSElliott Hughes def addPoint( 514*e1fe3e4aSElliott Hughes self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs 515*e1fe3e4aSElliott Hughes ): 516*e1fe3e4aSElliott Hughes if self.currentContour is None: 517*e1fe3e4aSElliott Hughes raise PenError("Path not begun") 518*e1fe3e4aSElliott Hughes if identifier is not None: 519*e1fe3e4aSElliott Hughes kwargs["identifier"] = identifier 520*e1fe3e4aSElliott Hughes self.currentContour.append((pt, segmentType, smooth, name, kwargs)) 521*e1fe3e4aSElliott Hughes 522*e1fe3e4aSElliott Hughes def addComponent(self, glyphName, transform, identifier=None, **kwargs): 523*e1fe3e4aSElliott Hughes if self.currentContour is not None: 524*e1fe3e4aSElliott Hughes raise PenError("Components must be added before or after contours") 525*e1fe3e4aSElliott Hughes self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs) 526