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). 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] = 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 = 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="", 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 += '\n' % ( width, ascent, commands, ) width += glyph.width print('') print( '' % (width, ascent - descent) ) print(s, end="") print("") if __name__ == "__main__": import sys if len(sys.argv) == 1: import doctest sys.exit(doctest.testmod().failed) sys.exit(main())