Skip to content

Instantly share code, notes, and snippets.

@okay-type
Created November 23, 2025 22:43
Show Gist options
  • Select an option

  • Save okay-type/7762236338faa105d0968da14544a112 to your computer and use it in GitHub Desktop.

Select an option

Save okay-type/7762236338faa105d0968da14544a112 to your computer and use it in GitHub Desktop.
A fontTools pen that draws an interpolatable SVG (H and V segments are written as L, always adds the final point)
from typing import Callable
from fontTools.pens.basePen import BasePen
def pointToString(pt, ntos=str):
return " ".join(ntos(i) for i in pt)
class SVGPathPenOverlapped(BasePen):
"""Pen to draw SVG path d commands.
Args:
glyphSet: a dictionary of drawable glyph objects keyed by name
used to resolve component references in composite glyphs.
ntos: a callable that takes a number and returns a string, to
customize how numbers are formatted (default: str).
:Example:
.. code-block::
>>> pen = SVGPathPenOverlapped(None)
>>> pen.moveTo((0, 0))
>>> pen.lineTo((1, 1))
>>> pen.curveTo((2, 2), (3, 3), (4, 4))
>>> pen.closePath()
>>> pen.getCommands()
'M0 0 1 1C2 2 3 3 4 4Z'
Note:
Fonts have a coordinate system where Y grows up, whereas in SVG,
Y grows down. As such, rendering path data from this pen in
SVG typically results in upside-down glyphs. You can fix this
by wrapping the data from this pen in an SVG group element with
transform, or wrap this pen in a transform pen. For example:
.. code-block:: python
spen = SVGPathPenOverlapped.SVGPathPenOverlapped(glyphset)
pen= TransformPen(spen , (1, 0, 0, -1, 0, 0))
glyphset[glyphname].draw(pen)
print(tpen.getCommands())
"""
def __init__(self, glyphSet, ntos: Callable[[float], str] = str):
BasePen.__init__(self, glyphSet)
self._commands = []
self._lastCommand = None
self._lastX = None
self._lastY = None
self._ntos = ntos
self._firstX = None
self._firstY = None
def _handleAnchor(self):
"""
>>> pen = SVGPathPenOverlapped(None)
>>> pen.moveTo((0, 0))
>>> pen.moveTo((10, 10))
>>> pen._commands
['M10 10']
"""
if self._lastCommand == " M ":
self._commands.pop(-1)
def _moveTo(self, pt):
"""
>>> pen = SVGPathPenOverlapped(None)
>>> pen.moveTo((0, 0))
>>> pen._commands
['M0 0']
>>> pen = SVGPathPenOverlapped(None)
>>> pen.moveTo((10, 0))
>>> pen._commands
['M10 0']
>>> pen = SVGPathPenOverlapped(None)
>>> pen.moveTo((0, 10))
>>> pen._commands
['M0 10']
"""
self._handleAnchor()
t = " M %s" % (pointToString(pt, self._ntos))
self._commands.append(t)
self._lastCommand = " M "
self._lastX, self._lastY = pt
self._firstX, self._firstY = pt
def _lineTo(self, pt):
"""
# duplicate point
>>> pen = SVGPathPenOverlapped(None)
>>> pen.moveTo((10, 10))
>>> pen.lineTo((10, 10))
>>> pen._commands
['M10 10']
# vertical line
>>> pen = SVGPathPenOverlapped(None)
>>> pen.moveTo((10, 10))
>>> pen.lineTo((10, 0))
>>> pen._commands
['M10 10', 'V0']
# horizontal line
>>> pen = SVGPathPenOverlapped(None)
>>> pen.moveTo((10, 10))
>>> pen.lineTo((0, 10))
>>> pen._commands
['M10 10', 'H0']
# basic
>>> pen = SVGPathPenOverlapped(None)
>>> pen.lineTo((70, 80))
>>> pen._commands
['L70 80']
# basic following a moveto
>>> pen = SVGPathPenOverlapped(None)
>>> pen.moveTo((0, 0))
>>> pen.lineTo((10, 10))
>>> pen._commands
['M0 0', ' 10 10']
"""
x, y = pt
# duplicate point
if x == self._lastX and y == self._lastY:
cmd = " L "
pts = "" + pointToString(pt, self._ntos)
# vertical line
elif x == self._lastX:
cmd = " L "
pts = "" + pointToString(pt, self._ntos)
# horizontal line
elif y == self._lastY:
cmd = " L "
pts = "" + pointToString(pt, self._ntos)
# previous was a moveto
elif self._lastCommand == " M ":
cmd = None
pts = " " + pointToString(pt, self._ntos)
# basic
else:
cmd = " L "
pts = "" + pointToString(pt, self._ntos)
# write the string
t = ""
if cmd:
t += cmd
self._lastCommand = cmd
t += pts
self._commands.append(t)
# store for future reference
self._lastX, self._lastY = pt
def _curveToOne(self, pt1, pt2, pt3):
"""
>>> pen = SVGPathPenOverlapped(None)
>>> pen.curveTo((10, 20), (30, 40), (50, 60))
>>> pen._commands
['C10 20 30 40 50 60']
"""
t = " C "
t += pointToString(pt1, self._ntos) + " "
t += pointToString(pt2, self._ntos) + " "
t += pointToString(pt3, self._ntos)
self._commands.append(t)
self._lastCommand = " C "
self._lastX, self._lastY = pt3
def _qCurveToOne(self, pt1, pt2):
"""
>>> pen = SVGPathPenOverlapped(None)
>>> pen.qCurveTo((10, 20), (30, 40))
>>> pen._commands
['Q10 20 30 40']
>>> from fontTools.misc.roundTools import otRound
>>> pen = SVGPathPenOverlapped(None, ntos=lambda v: str(otRound(v)))
>>> pen.qCurveTo((3, 3), (7, 5), (11, 4))
>>> pen._commands
['Q3 3 5 4', 'Q7 5 11 4']
"""
assert pt2 is not None
t = " Q "
t += pointToString(pt1, self._ntos) + " "
t += pointToString(pt2, self._ntos)
self._commands.append(t)
self._lastCommand = " Q "
self._lastX, self._lastY = pt2
def _closePath(self):
"""
>>> pen = SVGPathPenOverlapped(None)
>>> pen.closePath()
>>> pen._commands
['Z']
"""
if self._lastCommand == " L " and self._lastX != self._firstX or self._lastY != self._firstY:
cmd = " L "
pts = "" + pointToString((self._firstX, self._firstY), self._ntos)
# write the string
t = cmd
self._lastCommand = cmd
t += pts
self._commands.append(t)
# store for future reference
self._lastX, self._lastY = (self._firstX, self._firstY)
self._commands.append(" Z")
self._lastCommand = " Z"
self._lastX = self._lastY = None
def _endPath(self):
"""
>>> pen = SVGPathPenOverlapped(None)
>>> pen.endPath()
>>> pen._commands
[]
"""
self._lastCommand = None
self._lastX = self._lastY = None
def getCommands(self):
return "".join(self._commands)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment