1# SVG Path specification parser.
2# This is an adaptation from 'svg.path' by Lennart Regebro (@regebro),
3# modified so that the parser takes a FontTools Pen object instead of
4# returning a list of svg.path Path objects.
5# The original code can be found at:
6# https://github.com/regebro/svg.path/blob/4f9b6e3/src/svg/path/parser.py
7# Copyright (c) 2013-2014 Lennart Regebro
8# License: MIT
9
10from __future__ import (
11    print_function, division, absolute_import, unicode_literals)
12from fontTools.misc.py23 import *
13from .arc import EllipticalArc
14import re
15
16
17COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
18UPPERCASE = set('MZLHVCSQTA')
19
20COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
21FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
22
23
24def _tokenize_path(pathdef):
25    for x in COMMAND_RE.split(pathdef):
26        if x in COMMANDS:
27            yield x
28        for token in FLOAT_RE.findall(x):
29            yield token
30
31
32def parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc):
33    """ Parse SVG path definition (i.e. "d" attribute of <path> elements)
34    and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath
35    methods.
36
37    If 'current_pos' (2-float tuple) is provided, the initial moveTo will
38    be relative to that instead being absolute.
39
40    If the pen has an "arcTo" method, it is called with the original values
41    of the elliptical arc curve commands:
42
43        pen.arcTo(rx, ry, rotation, arc_large, arc_sweep, (x, y))
44
45    Otherwise, the arcs are approximated by series of cubic Bezier segments
46    ("curveTo"), one every 90 degrees.
47    """
48    # In the SVG specs, initial movetos are absolute, even if
49    # specified as 'm'. This is the default behavior here as well.
50    # But if you pass in a current_pos variable, the initial moveto
51    # will be relative to that current_pos. This is useful.
52    current_pos = complex(*current_pos)
53
54    elements = list(_tokenize_path(pathdef))
55    # Reverse for easy use of .pop()
56    elements.reverse()
57
58    start_pos = None
59    command = None
60    last_control = None
61
62    have_arcTo = hasattr(pen, "arcTo")
63
64    while elements:
65
66        if elements[-1] in COMMANDS:
67            # New command.
68            last_command = command  # Used by S and T
69            command = elements.pop()
70            absolute = command in UPPERCASE
71            command = command.upper()
72        else:
73            # If this element starts with numbers, it is an implicit command
74            # and we don't change the command. Check that it's allowed:
75            if command is None:
76                raise ValueError("Unallowed implicit command in %s, position %s" % (
77                    pathdef, len(pathdef.split()) - len(elements)))
78            last_command = command  # Used by S and T
79
80        if command == 'M':
81            # Moveto command.
82            x = elements.pop()
83            y = elements.pop()
84            pos = float(x) + float(y) * 1j
85            if absolute:
86                current_pos = pos
87            else:
88                current_pos += pos
89
90            # M is not preceded by Z; it's an open subpath
91            if start_pos is not None:
92                pen.endPath()
93
94            pen.moveTo((current_pos.real, current_pos.imag))
95
96            # when M is called, reset start_pos
97            # This behavior of Z is defined in svg spec:
98            # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
99            start_pos = current_pos
100
101            # Implicit moveto commands are treated as lineto commands.
102            # So we set command to lineto here, in case there are
103            # further implicit commands after this moveto.
104            command = 'L'
105
106        elif command == 'Z':
107            # Close path
108            if current_pos != start_pos:
109                pen.lineTo((start_pos.real, start_pos.imag))
110            pen.closePath()
111            current_pos = start_pos
112            start_pos = None
113            command = None  # You can't have implicit commands after closing.
114
115        elif command == 'L':
116            x = elements.pop()
117            y = elements.pop()
118            pos = float(x) + float(y) * 1j
119            if not absolute:
120                pos += current_pos
121            pen.lineTo((pos.real, pos.imag))
122            current_pos = pos
123
124        elif command == 'H':
125            x = elements.pop()
126            pos = float(x) + current_pos.imag * 1j
127            if not absolute:
128                pos += current_pos.real
129            pen.lineTo((pos.real, pos.imag))
130            current_pos = pos
131
132        elif command == 'V':
133            y = elements.pop()
134            pos = current_pos.real + float(y) * 1j
135            if not absolute:
136                pos += current_pos.imag * 1j
137            pen.lineTo((pos.real, pos.imag))
138            current_pos = pos
139
140        elif command == 'C':
141            control1 = float(elements.pop()) + float(elements.pop()) * 1j
142            control2 = float(elements.pop()) + float(elements.pop()) * 1j
143            end = float(elements.pop()) + float(elements.pop()) * 1j
144
145            if not absolute:
146                control1 += current_pos
147                control2 += current_pos
148                end += current_pos
149
150            pen.curveTo((control1.real, control1.imag),
151                        (control2.real, control2.imag),
152                        (end.real, end.imag))
153            current_pos = end
154            last_control = control2
155
156        elif command == 'S':
157            # Smooth curve. First control point is the "reflection" of
158            # the second control point in the previous path.
159
160            if last_command not in 'CS':
161                # If there is no previous command or if the previous command
162                # was not an C, c, S or s, assume the first control point is
163                # coincident with the current point.
164                control1 = current_pos
165            else:
166                # The first control point is assumed to be the reflection of
167                # the second control point on the previous command relative
168                # to the current point.
169                control1 = current_pos + current_pos - last_control
170
171            control2 = float(elements.pop()) + float(elements.pop()) * 1j
172            end = float(elements.pop()) + float(elements.pop()) * 1j
173
174            if not absolute:
175                control2 += current_pos
176                end += current_pos
177
178            pen.curveTo((control1.real, control1.imag),
179                        (control2.real, control2.imag),
180                        (end.real, end.imag))
181            current_pos = end
182            last_control = control2
183
184        elif command == 'Q':
185            control = float(elements.pop()) + float(elements.pop()) * 1j
186            end = float(elements.pop()) + float(elements.pop()) * 1j
187
188            if not absolute:
189                control += current_pos
190                end += current_pos
191
192            pen.qCurveTo((control.real, control.imag), (end.real, end.imag))
193            current_pos = end
194            last_control = control
195
196        elif command == 'T':
197            # Smooth curve. Control point is the "reflection" of
198            # the second control point in the previous path.
199
200            if last_command not in 'QT':
201                # If there is no previous command or if the previous command
202                # was not an Q, q, T or t, assume the first control point is
203                # coincident with the current point.
204                control = current_pos
205            else:
206                # The control point is assumed to be the reflection of
207                # the control point on the previous command relative
208                # to the current point.
209                control = current_pos + current_pos - last_control
210
211            end = float(elements.pop()) + float(elements.pop()) * 1j
212
213            if not absolute:
214                end += current_pos
215
216            pen.qCurveTo((control.real, control.imag), (end.real, end.imag))
217            current_pos = end
218            last_control = control
219
220        elif command == 'A':
221            rx = float(elements.pop())
222            ry = float(elements.pop())
223            rotation = float(elements.pop())
224            arc_large = bool(int(elements.pop()))
225            arc_sweep = bool(int(elements.pop()))
226            end = float(elements.pop()) + float(elements.pop()) * 1j
227
228            if not absolute:
229                end += current_pos
230
231            # if the pen supports arcs, pass the values unchanged, otherwise
232            # approximate the arc with a series of cubic bezier curves
233            if have_arcTo:
234                pen.arcTo(
235                    rx,
236                    ry,
237                    rotation,
238                    arc_large,
239                    arc_sweep,
240                    (end.real, end.imag),
241                )
242            else:
243                arc = arc_class(
244                    current_pos, rx, ry, rotation, arc_large, arc_sweep, end
245                )
246                arc.draw(pen)
247
248            current_pos = end
249
250    # no final Z command, it's an open path
251    if start_pos is not None:
252        pen.endPath()
253