• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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