Skip to content

Instantly share code, notes, and snippets.

@typesupply
Created April 28, 2024 22:12
Show Gist options
  • Save typesupply/2cf13438da874169c21090e215326a82 to your computer and use it in GitHub Desktop.
Save typesupply/2cf13438da874169c21090e215326a82 to your computer and use it in GitHub Desktop.
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