Spaces:
Running
Running
from typing import Callable | |
from fontTools.pens.basePen import BasePen | |
def pointToString(pt, ntos): | |
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). | |
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: | |
spen = svgPathPen.SVGPathPen(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] = ( | |
lambda x: ("%.2f" % x) if x != int(x) else str(int(x)) | |
), | |
): | |
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 = self._ntos(y) | |
# horizontal line | |
elif y == self._lastY: | |
cmd = "H" | |
pts = self._ntos(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 | |
[] | |
""" | |
self._lastCommand = None | |
self._lastX = self._lastY = None | |
def getCommands(self): | |
return "".join(self._commands) | |
def main(args=None): | |
"""Generate per-character SVG from font and text""" | |
if args is None: | |
import sys | |
args = sys.argv[1:] | |
from fontTools.ttLib import TTFont | |
import argparse | |
parser = argparse.ArgumentParser( | |
"fonttools pens.svgPathPen", description="Generate SVG from text" | |
) | |
parser.add_argument("font", metavar="font.ttf", help="Font file.") | |
parser.add_argument("text", metavar="text", nargs="?", help="Text string.") | |
parser.add_argument( | |
"-y", | |
metavar="<number>", | |
help="Face index into a collection to open. Zero based.", | |
) | |
parser.add_argument( | |
"--glyphs", | |
metavar="whitespace-separated list of glyph names", | |
type=str, | |
help="Glyphs to show. Exclusive with text option", | |
) | |
parser.add_argument( | |
"--variations", | |
metavar="AXIS=LOC", | |
default="", | |
help="List of space separated locations. A location consist in " | |
"the name of a variation axis, followed by '=' and a number. E.g.: " | |
"wght=700 wdth=80. The default is the location of the base master.", | |
) | |
options = parser.parse_args(args) | |
fontNumber = int(options.y) if options.y is not None else 0 | |
font = TTFont(options.font, fontNumber=fontNumber) | |
text = options.text | |
glyphs = options.glyphs | |
location = {} | |
for tag_v in options.variations.split(): | |
fields = tag_v.split("=") | |
tag = fields[0].strip() | |
v = float(fields[1]) | |
location[tag] = v | |
hhea = font["hhea"] | |
ascent, descent = hhea.ascent, hhea.descent | |
glyphset = font.getGlyphSet(location=location) | |
cmap = font["cmap"].getBestCmap() | |
if glyphs is not None and text is not None: | |
raise ValueError("Options --glyphs and --text are exclusive") | |
if glyphs is None: | |
glyphs = " ".join(cmap[ord(u)] for u in text) | |
glyphs = glyphs.split() | |
s = "" | |
width = 0 | |
for g in glyphs: | |
glyph = glyphset[g] | |
pen = SVGPathPen(glyphset) | |
glyph.draw(pen) | |
commands = pen.getCommands() | |
s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % ( | |
width, | |
ascent, | |
commands, | |
) | |
width += glyph.width | |
print('<?xml version="1.0" encoding="UTF-8"?>') | |
print( | |
'<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">' | |
% (width, ascent - descent) | |
) | |
print(s, end="") | |
print("</svg>") | |
if __name__ == "__main__": | |
import sys | |
if len(sys.argv) == 1: | |
import doctest | |
sys.exit(doctest.testmod().failed) | |
sys.exit(main()) | |