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