Created
April 28, 2024 22:12
-
-
Save typesupply/2cf13438da874169c21090e215326a82 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from fontTools.pens.pointPen import ( | |
AbstractPointPen, | |
GuessSmoothPointPen | |
) | |
from fontParts.base.bPoint import ( | |
relativeBCPIn, | |
absoluteBCPIn, | |
relativeBCPOut, | |
absoluteBCPOut | |
) | |
def glyph_drawBPoints(glyph, bPointPen): | |
""" | |
XXX this will be added to BaseGlyph. | |
Draw the glyph's outline data to the given :ref:`type-b-point-pen`. | |
>>> contour.drawBPoints(bPointPen) | |
""" | |
for contour in glyph: | |
contour_drawBPoints(contour, bPointPen) | |
def contour_drawBPoints(contour, bPointPen): | |
""" | |
XXX this will be added to BaseContour. | |
Draw the contour's outline data to the given :ref:`type-b-point-pen`. | |
>>> contour.drawBPoints(bPointPen) | |
""" | |
bPointPen.beginPath( | |
open=contour.open, | |
identifier=contour.identifier | |
) | |
for bPoint in contour.bPoints: | |
anchorObject = bPoint.anchorPointObject | |
bcpInObject = bPoint.bcpInPointObject | |
bcpOutObject = bPoint.bcpOutPointObject | |
anchorName = anchorObject.name | |
anchorIdentifier = anchorObject.identifier | |
bcpInName = None | |
bcpInIdentifier = None | |
bcpOutName = None | |
bcpOutIdentifier = None | |
if bcpInObject is not None: | |
bcpInName = bcpInObject.name | |
bcpInIdentifier = bcpInObject.identifier | |
if bcpOutObject is not None: | |
bcpOutName = bcpOutObject.name | |
bcpOutName = bcpOutObject.identifier | |
bPointPen.addBPoint( | |
type=bPoint.type, | |
anchor=bPoint.anchor, | |
bcpIn=bPoint.bcpIn, | |
bcpOut=bPoint.bcpOut, | |
anchorName=anchorName, | |
bcpInName=bcpInName, | |
bcpOutName=bcpOutName, | |
anchorIdentifier=anchorIdentifier, | |
bcpInIdentifier=bcpInIdentifier, | |
bcpOutIdentifier=bcpOutIdentifier | |
) | |
bPointPen.endPath() | |
class BPointPenError(Exception): pass | |
class AbstractBPointPen: | |
def beginPath(self, | |
open=False, | |
identifier=None, | |
**kwargs | |
): | |
""" | |
Begin a new contour. | |
""" | |
raise NotImplementedError | |
def endPath(self, | |
**kwargs | |
): | |
""" | |
End the current contour. | |
""" | |
raise NotImplementedError | |
def addBPoint(self, | |
type=None, | |
anchor=None, | |
bcpIn=None, | |
bcpOut=None, | |
anchorName=None, | |
bcpInName=None, | |
bcpOutName=None, | |
anchorIdentifier=None, | |
bcpInIdentifier=None, | |
bcpOutIdentifier=None, | |
**kwargs | |
): | |
""" | |
Add a bPoint to the current contour. | |
Refer to the fontParts | |
`BPoint documentation <https://fontparts.robotools.dev/en/stable/objectref/objects/bpoint.html>`_ | |
for details on how the values are structured. | |
""" | |
raise NotImplementedError | |
class PrintingBPointPen(AbstractBPointPen): | |
""" | |
A printing BPointPen. | |
""" | |
def beginPath(self, | |
open=False, | |
identifier=None, | |
**kwargs | |
): | |
print("BPointPen.beginPath(") | |
print(f" open={open},") | |
print(f" identifier={identifier})") | |
print(")") | |
def endPath(self, | |
**kwargs | |
): | |
print("BPointPen.endPath()") | |
def addBPoint(self, | |
type=None, | |
anchor=None, | |
bcpIn=None, | |
bcpOut=None, | |
anchorName=None, | |
bcpInName=None, | |
bcpOutName=None, | |
anchorIdentifier=None, | |
bcpInIdentifier=None, | |
bcpOutIdentifier=None, | |
**kwargs | |
): | |
print("BPointPen.addBPoint(") | |
print(f" type={type},") | |
print(f" anchor={anchor},") | |
print(f" bcpIn={bcpIn},") | |
print(f" bcpOut={bcpOut},") | |
print(f" anchorName={anchorName},") | |
print(f" bcpInName={bcpInName},") | |
print(f" bcpOutName={bcpOutName},") | |
print(f" anchorIdentifier={anchorIdentifier},") | |
print(f" bcpInIdentifier={bcpInIdentifier},") | |
print(f" bcpOutIdentifier={bcpOutIdentifier}") | |
print(")") | |
class FilterBPointPen(AbstractBPointPen): | |
""" | |
A base class for pens that modify incoming | |
bPoints and then pass them to `outPen`. | |
""" | |
def __init__(self, outPen): | |
self._outPen = outPen | |
def beginPath(self, | |
open=False, | |
identifier=None, | |
**kwargs | |
): | |
self._outPen.beginPath( | |
open=open, | |
identifier=identifier, | |
**kwargs | |
) | |
def endPath(self, | |
**kwargs | |
): | |
self._outPen.endPath() | |
def addBPoint(self, | |
type=None, | |
anchor=None, | |
bcpIn=None, | |
bcpOut=None, | |
anchorName=None, | |
bcpInName=None, | |
bcpOutName=None, | |
anchorIdentifier=None, | |
bcpInIdentifier=None, | |
bcpOutIdentifier=None, | |
**kwargs | |
): | |
self._outPen.addBPoint( | |
type=type, | |
anchor=anchor, | |
bcpIn=bcpIn, | |
bcpOut=bcpOut, | |
anchorName=anchorName, | |
bcpInName=bcpInName, | |
bcpOutName=bcpOutName, | |
anchorIdentifier=anchorIdentifier, | |
bcpInIdentifier=bcpInIdentifier, | |
bcpOutIdentifier=bcpOutIdentifier, | |
**kwargs | |
) | |
class RecordingBPointPen(AbstractBPointPen): | |
""" | |
A pen that records the data written to the pen. | |
The data will be stored as a list at the value | |
attribute with items in the list representing | |
contours with this form: | |
{ | |
closed: bool, | |
identifier : None | identifier, | |
bPoints : [] | |
} | |
The items in the bPoints list will be dicts with key/value | |
pairs corresponding to the arguments of the BPointPen.addBPoint | |
method. | |
""" | |
def __init__(self): | |
self.value = [] | |
def beginPath(self, | |
open=False, | |
identifier=None, | |
**kwargs | |
): | |
contour = dict( | |
open=open, | |
identifier=identifier, | |
bPoints=[] | |
) | |
self.value.append(contour) | |
def endPath(self, | |
**kwargs | |
): | |
pass | |
def addBPoint(self, | |
type=None, | |
anchor=None, | |
bcpIn=None, | |
bcpOut=None, | |
anchorName=None, | |
bcpInName=None, | |
bcpOutName=None, | |
anchorIdentifier=None, | |
bcpInIdentifier=None, | |
bcpOutIdentifier=None, | |
**kwargs | |
): | |
bPoint = dict( | |
type=type, | |
anchor=anchor, | |
bcpIn=bcpIn, | |
bcpOut=bcpOut, | |
anchorName=anchorName, | |
bcpInName=bcpInName, | |
bcpOutName=bcpOutName, | |
anchorIdentifier=anchorIdentifier, | |
bcpInIdentifier=bcpInIdentifier, | |
bcpOutIdentifier=bcpOutIdentifier, | |
) | |
self.value[-1]["bPoints"].append(bPoint) | |
class ContourFilterBPointPen(RecordingBPointPen): | |
""" | |
A "buffered" filter pen that accumulates contour data, | |
passes it through a `filterContour method when the | |
contour is closed or ended, and finally draws the result | |
with the output pen. | |
Inspired by `fontTools.pens.filterPen.ContourFilterPen` | |
(which is also where the above paragraph came from). | |
""" | |
def __init__(self, outPen): | |
super().__init__() | |
self._outPen = outPen | |
def endPath(self, | |
**kwargs | |
): | |
super().endPath(**kwargs) | |
contour = self.value.pop(0) | |
result = self.filterContour( | |
contour=contour | |
) | |
if result is None: | |
result = contour | |
self._outPen.beginPath( | |
open=result["open"], | |
identifier=result["identifier"] | |
) | |
for bPoint in result["bPoints"]: | |
self._outPen.addBPoint(**bPoint) | |
self._outPen.endPath() | |
def filterContour(self, | |
contour, | |
**kwargs | |
): | |
""" | |
Filter the given contour. The contour will be a dict with | |
the structure defined in RecordingBPointPen. | |
This can return a dict of the same form and the returned | |
dict be written to outPen. The contour dict and bPoint list | |
can also be modified in place instead of returning a new one | |
if simple data manipulations are the only changes. | |
""" | |
pass | |
# -------- | |
# Adapters | |
# -------- | |
class BPointToPointPen(RecordingBPointPen): | |
def __init__(self, outPen): | |
super().__init__() | |
# converting the smooth value is a bit | |
# complex, so defer to the converter | |
# that has been in use for decades | |
self._smoothPen = GuessSmoothPointPen(outPen) | |
self._outPen = outPen | |
def endPath(self, **kwargs): | |
super().endPath(**kwargs) | |
contour = self.value[-1] | |
contourOpen = contour["open"] | |
contourIdentifier = contour["identifier"] | |
bPoints = contour["bPoints"] | |
points = [] | |
previousBPoint = bPoints[-1] | |
for i, bPoint in enumerate(bPoints): | |
previousAnchor = previousBPoint["anchor"] | |
previousBCPOut = previousBPoint["bcpOut"] | |
anchor = bPoint["anchor"] | |
bcpIn = bPoint["bcpIn"] | |
if previousBCPOut == (0, 0) and bcpIn == (0, 0): | |
point = dict( | |
pt=anchor, | |
segmentType="line", | |
name=bPoint["anchorName"], | |
identifier=bPoint["anchorIdentifier"] | |
) | |
points.append(point) | |
else: | |
point1 = dict( | |
pt=absoluteBCPOut(previousAnchor, previousBCPOut), | |
segmentType=None, | |
name=previousBPoint["bcpOutName"], | |
identifier=previousBPoint["bcpOutIdentifier"] | |
) | |
point2 = dict( | |
pt=absoluteBCPIn(anchor, bcpIn), | |
segmentType=None, | |
name=bPoint["bcpInName"], | |
identifier=bPoint["bcpInIdentifier"] | |
) | |
point3 = dict( | |
pt=anchor, | |
segmentType="curve", | |
smooth=bPoint["type"]=="curve", | |
name=bPoint["anchorName"], | |
identifier=bPoint["anchorIdentifier"] | |
) | |
points.extend([point1, point2, point3]) | |
previousBPoint = bPoint | |
# move leading off curves to end | |
start = [] | |
end = [] | |
for point in points: | |
if start: | |
start.append(point) | |
else: | |
if point["segmentType"] is not None: | |
start.append(point) | |
else: | |
end.append(point) | |
points = start + end | |
# handle open contours | |
if contourOpen: | |
points[0]["segmentType"] = "move" | |
while 1: | |
if points[-1]["segmentType"] is None: | |
points.pop(-1) | |
else: | |
break | |
self._smoothPen.beginPath( | |
identifier=contourIdentifier | |
) | |
for point in points: | |
self._smoothPen.addPoint(**point) | |
self._smoothPen.endPath() | |
class PointToBPointPen(AbstractPointPen): | |
def __init__(self, outPen): | |
super().__init__() | |
self._outPen = outPen | |
def beginPath(self, | |
identifier=None, | |
**kwargs | |
): | |
self._contourOpen = False | |
self._contourIdentifier = None | |
self._contour = [] | |
def endPath(self): | |
self._flushContour() | |
def addPoint(self, | |
pt, | |
segmentType=None, | |
smooth=False, | |
name=None, | |
identifier=None, | |
**kwargs | |
): | |
if segmentType == "move": | |
for point in self._contour: | |
if point["type"] is not None: | |
raise BPointPenError("A move point is defined after another oncurve point.") | |
self._contourOpen = True | |
point = dict( | |
point=pt, | |
type=segmentType, | |
smooth=smooth, | |
name=name, | |
identifier=identifier | |
) | |
self._contour.append(point) | |
def addComponent(self, *args, **kwargs): | |
pass | |
def _flushContour(self): | |
offCurveCount = 0 | |
for point in reversed(self._contour): | |
if point["type"] != None: | |
break | |
offCurveCount += 1 | |
bPoints = [] | |
for i, point in enumerate(self._contour): | |
if point["type"] is None: | |
offCurveCount += 1 | |
continue | |
if offCurveCount > 2: | |
raise BPointPenError("This contour has too many consecutive offcurve points to be represented with a bPoint.") | |
offCurveCount = 0 | |
anchor = point["point"] | |
anchorName = point["name"] | |
anchorIdentifier = point["identifier"] | |
bcpIn = (0, 0) | |
bcpInName = None | |
bcpInIdentifier = None | |
bcpOut = (0, 0) | |
bcpOutName = None | |
bcpOutIdentifier = None | |
h = i - 1 | |
j = i + 1 | |
if j >= len(self._contour): | |
j = 0 | |
prevPoint = self._contour[h] | |
nextPoint = self._contour[j] | |
if prevPoint["type"] is None: | |
bcpIn = relativeBCPIn(anchor, prevPoint["point"]) | |
bcpInName = prevPoint["name"] | |
bcpInIdentifier = prevPoint["identifier"] | |
if nextPoint["type"] is None: | |
bcpOut = relativeBCPOut(anchor, nextPoint["point"]) | |
bcpOutName = nextPoint["name"] | |
bcpOutIdentifier = nextPoint["identifier"] | |
pointType = point["type"] | |
if pointType not in ("move", "line", "curve"): | |
raise BPointPenError("This contour has an oncurve point that can not be represented with a bPoint.") | |
# this type logic seems convoluted, but it | |
# has been working since 2003, so... | |
anchorType = "corner" | |
if point["smooth"]: | |
if pointType == "curve": | |
anchorType = "curve" | |
elif pointType == "line" or pointType == "move": | |
if nextPoint["type"] is None: | |
anchorType = "curve" | |
else: | |
anchorType = "corner" | |
elif pointType in ("move", "line", "curve"): | |
anchorType = "corner" | |
bPoint = dict( | |
type=anchorType, | |
anchor=anchor, | |
bcpIn=bcpIn, | |
bcpOut=bcpOut, | |
anchorName=anchorName, | |
bcpInName=bcpInName, | |
bcpOutName=bcpOutName, | |
anchorIdentifier=anchorIdentifier, | |
bcpInIdentifier=bcpInIdentifier, | |
bcpOutIdentifier=bcpOutIdentifier, | |
) | |
bPoints.append(bPoint) | |
self._outPen.beginPath( | |
open=self._contourOpen, | |
identifier=self._contourIdentifier | |
) | |
for bPoint in bPoints: | |
self._outPen.addBPoint(**bPoint) | |
self._outPen.endPath() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment