Spaces:
Running
Running
""" | |
========= | |
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.misc.loggingTools import LogMixin | |
from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError | |
from fontTools.misc.transform import DecomposedTransform, Identity | |
__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. | |
""" | |
# ttGlyphSet decomposes for us | |
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: | |
# 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) | |
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 | |
# 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) | |
class DecomposingPointPen(LogMixin, AbstractPointPen): | |
"""Implements a 'addComponent' method that decomposes components | |
(i.e. draws them onto self as simple contours). | |
It can also be used as a mixin class (e.g. see DecomposingRecordingPointPen). | |
You must override beginPath, addPoint, endPath. You may | |
additionally override addVarComponent and addComponent. | |
By default a warning message is logged when a base glyph is missing; | |
set the class variable ``skipMissingComponents`` to False if you want | |
all instances of a sub-class to raise a :class:`MissingComponentError` | |
exception by default. | |
""" | |
skipMissingComponents = True | |
# alias error for convenience | |
MissingComponentError = MissingComponentError | |
def __init__( | |
self, | |
glyphSet, | |
*args, | |
skipMissingComponents=None, | |
reverseFlipped=False, | |
**kwargs, | |
): | |
"""Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced | |
as components are looked up by their name. | |
If the optional 'reverseFlipped' argument is True, components whose transformation | |
matrix has a negative determinant will be decomposed with a reversed path direction | |
to compensate for the flip. | |
The optional 'skipMissingComponents' argument can be set to True/False to | |
override the homonymous class attribute for a given pen instance. | |
""" | |
super().__init__(*args, **kwargs) | |
self.glyphSet = glyphSet | |
self.skipMissingComponents = ( | |
self.__class__.skipMissingComponents | |
if skipMissingComponents is None | |
else skipMissingComponents | |
) | |
self.reverseFlipped = reverseFlipped | |
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs): | |
"""Transform the points of the base glyph and draw it onto self. | |
The `identifier` parameter and any extra kwargs are ignored. | |
""" | |
from fontTools.pens.transformPen import TransformPointPen | |
try: | |
glyph = self.glyphSet[baseGlyphName] | |
except KeyError: | |
if not self.skipMissingComponents: | |
raise MissingComponentError(baseGlyphName) | |
self.log.warning( | |
"glyph '%s' is missing from glyphSet; skipped" % baseGlyphName | |
) | |
else: | |
pen = self | |
if transformation != Identity: | |
pen = TransformPointPen(pen, transformation) | |
if self.reverseFlipped: | |
# if the transformation has a negative determinant, it will | |
# reverse the contour direction of the component | |
a, b, c, d = transformation[:4] | |
det = a * d - b * c | |
if a * d - b * c < 0: | |
pen = ReverseContourPointPen(pen) | |
glyph.drawPoints(pen) | |