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