1*e1fe3e4aSElliott Hughesfrom typing import Callable 2*e1fe3e4aSElliott Hughesfrom fontTools.pens.basePen import BasePen 3*e1fe3e4aSElliott Hughes 4*e1fe3e4aSElliott Hughes 5*e1fe3e4aSElliott Hughesdef pointToString(pt, ntos=str): 6*e1fe3e4aSElliott Hughes return " ".join(ntos(i) for i in pt) 7*e1fe3e4aSElliott Hughes 8*e1fe3e4aSElliott Hughes 9*e1fe3e4aSElliott Hughesclass SVGPathPen(BasePen): 10*e1fe3e4aSElliott Hughes """Pen to draw SVG path d commands. 11*e1fe3e4aSElliott Hughes 12*e1fe3e4aSElliott Hughes Example:: 13*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 14*e1fe3e4aSElliott Hughes >>> pen.moveTo((0, 0)) 15*e1fe3e4aSElliott Hughes >>> pen.lineTo((1, 1)) 16*e1fe3e4aSElliott Hughes >>> pen.curveTo((2, 2), (3, 3), (4, 4)) 17*e1fe3e4aSElliott Hughes >>> pen.closePath() 18*e1fe3e4aSElliott Hughes >>> pen.getCommands() 19*e1fe3e4aSElliott Hughes 'M0 0 1 1C2 2 3 3 4 4Z' 20*e1fe3e4aSElliott Hughes 21*e1fe3e4aSElliott Hughes Args: 22*e1fe3e4aSElliott Hughes glyphSet: a dictionary of drawable glyph objects keyed by name 23*e1fe3e4aSElliott Hughes used to resolve component references in composite glyphs. 24*e1fe3e4aSElliott Hughes ntos: a callable that takes a number and returns a string, to 25*e1fe3e4aSElliott Hughes customize how numbers are formatted (default: str). 26*e1fe3e4aSElliott Hughes 27*e1fe3e4aSElliott Hughes Note: 28*e1fe3e4aSElliott Hughes Fonts have a coordinate system where Y grows up, whereas in SVG, 29*e1fe3e4aSElliott Hughes Y grows down. As such, rendering path data from this pen in 30*e1fe3e4aSElliott Hughes SVG typically results in upside-down glyphs. You can fix this 31*e1fe3e4aSElliott Hughes by wrapping the data from this pen in an SVG group element with 32*e1fe3e4aSElliott Hughes transform, or wrap this pen in a transform pen. For example: 33*e1fe3e4aSElliott Hughes 34*e1fe3e4aSElliott Hughes spen = svgPathPen.SVGPathPen(glyphset) 35*e1fe3e4aSElliott Hughes pen= TransformPen(spen , (1, 0, 0, -1, 0, 0)) 36*e1fe3e4aSElliott Hughes glyphset[glyphname].draw(pen) 37*e1fe3e4aSElliott Hughes print(tpen.getCommands()) 38*e1fe3e4aSElliott Hughes """ 39*e1fe3e4aSElliott Hughes 40*e1fe3e4aSElliott Hughes def __init__(self, glyphSet, ntos: Callable[[float], str] = str): 41*e1fe3e4aSElliott Hughes BasePen.__init__(self, glyphSet) 42*e1fe3e4aSElliott Hughes self._commands = [] 43*e1fe3e4aSElliott Hughes self._lastCommand = None 44*e1fe3e4aSElliott Hughes self._lastX = None 45*e1fe3e4aSElliott Hughes self._lastY = None 46*e1fe3e4aSElliott Hughes self._ntos = ntos 47*e1fe3e4aSElliott Hughes 48*e1fe3e4aSElliott Hughes def _handleAnchor(self): 49*e1fe3e4aSElliott Hughes """ 50*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 51*e1fe3e4aSElliott Hughes >>> pen.moveTo((0, 0)) 52*e1fe3e4aSElliott Hughes >>> pen.moveTo((10, 10)) 53*e1fe3e4aSElliott Hughes >>> pen._commands 54*e1fe3e4aSElliott Hughes ['M10 10'] 55*e1fe3e4aSElliott Hughes """ 56*e1fe3e4aSElliott Hughes if self._lastCommand == "M": 57*e1fe3e4aSElliott Hughes self._commands.pop(-1) 58*e1fe3e4aSElliott Hughes 59*e1fe3e4aSElliott Hughes def _moveTo(self, pt): 60*e1fe3e4aSElliott Hughes """ 61*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 62*e1fe3e4aSElliott Hughes >>> pen.moveTo((0, 0)) 63*e1fe3e4aSElliott Hughes >>> pen._commands 64*e1fe3e4aSElliott Hughes ['M0 0'] 65*e1fe3e4aSElliott Hughes 66*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 67*e1fe3e4aSElliott Hughes >>> pen.moveTo((10, 0)) 68*e1fe3e4aSElliott Hughes >>> pen._commands 69*e1fe3e4aSElliott Hughes ['M10 0'] 70*e1fe3e4aSElliott Hughes 71*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 72*e1fe3e4aSElliott Hughes >>> pen.moveTo((0, 10)) 73*e1fe3e4aSElliott Hughes >>> pen._commands 74*e1fe3e4aSElliott Hughes ['M0 10'] 75*e1fe3e4aSElliott Hughes """ 76*e1fe3e4aSElliott Hughes self._handleAnchor() 77*e1fe3e4aSElliott Hughes t = "M%s" % (pointToString(pt, self._ntos)) 78*e1fe3e4aSElliott Hughes self._commands.append(t) 79*e1fe3e4aSElliott Hughes self._lastCommand = "M" 80*e1fe3e4aSElliott Hughes self._lastX, self._lastY = pt 81*e1fe3e4aSElliott Hughes 82*e1fe3e4aSElliott Hughes def _lineTo(self, pt): 83*e1fe3e4aSElliott Hughes """ 84*e1fe3e4aSElliott Hughes # duplicate point 85*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 86*e1fe3e4aSElliott Hughes >>> pen.moveTo((10, 10)) 87*e1fe3e4aSElliott Hughes >>> pen.lineTo((10, 10)) 88*e1fe3e4aSElliott Hughes >>> pen._commands 89*e1fe3e4aSElliott Hughes ['M10 10'] 90*e1fe3e4aSElliott Hughes 91*e1fe3e4aSElliott Hughes # vertical line 92*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 93*e1fe3e4aSElliott Hughes >>> pen.moveTo((10, 10)) 94*e1fe3e4aSElliott Hughes >>> pen.lineTo((10, 0)) 95*e1fe3e4aSElliott Hughes >>> pen._commands 96*e1fe3e4aSElliott Hughes ['M10 10', 'V0'] 97*e1fe3e4aSElliott Hughes 98*e1fe3e4aSElliott Hughes # horizontal line 99*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 100*e1fe3e4aSElliott Hughes >>> pen.moveTo((10, 10)) 101*e1fe3e4aSElliott Hughes >>> pen.lineTo((0, 10)) 102*e1fe3e4aSElliott Hughes >>> pen._commands 103*e1fe3e4aSElliott Hughes ['M10 10', 'H0'] 104*e1fe3e4aSElliott Hughes 105*e1fe3e4aSElliott Hughes # basic 106*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 107*e1fe3e4aSElliott Hughes >>> pen.lineTo((70, 80)) 108*e1fe3e4aSElliott Hughes >>> pen._commands 109*e1fe3e4aSElliott Hughes ['L70 80'] 110*e1fe3e4aSElliott Hughes 111*e1fe3e4aSElliott Hughes # basic following a moveto 112*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 113*e1fe3e4aSElliott Hughes >>> pen.moveTo((0, 0)) 114*e1fe3e4aSElliott Hughes >>> pen.lineTo((10, 10)) 115*e1fe3e4aSElliott Hughes >>> pen._commands 116*e1fe3e4aSElliott Hughes ['M0 0', ' 10 10'] 117*e1fe3e4aSElliott Hughes """ 118*e1fe3e4aSElliott Hughes x, y = pt 119*e1fe3e4aSElliott Hughes # duplicate point 120*e1fe3e4aSElliott Hughes if x == self._lastX and y == self._lastY: 121*e1fe3e4aSElliott Hughes return 122*e1fe3e4aSElliott Hughes # vertical line 123*e1fe3e4aSElliott Hughes elif x == self._lastX: 124*e1fe3e4aSElliott Hughes cmd = "V" 125*e1fe3e4aSElliott Hughes pts = self._ntos(y) 126*e1fe3e4aSElliott Hughes # horizontal line 127*e1fe3e4aSElliott Hughes elif y == self._lastY: 128*e1fe3e4aSElliott Hughes cmd = "H" 129*e1fe3e4aSElliott Hughes pts = self._ntos(x) 130*e1fe3e4aSElliott Hughes # previous was a moveto 131*e1fe3e4aSElliott Hughes elif self._lastCommand == "M": 132*e1fe3e4aSElliott Hughes cmd = None 133*e1fe3e4aSElliott Hughes pts = " " + pointToString(pt, self._ntos) 134*e1fe3e4aSElliott Hughes # basic 135*e1fe3e4aSElliott Hughes else: 136*e1fe3e4aSElliott Hughes cmd = "L" 137*e1fe3e4aSElliott Hughes pts = pointToString(pt, self._ntos) 138*e1fe3e4aSElliott Hughes # write the string 139*e1fe3e4aSElliott Hughes t = "" 140*e1fe3e4aSElliott Hughes if cmd: 141*e1fe3e4aSElliott Hughes t += cmd 142*e1fe3e4aSElliott Hughes self._lastCommand = cmd 143*e1fe3e4aSElliott Hughes t += pts 144*e1fe3e4aSElliott Hughes self._commands.append(t) 145*e1fe3e4aSElliott Hughes # store for future reference 146*e1fe3e4aSElliott Hughes self._lastX, self._lastY = pt 147*e1fe3e4aSElliott Hughes 148*e1fe3e4aSElliott Hughes def _curveToOne(self, pt1, pt2, pt3): 149*e1fe3e4aSElliott Hughes """ 150*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 151*e1fe3e4aSElliott Hughes >>> pen.curveTo((10, 20), (30, 40), (50, 60)) 152*e1fe3e4aSElliott Hughes >>> pen._commands 153*e1fe3e4aSElliott Hughes ['C10 20 30 40 50 60'] 154*e1fe3e4aSElliott Hughes """ 155*e1fe3e4aSElliott Hughes t = "C" 156*e1fe3e4aSElliott Hughes t += pointToString(pt1, self._ntos) + " " 157*e1fe3e4aSElliott Hughes t += pointToString(pt2, self._ntos) + " " 158*e1fe3e4aSElliott Hughes t += pointToString(pt3, self._ntos) 159*e1fe3e4aSElliott Hughes self._commands.append(t) 160*e1fe3e4aSElliott Hughes self._lastCommand = "C" 161*e1fe3e4aSElliott Hughes self._lastX, self._lastY = pt3 162*e1fe3e4aSElliott Hughes 163*e1fe3e4aSElliott Hughes def _qCurveToOne(self, pt1, pt2): 164*e1fe3e4aSElliott Hughes """ 165*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 166*e1fe3e4aSElliott Hughes >>> pen.qCurveTo((10, 20), (30, 40)) 167*e1fe3e4aSElliott Hughes >>> pen._commands 168*e1fe3e4aSElliott Hughes ['Q10 20 30 40'] 169*e1fe3e4aSElliott Hughes >>> from fontTools.misc.roundTools import otRound 170*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None, ntos=lambda v: str(otRound(v))) 171*e1fe3e4aSElliott Hughes >>> pen.qCurveTo((3, 3), (7, 5), (11, 4)) 172*e1fe3e4aSElliott Hughes >>> pen._commands 173*e1fe3e4aSElliott Hughes ['Q3 3 5 4', 'Q7 5 11 4'] 174*e1fe3e4aSElliott Hughes """ 175*e1fe3e4aSElliott Hughes assert pt2 is not None 176*e1fe3e4aSElliott Hughes t = "Q" 177*e1fe3e4aSElliott Hughes t += pointToString(pt1, self._ntos) + " " 178*e1fe3e4aSElliott Hughes t += pointToString(pt2, self._ntos) 179*e1fe3e4aSElliott Hughes self._commands.append(t) 180*e1fe3e4aSElliott Hughes self._lastCommand = "Q" 181*e1fe3e4aSElliott Hughes self._lastX, self._lastY = pt2 182*e1fe3e4aSElliott Hughes 183*e1fe3e4aSElliott Hughes def _closePath(self): 184*e1fe3e4aSElliott Hughes """ 185*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 186*e1fe3e4aSElliott Hughes >>> pen.closePath() 187*e1fe3e4aSElliott Hughes >>> pen._commands 188*e1fe3e4aSElliott Hughes ['Z'] 189*e1fe3e4aSElliott Hughes """ 190*e1fe3e4aSElliott Hughes self._commands.append("Z") 191*e1fe3e4aSElliott Hughes self._lastCommand = "Z" 192*e1fe3e4aSElliott Hughes self._lastX = self._lastY = None 193*e1fe3e4aSElliott Hughes 194*e1fe3e4aSElliott Hughes def _endPath(self): 195*e1fe3e4aSElliott Hughes """ 196*e1fe3e4aSElliott Hughes >>> pen = SVGPathPen(None) 197*e1fe3e4aSElliott Hughes >>> pen.endPath() 198*e1fe3e4aSElliott Hughes >>> pen._commands 199*e1fe3e4aSElliott Hughes [] 200*e1fe3e4aSElliott Hughes """ 201*e1fe3e4aSElliott Hughes self._lastCommand = None 202*e1fe3e4aSElliott Hughes self._lastX = self._lastY = None 203*e1fe3e4aSElliott Hughes 204*e1fe3e4aSElliott Hughes def getCommands(self): 205*e1fe3e4aSElliott Hughes return "".join(self._commands) 206*e1fe3e4aSElliott Hughes 207*e1fe3e4aSElliott Hughes 208*e1fe3e4aSElliott Hughesdef main(args=None): 209*e1fe3e4aSElliott Hughes """Generate per-character SVG from font and text""" 210*e1fe3e4aSElliott Hughes 211*e1fe3e4aSElliott Hughes if args is None: 212*e1fe3e4aSElliott Hughes import sys 213*e1fe3e4aSElliott Hughes 214*e1fe3e4aSElliott Hughes args = sys.argv[1:] 215*e1fe3e4aSElliott Hughes 216*e1fe3e4aSElliott Hughes from fontTools.ttLib import TTFont 217*e1fe3e4aSElliott Hughes import argparse 218*e1fe3e4aSElliott Hughes 219*e1fe3e4aSElliott Hughes parser = argparse.ArgumentParser( 220*e1fe3e4aSElliott Hughes "fonttools pens.svgPathPen", description="Generate SVG from text" 221*e1fe3e4aSElliott Hughes ) 222*e1fe3e4aSElliott Hughes parser.add_argument("font", metavar="font.ttf", help="Font file.") 223*e1fe3e4aSElliott Hughes parser.add_argument("text", metavar="text", nargs="?", help="Text string.") 224*e1fe3e4aSElliott Hughes parser.add_argument( 225*e1fe3e4aSElliott Hughes "-y", 226*e1fe3e4aSElliott Hughes metavar="<number>", 227*e1fe3e4aSElliott Hughes help="Face index into a collection to open. Zero based.", 228*e1fe3e4aSElliott Hughes ) 229*e1fe3e4aSElliott Hughes parser.add_argument( 230*e1fe3e4aSElliott Hughes "--glyphs", 231*e1fe3e4aSElliott Hughes metavar="whitespace-separated list of glyph names", 232*e1fe3e4aSElliott Hughes type=str, 233*e1fe3e4aSElliott Hughes help="Glyphs to show. Exclusive with text option", 234*e1fe3e4aSElliott Hughes ) 235*e1fe3e4aSElliott Hughes parser.add_argument( 236*e1fe3e4aSElliott Hughes "--variations", 237*e1fe3e4aSElliott Hughes metavar="AXIS=LOC", 238*e1fe3e4aSElliott Hughes default="", 239*e1fe3e4aSElliott Hughes help="List of space separated locations. A location consist in " 240*e1fe3e4aSElliott Hughes "the name of a variation axis, followed by '=' and a number. E.g.: " 241*e1fe3e4aSElliott Hughes "wght=700 wdth=80. The default is the location of the base master.", 242*e1fe3e4aSElliott Hughes ) 243*e1fe3e4aSElliott Hughes 244*e1fe3e4aSElliott Hughes options = parser.parse_args(args) 245*e1fe3e4aSElliott Hughes 246*e1fe3e4aSElliott Hughes fontNumber = int(options.y) if options.y is not None else 0 247*e1fe3e4aSElliott Hughes 248*e1fe3e4aSElliott Hughes font = TTFont(options.font, fontNumber=fontNumber) 249*e1fe3e4aSElliott Hughes text = options.text 250*e1fe3e4aSElliott Hughes glyphs = options.glyphs 251*e1fe3e4aSElliott Hughes 252*e1fe3e4aSElliott Hughes location = {} 253*e1fe3e4aSElliott Hughes for tag_v in options.variations.split(): 254*e1fe3e4aSElliott Hughes fields = tag_v.split("=") 255*e1fe3e4aSElliott Hughes tag = fields[0].strip() 256*e1fe3e4aSElliott Hughes v = float(fields[1]) 257*e1fe3e4aSElliott Hughes location[tag] = v 258*e1fe3e4aSElliott Hughes 259*e1fe3e4aSElliott Hughes hhea = font["hhea"] 260*e1fe3e4aSElliott Hughes ascent, descent = hhea.ascent, hhea.descent 261*e1fe3e4aSElliott Hughes 262*e1fe3e4aSElliott Hughes glyphset = font.getGlyphSet(location=location) 263*e1fe3e4aSElliott Hughes cmap = font["cmap"].getBestCmap() 264*e1fe3e4aSElliott Hughes 265*e1fe3e4aSElliott Hughes if glyphs is not None and text is not None: 266*e1fe3e4aSElliott Hughes raise ValueError("Options --glyphs and --text are exclusive") 267*e1fe3e4aSElliott Hughes 268*e1fe3e4aSElliott Hughes if glyphs is None: 269*e1fe3e4aSElliott Hughes glyphs = " ".join(cmap[ord(u)] for u in text) 270*e1fe3e4aSElliott Hughes 271*e1fe3e4aSElliott Hughes glyphs = glyphs.split() 272*e1fe3e4aSElliott Hughes 273*e1fe3e4aSElliott Hughes s = "" 274*e1fe3e4aSElliott Hughes width = 0 275*e1fe3e4aSElliott Hughes for g in glyphs: 276*e1fe3e4aSElliott Hughes glyph = glyphset[g] 277*e1fe3e4aSElliott Hughes 278*e1fe3e4aSElliott Hughes pen = SVGPathPen(glyphset) 279*e1fe3e4aSElliott Hughes glyph.draw(pen) 280*e1fe3e4aSElliott Hughes commands = pen.getCommands() 281*e1fe3e4aSElliott Hughes 282*e1fe3e4aSElliott Hughes s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % ( 283*e1fe3e4aSElliott Hughes width, 284*e1fe3e4aSElliott Hughes ascent, 285*e1fe3e4aSElliott Hughes commands, 286*e1fe3e4aSElliott Hughes ) 287*e1fe3e4aSElliott Hughes 288*e1fe3e4aSElliott Hughes width += glyph.width 289*e1fe3e4aSElliott Hughes 290*e1fe3e4aSElliott Hughes print('<?xml version="1.0" encoding="UTF-8"?>') 291*e1fe3e4aSElliott Hughes print( 292*e1fe3e4aSElliott Hughes '<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">' 293*e1fe3e4aSElliott Hughes % (width, ascent - descent) 294*e1fe3e4aSElliott Hughes ) 295*e1fe3e4aSElliott Hughes print(s, end="") 296*e1fe3e4aSElliott Hughes print("</svg>") 297*e1fe3e4aSElliott Hughes 298*e1fe3e4aSElliott Hughes 299*e1fe3e4aSElliott Hughesif __name__ == "__main__": 300*e1fe3e4aSElliott Hughes import sys 301*e1fe3e4aSElliott Hughes 302*e1fe3e4aSElliott Hughes if len(sys.argv) == 1: 303*e1fe3e4aSElliott Hughes import doctest 304*e1fe3e4aSElliott Hughes 305*e1fe3e4aSElliott Hughes sys.exit(doctest.testmod().failed) 306*e1fe3e4aSElliott Hughes 307*e1fe3e4aSElliott Hughes sys.exit(main()) 308