|
""" |
|
========= |
|
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, Dict |
|
|
|
from fontTools.pens.basePen import AbstractPen, PenError |
|
from fontTools.misc.transform import DecomposedTransform |
|
|
|
__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 |
|
|
|
def addVarComponent( |
|
self, |
|
glyphName: str, |
|
transformation: DecomposedTransform, |
|
location: Dict[str, float], |
|
identifier: Optional[str] = None, |
|
**kwargs: Any, |
|
) -> None: |
|
"""Add a VarComponent sub glyph. The 'transformation' argument |
|
must be a DecomposedTransform from the fontTools.misc.transform module, |
|
and the 'location' argument must be a dictionary mapping axis tags |
|
to their locations. |
|
""" |
|
|
|
raise AttributeError |
|
|
|
|
|
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: |
|
|
|
pt, segmentType, smooth, name, kwargs = points[0] |
|
segments = [("move", [(pt, smooth, name, kwargs)])] |
|
self._flushContour(segments) |
|
return |
|
segments = [] |
|
if points[0][1] == "move": |
|
|
|
|
|
pt, segmentType, smooth, name, kwargs = points[0] |
|
segments.append(("move", [(pt, smooth, name, kwargs)])) |
|
points.pop(0) |
|
else: |
|
|
|
|
|
|
|
firstOnCurve = None |
|
for i in range(len(points)): |
|
segmentType = points[i][1] |
|
if segmentType is not None: |
|
firstOnCurve = i |
|
break |
|
if firstOnCurve is None: |
|
|
|
|
|
|
|
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": |
|
|
|
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: |
|
|
|
|
|
closed = True |
|
segmentType, points = segments[-1] |
|
movePt, _, _, _ = points[-1] |
|
if movePt is None: |
|
|
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
del kwargs |
|
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: |
|
|
|
|
|
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": |
|
|
|
indices = range(1, nPoints - 1) |
|
elif nPoints > 1: |
|
|
|
|
|
indices = range(-1, nPoints - 1) |
|
else: |
|
|
|
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 |
|
|
|
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) |
|
|
|
def addVarComponent( |
|
self, glyphName, transformation, location, identifier=None, **kwargs |
|
): |
|
if self._points is not None: |
|
raise PenError("VarComponents must be added before or after contours") |
|
if identifier is not None: |
|
kwargs["identifier"] = identifier |
|
self._outPen.addVarComponent(glyphName, transformation, location, **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 |
|
|
|
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: |
|
|
|
|
|
|
|
|
|
|
|
|
|
contour.append(contour.pop(0)) |
|
|
|
firstOnCurve = None |
|
for i in range(len(contour)): |
|
if contour[i][1] is not None: |
|
firstOnCurve = i |
|
break |
|
if firstOnCurve is None: |
|
|
|
|
|
lastSegmentType = None |
|
else: |
|
lastSegmentType = contour[firstOnCurve][1] |
|
|
|
contour.reverse() |
|
if not closed: |
|
|
|
|
|
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) |
|
|