Using a RoundingPen as a filter doesn't work for TrueType quadratic splines (qCurveTo), because the rounding would occur too early, before these get split into atomic quadratic Bezier segments (by the superclass BasePen.qCurveTo method), thus leaving unwanted floating-point coordinates in the SVG output. So here we add a 'ntos' parameter (by default simpli calls str()) that takes a Callable[[float], str] and can be used to customize the formatting of the numbers in SVG path coordinates.
200 lines
5.3 KiB
Python
200 lines
5.3 KiB
Python
from typing import Callable
|
|
from fontTools.pens.basePen import BasePen
|
|
|
|
|
|
def pointToString(pt, ntos=str):
|
|
return " ".join(ntos(i) for i in pt)
|
|
|
|
|
|
class SVGPathPen(BasePen):
|
|
""" Pen to draw SVG path d commands.
|
|
|
|
Example::
|
|
>>> pen = SVGPathPen(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'
|
|
|
|
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).
|
|
"""
|
|
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
|
|
|
|
def _handleAnchor(self):
|
|
"""
|
|
>>> pen = SVGPathPen(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 = SVGPathPen(None)
|
|
>>> pen.moveTo((0, 0))
|
|
>>> pen._commands
|
|
['M0 0']
|
|
|
|
>>> pen = SVGPathPen(None)
|
|
>>> pen.moveTo((10, 0))
|
|
>>> pen._commands
|
|
['M10 0']
|
|
|
|
>>> pen = SVGPathPen(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
|
|
|
|
def _lineTo(self, pt):
|
|
"""
|
|
# duplicate point
|
|
>>> pen = SVGPathPen(None)
|
|
>>> pen.moveTo((10, 10))
|
|
>>> pen.lineTo((10, 10))
|
|
>>> pen._commands
|
|
['M10 10']
|
|
|
|
# vertical line
|
|
>>> pen = SVGPathPen(None)
|
|
>>> pen.moveTo((10, 10))
|
|
>>> pen.lineTo((10, 0))
|
|
>>> pen._commands
|
|
['M10 10', 'V0']
|
|
|
|
# horizontal line
|
|
>>> pen = SVGPathPen(None)
|
|
>>> pen.moveTo((10, 10))
|
|
>>> pen.lineTo((0, 10))
|
|
>>> pen._commands
|
|
['M10 10', 'H0']
|
|
|
|
# basic
|
|
>>> pen = SVGPathPen(None)
|
|
>>> pen.lineTo((70, 80))
|
|
>>> pen._commands
|
|
['L70 80']
|
|
|
|
# basic following a moveto
|
|
>>> pen = SVGPathPen(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:
|
|
return
|
|
# vertical line
|
|
elif x == self._lastX:
|
|
cmd = "V"
|
|
pts = str(y)
|
|
# horizontal line
|
|
elif y == self._lastY:
|
|
cmd = "H"
|
|
pts = str(x)
|
|
# 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 = SVGPathPen(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 = SVGPathPen(None)
|
|
>>> pen.qCurveTo((10, 20), (30, 40))
|
|
>>> pen._commands
|
|
['Q10 20 30 40']
|
|
>>> from fontTools.misc.roundTools import otRound
|
|
>>> pen = SVGPathPen(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 = SVGPathPen(None)
|
|
>>> pen.closePath()
|
|
>>> pen._commands
|
|
['Z']
|
|
"""
|
|
self._commands.append("Z")
|
|
self._lastCommand = "Z"
|
|
self._lastX = self._lastY = None
|
|
|
|
def _endPath(self):
|
|
"""
|
|
>>> pen = SVGPathPen(None)
|
|
>>> pen.endPath()
|
|
>>> pen._commands
|
|
['Z']
|
|
"""
|
|
self._closePath()
|
|
self._lastCommand = None
|
|
self._lastX = self._lastY = None
|
|
|
|
def getCommands(self):
|
|
return "".join(self._commands)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
import doctest
|
|
sys.exit(doctest.testmod().failed)
|