| """ |
| ========= |
| PointPens |
| ========= |
| |
| Where **SegmentPens** have an intuitive approach to drawing |
| (if you're familiar with postscript anyway), the **PointPen** |
| is geared towards accessing all the data in the contours of |
| the glyph. A PointPen has a very simple interface, it just |
| steps through all the points in a call from glyph.drawPoints(). |
| This allows the caller to provide more data for each point. |
| For instance, whether or not a point is smooth, and its name. |
| """ |
| |
| import math |
| from typing import Any, Optional, Tuple |
| |
| from fontTools.pens.basePen import AbstractPen, PenError |
| |
| __all__ = [ |
| "AbstractPointPen", |
| "BasePointToSegmentPen", |
| "PointToSegmentPen", |
| "SegmentToPointPen", |
| "GuessSmoothPointPen", |
| "ReverseContourPointPen", |
| ] |
| |
| |
| class AbstractPointPen: |
| """Baseclass for all PointPens.""" |
| |
| def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None: |
| """Start a new sub path.""" |
| raise NotImplementedError |
| |
| def endPath(self) -> None: |
| """End the current sub path.""" |
| raise NotImplementedError |
| |
| def addPoint( |
| self, |
| pt: Tuple[float, float], |
| segmentType: Optional[str] = None, |
| smooth: bool = False, |
| name: Optional[str] = None, |
| identifier: Optional[str] = None, |
| **kwargs: Any |
| ) -> None: |
| """Add a point to the current sub path.""" |
| raise NotImplementedError |
| |
| def addComponent( |
| self, |
| baseGlyphName: str, |
| transformation: Tuple[float, float, float, float, float, float], |
| identifier: Optional[str] = None, |
| **kwargs: Any |
| ) -> None: |
| """Add a sub glyph.""" |
| raise NotImplementedError |
| |
| |
| class BasePointToSegmentPen(AbstractPointPen): |
| """ |
| Base class for retrieving the outline in a segment-oriented |
| way. The PointPen protocol is simple yet also a little tricky, |
| so when you need an outline presented as segments but you have |
| as points, do use this base implementation as it properly takes |
| care of all the edge cases. |
| """ |
| |
| def __init__(self): |
| self.currentPath = None |
| |
| def beginPath(self, identifier=None, **kwargs): |
| if self.currentPath is not None: |
| raise PenError("Path already begun.") |
| self.currentPath = [] |
| |
| def _flushContour(self, segments): |
| """Override this method. |
| |
| It will be called for each non-empty sub path with a list |
| of segments: the 'segments' argument. |
| |
| The segments list contains tuples of length 2: |
| (segmentType, points) |
| |
| segmentType is one of "move", "line", "curve" or "qcurve". |
| "move" may only occur as the first segment, and it signifies |
| an OPEN path. A CLOSED path does NOT start with a "move", in |
| fact it will not contain a "move" at ALL. |
| |
| The 'points' field in the 2-tuple is a list of point info |
| tuples. The list has 1 or more items, a point tuple has |
| four items: |
| (point, smooth, name, kwargs) |
| 'point' is an (x, y) coordinate pair. |
| |
| For a closed path, the initial moveTo point is defined as |
| the last point of the last segment. |
| |
| The 'points' list of "move" and "line" segments always contains |
| exactly one point tuple. |
| """ |
| raise NotImplementedError |
| |
| def endPath(self): |
| if self.currentPath is None: |
| raise PenError("Path not begun.") |
| points = self.currentPath |
| self.currentPath = None |
| if not points: |
| return |
| if len(points) == 1: |
| # Not much more we can do than output a single move segment. |
| pt, segmentType, smooth, name, kwargs = points[0] |
| segments = [("move", [(pt, smooth, name, kwargs)])] |
| self._flushContour(segments) |
| return |
| segments = [] |
| if points[0][1] == "move": |
| # It's an open contour, insert a "move" segment for the first |
| # point and remove that first point from the point list. |
| pt, segmentType, smooth, name, kwargs = points[0] |
| segments.append(("move", [(pt, smooth, name, kwargs)])) |
| points.pop(0) |
| else: |
| # It's a closed contour. Locate the first on-curve point, and |
| # rotate the point list so that it _ends_ with an on-curve |
| # point. |
| firstOnCurve = None |
| for i in range(len(points)): |
| segmentType = points[i][1] |
| if segmentType is not None: |
| firstOnCurve = i |
| break |
| if firstOnCurve is None: |
| # Special case for quadratics: a contour with no on-curve |
| # points. Add a "None" point. (See also the Pen protocol's |
| # qCurveTo() method and fontTools.pens.basePen.py.) |
| points.append((None, "qcurve", None, None, None)) |
| else: |
| points = points[firstOnCurve+1:] + points[:firstOnCurve+1] |
| |
| currentSegment = [] |
| for pt, segmentType, smooth, name, kwargs in points: |
| currentSegment.append((pt, smooth, name, kwargs)) |
| if segmentType is None: |
| continue |
| segments.append((segmentType, currentSegment)) |
| currentSegment = [] |
| |
| self._flushContour(segments) |
| |
| def addPoint(self, pt, segmentType=None, smooth=False, name=None, |
| identifier=None, **kwargs): |
| if self.currentPath is None: |
| raise PenError("Path not begun") |
| self.currentPath.append((pt, segmentType, smooth, name, kwargs)) |
| |
| |
| class PointToSegmentPen(BasePointToSegmentPen): |
| """ |
| Adapter class that converts the PointPen protocol to the |
| (Segment)Pen protocol. |
| |
| NOTE: The segment pen does not support and will drop point names, identifiers |
| and kwargs. |
| """ |
| |
| def __init__(self, segmentPen, outputImpliedClosingLine=False): |
| BasePointToSegmentPen.__init__(self) |
| self.pen = segmentPen |
| self.outputImpliedClosingLine = outputImpliedClosingLine |
| |
| def _flushContour(self, segments): |
| if not segments: |
| raise PenError("Must have at least one segment.") |
| pen = self.pen |
| if segments[0][0] == "move": |
| # It's an open path. |
| closed = False |
| points = segments[0][1] |
| if len(points) != 1: |
| raise PenError(f"Illegal move segment point count: {len(points)}") |
| movePt, _, _ , _ = points[0] |
| del segments[0] |
| else: |
| # It's a closed path, do a moveTo to the last |
| # point of the last segment. |
| closed = True |
| segmentType, points = segments[-1] |
| movePt, _, _ , _ = points[-1] |
| if movePt is None: |
| # quad special case: a contour with no on-curve points contains |
| # one "qcurve" segment that ends with a point that's None. We |
| # must not output a moveTo() in that case. |
| pass |
| else: |
| pen.moveTo(movePt) |
| outputImpliedClosingLine = self.outputImpliedClosingLine |
| nSegments = len(segments) |
| lastPt = movePt |
| for i in range(nSegments): |
| segmentType, points = segments[i] |
| points = [pt for pt, _, _ , _ in points] |
| if segmentType == "line": |
| if len(points) != 1: |
| raise PenError(f"Illegal line segment point count: {len(points)}") |
| pt = points[0] |
| # For closed contours, a 'lineTo' is always implied from the last oncurve |
| # point to the starting point, thus we can omit it when the last and |
| # starting point don't overlap. |
| # However, when the last oncurve point is a "line" segment and has same |
| # coordinates as the starting point of a closed contour, we need to output |
| # the closing 'lineTo' explicitly (regardless of the value of the |
| # 'outputImpliedClosingLine' option) in order to disambiguate this case from |
| # the implied closing 'lineTo', otherwise the duplicate point would be lost. |
| # See https://github.com/googlefonts/fontmake/issues/572. |
| if ( |
| i + 1 != nSegments |
| or outputImpliedClosingLine |
| or not closed |
| or pt == lastPt |
| ): |
| pen.lineTo(pt) |
| lastPt = pt |
| elif segmentType == "curve": |
| pen.curveTo(*points) |
| lastPt = points[-1] |
| elif segmentType == "qcurve": |
| pen.qCurveTo(*points) |
| lastPt = points[-1] |
| else: |
| raise PenError(f"Illegal segmentType: {segmentType}") |
| if closed: |
| pen.closePath() |
| else: |
| pen.endPath() |
| |
| def addComponent(self, glyphName, transform, identifier=None, **kwargs): |
| del identifier # unused |
| del kwargs # unused |
| self.pen.addComponent(glyphName, transform) |
| |
| |
| class SegmentToPointPen(AbstractPen): |
| """ |
| Adapter class that converts the (Segment)Pen protocol to the |
| PointPen protocol. |
| """ |
| |
| def __init__(self, pointPen, guessSmooth=True): |
| if guessSmooth: |
| self.pen = GuessSmoothPointPen(pointPen) |
| else: |
| self.pen = pointPen |
| self.contour = None |
| |
| def _flushContour(self): |
| pen = self.pen |
| pen.beginPath() |
| for pt, segmentType in self.contour: |
| pen.addPoint(pt, segmentType=segmentType) |
| pen.endPath() |
| |
| def moveTo(self, pt): |
| self.contour = [] |
| self.contour.append((pt, "move")) |
| |
| def lineTo(self, pt): |
| if self.contour is None: |
| raise PenError("Contour missing required initial moveTo") |
| self.contour.append((pt, "line")) |
| |
| def curveTo(self, *pts): |
| if not pts: |
| raise TypeError("Must pass in at least one point") |
| if self.contour is None: |
| raise PenError("Contour missing required initial moveTo") |
| for pt in pts[:-1]: |
| self.contour.append((pt, None)) |
| self.contour.append((pts[-1], "curve")) |
| |
| def qCurveTo(self, *pts): |
| if not pts: |
| raise TypeError("Must pass in at least one point") |
| if pts[-1] is None: |
| self.contour = [] |
| else: |
| if self.contour is None: |
| raise PenError("Contour missing required initial moveTo") |
| for pt in pts[:-1]: |
| self.contour.append((pt, None)) |
| if pts[-1] is not None: |
| self.contour.append((pts[-1], "qcurve")) |
| |
| def closePath(self): |
| if self.contour is None: |
| raise PenError("Contour missing required initial moveTo") |
| if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]: |
| self.contour[0] = self.contour[-1] |
| del self.contour[-1] |
| else: |
| # There's an implied line at the end, replace "move" with "line" |
| # for the first point |
| pt, tp = self.contour[0] |
| if tp == "move": |
| self.contour[0] = pt, "line" |
| self._flushContour() |
| self.contour = None |
| |
| def endPath(self): |
| if self.contour is None: |
| raise PenError("Contour missing required initial moveTo") |
| self._flushContour() |
| self.contour = None |
| |
| def addComponent(self, glyphName, transform): |
| if self.contour is not None: |
| raise PenError("Components must be added before or after contours") |
| self.pen.addComponent(glyphName, transform) |
| |
| |
| class GuessSmoothPointPen(AbstractPointPen): |
| """ |
| Filtering PointPen that tries to determine whether an on-curve point |
| should be "smooth", ie. that it's a "tangent" point or a "curve" point. |
| """ |
| |
| def __init__(self, outPen, error=0.05): |
| self._outPen = outPen |
| self._error = error |
| self._points = None |
| |
| def _flushContour(self): |
| if self._points is None: |
| raise PenError("Path not begun") |
| points = self._points |
| nPoints = len(points) |
| if not nPoints: |
| return |
| if points[0][1] == "move": |
| # Open path. |
| indices = range(1, nPoints - 1) |
| elif nPoints > 1: |
| # Closed path. To avoid having to mod the contour index, we |
| # simply abuse Python's negative index feature, and start at -1 |
| indices = range(-1, nPoints - 1) |
| else: |
| # closed path containing 1 point (!), ignore. |
| indices = [] |
| for i in indices: |
| pt, segmentType, _, name, kwargs = points[i] |
| if segmentType is None: |
| continue |
| prev = i - 1 |
| next = i + 1 |
| if points[prev][1] is not None and points[next][1] is not None: |
| continue |
| # At least one of our neighbors is an off-curve point |
| pt = points[i][0] |
| prevPt = points[prev][0] |
| nextPt = points[next][0] |
| if pt != prevPt and pt != nextPt: |
| dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1] |
| dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1] |
| a1 = math.atan2(dy1, dx1) |
| a2 = math.atan2(dy2, dx2) |
| if abs(a1 - a2) < self._error: |
| points[i] = pt, segmentType, True, name, kwargs |
| |
| for pt, segmentType, smooth, name, kwargs in points: |
| self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) |
| |
| def beginPath(self, identifier=None, **kwargs): |
| if self._points is not None: |
| raise PenError("Path already begun") |
| self._points = [] |
| if identifier is not None: |
| kwargs["identifier"] = identifier |
| self._outPen.beginPath(**kwargs) |
| |
| def endPath(self): |
| self._flushContour() |
| self._outPen.endPath() |
| self._points = None |
| |
| def addPoint(self, pt, segmentType=None, smooth=False, name=None, |
| identifier=None, **kwargs): |
| if self._points is None: |
| raise PenError("Path not begun") |
| if identifier is not None: |
| kwargs["identifier"] = identifier |
| self._points.append((pt, segmentType, False, name, kwargs)) |
| |
| def addComponent(self, glyphName, transformation, identifier=None, **kwargs): |
| if self._points is not None: |
| raise PenError("Components must be added before or after contours") |
| if identifier is not None: |
| kwargs["identifier"] = identifier |
| self._outPen.addComponent(glyphName, transformation, **kwargs) |
| |
| |
| class ReverseContourPointPen(AbstractPointPen): |
| """ |
| This is a PointPen that passes outline data to another PointPen, but |
| reversing the winding direction of all contours. Components are simply |
| passed through unchanged. |
| |
| Closed contours are reversed in such a way that the first point remains |
| the first point. |
| """ |
| |
| def __init__(self, outputPointPen): |
| self.pen = outputPointPen |
| # a place to store the points for the current sub path |
| self.currentContour = None |
| |
| def _flushContour(self): |
| pen = self.pen |
| contour = self.currentContour |
| if not contour: |
| pen.beginPath(identifier=self.currentContourIdentifier) |
| pen.endPath() |
| return |
| |
| closed = contour[0][1] != "move" |
| if not closed: |
| lastSegmentType = "move" |
| else: |
| # Remove the first point and insert it at the end. When |
| # the list of points gets reversed, this point will then |
| # again be at the start. In other words, the following |
| # will hold: |
| # for N in range(len(originalContour)): |
| # originalContour[N] == reversedContour[-N] |
| contour.append(contour.pop(0)) |
| # Find the first on-curve point. |
| firstOnCurve = None |
| for i in range(len(contour)): |
| if contour[i][1] is not None: |
| firstOnCurve = i |
| break |
| if firstOnCurve is None: |
| # There are no on-curve points, be basically have to |
| # do nothing but contour.reverse(). |
| lastSegmentType = None |
| else: |
| lastSegmentType = contour[firstOnCurve][1] |
| |
| contour.reverse() |
| if not closed: |
| # Open paths must start with a move, so we simply dump |
| # all off-curve points leading up to the first on-curve. |
| while contour[0][1] is None: |
| contour.pop(0) |
| pen.beginPath(identifier=self.currentContourIdentifier) |
| for pt, nextSegmentType, smooth, name, kwargs in contour: |
| if nextSegmentType is not None: |
| segmentType = lastSegmentType |
| lastSegmentType = nextSegmentType |
| else: |
| segmentType = None |
| pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs) |
| pen.endPath() |
| |
| def beginPath(self, identifier=None, **kwargs): |
| if self.currentContour is not None: |
| raise PenError("Path already begun") |
| self.currentContour = [] |
| self.currentContourIdentifier = identifier |
| self.onCurve = [] |
| |
| def endPath(self): |
| if self.currentContour is None: |
| raise PenError("Path not begun") |
| self._flushContour() |
| self.currentContour = None |
| |
| def addPoint(self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs): |
| if self.currentContour is None: |
| raise PenError("Path not begun") |
| if identifier is not None: |
| kwargs["identifier"] = identifier |
| self.currentContour.append((pt, segmentType, smooth, name, kwargs)) |
| |
| def addComponent(self, glyphName, transform, identifier=None, **kwargs): |
| if self.currentContour is not None: |
| raise PenError("Components must be added before or after contours") |
| self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs) |