xref: /aosp_15_r20/external/fonttools/Lib/fontTools/svgLib/path/parser.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes# SVG Path specification parser.
2*e1fe3e4aSElliott Hughes# This is an adaptation from 'svg.path' by Lennart Regebro (@regebro),
3*e1fe3e4aSElliott Hughes# modified so that the parser takes a FontTools Pen object instead of
4*e1fe3e4aSElliott Hughes# returning a list of svg.path Path objects.
5*e1fe3e4aSElliott Hughes# The original code can be found at:
6*e1fe3e4aSElliott Hughes# https://github.com/regebro/svg.path/blob/4f9b6e3/src/svg/path/parser.py
7*e1fe3e4aSElliott Hughes# Copyright (c) 2013-2014 Lennart Regebro
8*e1fe3e4aSElliott Hughes# License: MIT
9*e1fe3e4aSElliott Hughes
10*e1fe3e4aSElliott Hughesfrom .arc import EllipticalArc
11*e1fe3e4aSElliott Hughesimport re
12*e1fe3e4aSElliott Hughes
13*e1fe3e4aSElliott Hughes
14*e1fe3e4aSElliott HughesCOMMANDS = set("MmZzLlHhVvCcSsQqTtAa")
15*e1fe3e4aSElliott HughesARC_COMMANDS = set("Aa")
16*e1fe3e4aSElliott HughesUPPERCASE = set("MZLHVCSQTA")
17*e1fe3e4aSElliott Hughes
18*e1fe3e4aSElliott HughesCOMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
19*e1fe3e4aSElliott Hughes
20*e1fe3e4aSElliott Hughes# https://www.w3.org/TR/css-syntax-3/#number-token-diagram
21*e1fe3e4aSElliott Hughes#   but -6.e-5 will be tokenized as "-6" then "-5" and confuse parsing
22*e1fe3e4aSElliott HughesFLOAT_RE = re.compile(
23*e1fe3e4aSElliott Hughes    r"[-+]?"  # optional sign
24*e1fe3e4aSElliott Hughes    r"(?:"
25*e1fe3e4aSElliott Hughes    r"(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?"  # int/float
26*e1fe3e4aSElliott Hughes    r"|"
27*e1fe3e4aSElliott Hughes    r"(?:\.[0-9]+(?:[eE][-+]?[0-9]+)?)"  # float with leading dot (e.g. '.42')
28*e1fe3e4aSElliott Hughes    r")"
29*e1fe3e4aSElliott Hughes)
30*e1fe3e4aSElliott HughesBOOL_RE = re.compile("^[01]")
31*e1fe3e4aSElliott HughesSEPARATOR_RE = re.compile(f"[, \t]")
32*e1fe3e4aSElliott Hughes
33*e1fe3e4aSElliott Hughes
34*e1fe3e4aSElliott Hughesdef _tokenize_path(pathdef):
35*e1fe3e4aSElliott Hughes    arc_cmd = None
36*e1fe3e4aSElliott Hughes    for x in COMMAND_RE.split(pathdef):
37*e1fe3e4aSElliott Hughes        if x in COMMANDS:
38*e1fe3e4aSElliott Hughes            arc_cmd = x if x in ARC_COMMANDS else None
39*e1fe3e4aSElliott Hughes            yield x
40*e1fe3e4aSElliott Hughes            continue
41*e1fe3e4aSElliott Hughes
42*e1fe3e4aSElliott Hughes        if arc_cmd:
43*e1fe3e4aSElliott Hughes            try:
44*e1fe3e4aSElliott Hughes                yield from _tokenize_arc_arguments(x)
45*e1fe3e4aSElliott Hughes            except ValueError as e:
46*e1fe3e4aSElliott Hughes                raise ValueError(f"Invalid arc command: '{arc_cmd}{x}'") from e
47*e1fe3e4aSElliott Hughes        else:
48*e1fe3e4aSElliott Hughes            for token in FLOAT_RE.findall(x):
49*e1fe3e4aSElliott Hughes                yield token
50*e1fe3e4aSElliott Hughes
51*e1fe3e4aSElliott Hughes
52*e1fe3e4aSElliott HughesARC_ARGUMENT_TYPES = (
53*e1fe3e4aSElliott Hughes    ("rx", FLOAT_RE),
54*e1fe3e4aSElliott Hughes    ("ry", FLOAT_RE),
55*e1fe3e4aSElliott Hughes    ("x-axis-rotation", FLOAT_RE),
56*e1fe3e4aSElliott Hughes    ("large-arc-flag", BOOL_RE),
57*e1fe3e4aSElliott Hughes    ("sweep-flag", BOOL_RE),
58*e1fe3e4aSElliott Hughes    ("x", FLOAT_RE),
59*e1fe3e4aSElliott Hughes    ("y", FLOAT_RE),
60*e1fe3e4aSElliott Hughes)
61*e1fe3e4aSElliott Hughes
62*e1fe3e4aSElliott Hughes
63*e1fe3e4aSElliott Hughesdef _tokenize_arc_arguments(arcdef):
64*e1fe3e4aSElliott Hughes    raw_args = [s for s in SEPARATOR_RE.split(arcdef) if s]
65*e1fe3e4aSElliott Hughes    if not raw_args:
66*e1fe3e4aSElliott Hughes        raise ValueError(f"Not enough arguments: '{arcdef}'")
67*e1fe3e4aSElliott Hughes    raw_args.reverse()
68*e1fe3e4aSElliott Hughes
69*e1fe3e4aSElliott Hughes    i = 0
70*e1fe3e4aSElliott Hughes    while raw_args:
71*e1fe3e4aSElliott Hughes        arg = raw_args.pop()
72*e1fe3e4aSElliott Hughes
73*e1fe3e4aSElliott Hughes        name, pattern = ARC_ARGUMENT_TYPES[i]
74*e1fe3e4aSElliott Hughes        match = pattern.search(arg)
75*e1fe3e4aSElliott Hughes        if not match:
76*e1fe3e4aSElliott Hughes            raise ValueError(f"Invalid argument for '{name}' parameter: {arg!r}")
77*e1fe3e4aSElliott Hughes
78*e1fe3e4aSElliott Hughes        j, k = match.span()
79*e1fe3e4aSElliott Hughes        yield arg[j:k]
80*e1fe3e4aSElliott Hughes        arg = arg[k:]
81*e1fe3e4aSElliott Hughes
82*e1fe3e4aSElliott Hughes        if arg:
83*e1fe3e4aSElliott Hughes            raw_args.append(arg)
84*e1fe3e4aSElliott Hughes
85*e1fe3e4aSElliott Hughes        # wrap around every 7 consecutive arguments
86*e1fe3e4aSElliott Hughes        if i == 6:
87*e1fe3e4aSElliott Hughes            i = 0
88*e1fe3e4aSElliott Hughes        else:
89*e1fe3e4aSElliott Hughes            i += 1
90*e1fe3e4aSElliott Hughes
91*e1fe3e4aSElliott Hughes    if i != 0:
92*e1fe3e4aSElliott Hughes        raise ValueError(f"Not enough arguments: '{arcdef}'")
93*e1fe3e4aSElliott Hughes
94*e1fe3e4aSElliott Hughes
95*e1fe3e4aSElliott Hughesdef parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc):
96*e1fe3e4aSElliott Hughes    """Parse SVG path definition (i.e. "d" attribute of <path> elements)
97*e1fe3e4aSElliott Hughes    and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath
98*e1fe3e4aSElliott Hughes    methods.
99*e1fe3e4aSElliott Hughes
100*e1fe3e4aSElliott Hughes    If 'current_pos' (2-float tuple) is provided, the initial moveTo will
101*e1fe3e4aSElliott Hughes    be relative to that instead being absolute.
102*e1fe3e4aSElliott Hughes
103*e1fe3e4aSElliott Hughes    If the pen has an "arcTo" method, it is called with the original values
104*e1fe3e4aSElliott Hughes    of the elliptical arc curve commands:
105*e1fe3e4aSElliott Hughes
106*e1fe3e4aSElliott Hughes        pen.arcTo(rx, ry, rotation, arc_large, arc_sweep, (x, y))
107*e1fe3e4aSElliott Hughes
108*e1fe3e4aSElliott Hughes    Otherwise, the arcs are approximated by series of cubic Bezier segments
109*e1fe3e4aSElliott Hughes    ("curveTo"), one every 90 degrees.
110*e1fe3e4aSElliott Hughes    """
111*e1fe3e4aSElliott Hughes    # In the SVG specs, initial movetos are absolute, even if
112*e1fe3e4aSElliott Hughes    # specified as 'm'. This is the default behavior here as well.
113*e1fe3e4aSElliott Hughes    # But if you pass in a current_pos variable, the initial moveto
114*e1fe3e4aSElliott Hughes    # will be relative to that current_pos. This is useful.
115*e1fe3e4aSElliott Hughes    current_pos = complex(*current_pos)
116*e1fe3e4aSElliott Hughes
117*e1fe3e4aSElliott Hughes    elements = list(_tokenize_path(pathdef))
118*e1fe3e4aSElliott Hughes    # Reverse for easy use of .pop()
119*e1fe3e4aSElliott Hughes    elements.reverse()
120*e1fe3e4aSElliott Hughes
121*e1fe3e4aSElliott Hughes    start_pos = None
122*e1fe3e4aSElliott Hughes    command = None
123*e1fe3e4aSElliott Hughes    last_control = None
124*e1fe3e4aSElliott Hughes
125*e1fe3e4aSElliott Hughes    have_arcTo = hasattr(pen, "arcTo")
126*e1fe3e4aSElliott Hughes
127*e1fe3e4aSElliott Hughes    while elements:
128*e1fe3e4aSElliott Hughes        if elements[-1] in COMMANDS:
129*e1fe3e4aSElliott Hughes            # New command.
130*e1fe3e4aSElliott Hughes            last_command = command  # Used by S and T
131*e1fe3e4aSElliott Hughes            command = elements.pop()
132*e1fe3e4aSElliott Hughes            absolute = command in UPPERCASE
133*e1fe3e4aSElliott Hughes            command = command.upper()
134*e1fe3e4aSElliott Hughes        else:
135*e1fe3e4aSElliott Hughes            # If this element starts with numbers, it is an implicit command
136*e1fe3e4aSElliott Hughes            # and we don't change the command. Check that it's allowed:
137*e1fe3e4aSElliott Hughes            if command is None:
138*e1fe3e4aSElliott Hughes                raise ValueError(
139*e1fe3e4aSElliott Hughes                    "Unallowed implicit command in %s, position %s"
140*e1fe3e4aSElliott Hughes                    % (pathdef, len(pathdef.split()) - len(elements))
141*e1fe3e4aSElliott Hughes                )
142*e1fe3e4aSElliott Hughes            last_command = command  # Used by S and T
143*e1fe3e4aSElliott Hughes
144*e1fe3e4aSElliott Hughes        if command == "M":
145*e1fe3e4aSElliott Hughes            # Moveto command.
146*e1fe3e4aSElliott Hughes            x = elements.pop()
147*e1fe3e4aSElliott Hughes            y = elements.pop()
148*e1fe3e4aSElliott Hughes            pos = float(x) + float(y) * 1j
149*e1fe3e4aSElliott Hughes            if absolute:
150*e1fe3e4aSElliott Hughes                current_pos = pos
151*e1fe3e4aSElliott Hughes            else:
152*e1fe3e4aSElliott Hughes                current_pos += pos
153*e1fe3e4aSElliott Hughes
154*e1fe3e4aSElliott Hughes            # M is not preceded by Z; it's an open subpath
155*e1fe3e4aSElliott Hughes            if start_pos is not None:
156*e1fe3e4aSElliott Hughes                pen.endPath()
157*e1fe3e4aSElliott Hughes
158*e1fe3e4aSElliott Hughes            pen.moveTo((current_pos.real, current_pos.imag))
159*e1fe3e4aSElliott Hughes
160*e1fe3e4aSElliott Hughes            # when M is called, reset start_pos
161*e1fe3e4aSElliott Hughes            # This behavior of Z is defined in svg spec:
162*e1fe3e4aSElliott Hughes            # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
163*e1fe3e4aSElliott Hughes            start_pos = current_pos
164*e1fe3e4aSElliott Hughes
165*e1fe3e4aSElliott Hughes            # Implicit moveto commands are treated as lineto commands.
166*e1fe3e4aSElliott Hughes            # So we set command to lineto here, in case there are
167*e1fe3e4aSElliott Hughes            # further implicit commands after this moveto.
168*e1fe3e4aSElliott Hughes            command = "L"
169*e1fe3e4aSElliott Hughes
170*e1fe3e4aSElliott Hughes        elif command == "Z":
171*e1fe3e4aSElliott Hughes            # Close path
172*e1fe3e4aSElliott Hughes            if current_pos != start_pos:
173*e1fe3e4aSElliott Hughes                pen.lineTo((start_pos.real, start_pos.imag))
174*e1fe3e4aSElliott Hughes            pen.closePath()
175*e1fe3e4aSElliott Hughes            current_pos = start_pos
176*e1fe3e4aSElliott Hughes            start_pos = None
177*e1fe3e4aSElliott Hughes            command = None  # You can't have implicit commands after closing.
178*e1fe3e4aSElliott Hughes
179*e1fe3e4aSElliott Hughes        elif command == "L":
180*e1fe3e4aSElliott Hughes            x = elements.pop()
181*e1fe3e4aSElliott Hughes            y = elements.pop()
182*e1fe3e4aSElliott Hughes            pos = float(x) + float(y) * 1j
183*e1fe3e4aSElliott Hughes            if not absolute:
184*e1fe3e4aSElliott Hughes                pos += current_pos
185*e1fe3e4aSElliott Hughes            pen.lineTo((pos.real, pos.imag))
186*e1fe3e4aSElliott Hughes            current_pos = pos
187*e1fe3e4aSElliott Hughes
188*e1fe3e4aSElliott Hughes        elif command == "H":
189*e1fe3e4aSElliott Hughes            x = elements.pop()
190*e1fe3e4aSElliott Hughes            pos = float(x) + current_pos.imag * 1j
191*e1fe3e4aSElliott Hughes            if not absolute:
192*e1fe3e4aSElliott Hughes                pos += current_pos.real
193*e1fe3e4aSElliott Hughes            pen.lineTo((pos.real, pos.imag))
194*e1fe3e4aSElliott Hughes            current_pos = pos
195*e1fe3e4aSElliott Hughes
196*e1fe3e4aSElliott Hughes        elif command == "V":
197*e1fe3e4aSElliott Hughes            y = elements.pop()
198*e1fe3e4aSElliott Hughes            pos = current_pos.real + float(y) * 1j
199*e1fe3e4aSElliott Hughes            if not absolute:
200*e1fe3e4aSElliott Hughes                pos += current_pos.imag * 1j
201*e1fe3e4aSElliott Hughes            pen.lineTo((pos.real, pos.imag))
202*e1fe3e4aSElliott Hughes            current_pos = pos
203*e1fe3e4aSElliott Hughes
204*e1fe3e4aSElliott Hughes        elif command == "C":
205*e1fe3e4aSElliott Hughes            control1 = float(elements.pop()) + float(elements.pop()) * 1j
206*e1fe3e4aSElliott Hughes            control2 = float(elements.pop()) + float(elements.pop()) * 1j
207*e1fe3e4aSElliott Hughes            end = float(elements.pop()) + float(elements.pop()) * 1j
208*e1fe3e4aSElliott Hughes
209*e1fe3e4aSElliott Hughes            if not absolute:
210*e1fe3e4aSElliott Hughes                control1 += current_pos
211*e1fe3e4aSElliott Hughes                control2 += current_pos
212*e1fe3e4aSElliott Hughes                end += current_pos
213*e1fe3e4aSElliott Hughes
214*e1fe3e4aSElliott Hughes            pen.curveTo(
215*e1fe3e4aSElliott Hughes                (control1.real, control1.imag),
216*e1fe3e4aSElliott Hughes                (control2.real, control2.imag),
217*e1fe3e4aSElliott Hughes                (end.real, end.imag),
218*e1fe3e4aSElliott Hughes            )
219*e1fe3e4aSElliott Hughes            current_pos = end
220*e1fe3e4aSElliott Hughes            last_control = control2
221*e1fe3e4aSElliott Hughes
222*e1fe3e4aSElliott Hughes        elif command == "S":
223*e1fe3e4aSElliott Hughes            # Smooth curve. First control point is the "reflection" of
224*e1fe3e4aSElliott Hughes            # the second control point in the previous path.
225*e1fe3e4aSElliott Hughes
226*e1fe3e4aSElliott Hughes            if last_command not in "CS":
227*e1fe3e4aSElliott Hughes                # If there is no previous command or if the previous command
228*e1fe3e4aSElliott Hughes                # was not an C, c, S or s, assume the first control point is
229*e1fe3e4aSElliott Hughes                # coincident with the current point.
230*e1fe3e4aSElliott Hughes                control1 = current_pos
231*e1fe3e4aSElliott Hughes            else:
232*e1fe3e4aSElliott Hughes                # The first control point is assumed to be the reflection of
233*e1fe3e4aSElliott Hughes                # the second control point on the previous command relative
234*e1fe3e4aSElliott Hughes                # to the current point.
235*e1fe3e4aSElliott Hughes                control1 = current_pos + current_pos - last_control
236*e1fe3e4aSElliott Hughes
237*e1fe3e4aSElliott Hughes            control2 = float(elements.pop()) + float(elements.pop()) * 1j
238*e1fe3e4aSElliott Hughes            end = float(elements.pop()) + float(elements.pop()) * 1j
239*e1fe3e4aSElliott Hughes
240*e1fe3e4aSElliott Hughes            if not absolute:
241*e1fe3e4aSElliott Hughes                control2 += current_pos
242*e1fe3e4aSElliott Hughes                end += current_pos
243*e1fe3e4aSElliott Hughes
244*e1fe3e4aSElliott Hughes            pen.curveTo(
245*e1fe3e4aSElliott Hughes                (control1.real, control1.imag),
246*e1fe3e4aSElliott Hughes                (control2.real, control2.imag),
247*e1fe3e4aSElliott Hughes                (end.real, end.imag),
248*e1fe3e4aSElliott Hughes            )
249*e1fe3e4aSElliott Hughes            current_pos = end
250*e1fe3e4aSElliott Hughes            last_control = control2
251*e1fe3e4aSElliott Hughes
252*e1fe3e4aSElliott Hughes        elif command == "Q":
253*e1fe3e4aSElliott Hughes            control = float(elements.pop()) + float(elements.pop()) * 1j
254*e1fe3e4aSElliott Hughes            end = float(elements.pop()) + float(elements.pop()) * 1j
255*e1fe3e4aSElliott Hughes
256*e1fe3e4aSElliott Hughes            if not absolute:
257*e1fe3e4aSElliott Hughes                control += current_pos
258*e1fe3e4aSElliott Hughes                end += current_pos
259*e1fe3e4aSElliott Hughes
260*e1fe3e4aSElliott Hughes            pen.qCurveTo((control.real, control.imag), (end.real, end.imag))
261*e1fe3e4aSElliott Hughes            current_pos = end
262*e1fe3e4aSElliott Hughes            last_control = control
263*e1fe3e4aSElliott Hughes
264*e1fe3e4aSElliott Hughes        elif command == "T":
265*e1fe3e4aSElliott Hughes            # Smooth curve. Control point is the "reflection" of
266*e1fe3e4aSElliott Hughes            # the second control point in the previous path.
267*e1fe3e4aSElliott Hughes
268*e1fe3e4aSElliott Hughes            if last_command not in "QT":
269*e1fe3e4aSElliott Hughes                # If there is no previous command or if the previous command
270*e1fe3e4aSElliott Hughes                # was not an Q, q, T or t, assume the first control point is
271*e1fe3e4aSElliott Hughes                # coincident with the current point.
272*e1fe3e4aSElliott Hughes                control = current_pos
273*e1fe3e4aSElliott Hughes            else:
274*e1fe3e4aSElliott Hughes                # The control point is assumed to be the reflection of
275*e1fe3e4aSElliott Hughes                # the control point on the previous command relative
276*e1fe3e4aSElliott Hughes                # to the current point.
277*e1fe3e4aSElliott Hughes                control = current_pos + current_pos - last_control
278*e1fe3e4aSElliott Hughes
279*e1fe3e4aSElliott Hughes            end = float(elements.pop()) + float(elements.pop()) * 1j
280*e1fe3e4aSElliott Hughes
281*e1fe3e4aSElliott Hughes            if not absolute:
282*e1fe3e4aSElliott Hughes                end += current_pos
283*e1fe3e4aSElliott Hughes
284*e1fe3e4aSElliott Hughes            pen.qCurveTo((control.real, control.imag), (end.real, end.imag))
285*e1fe3e4aSElliott Hughes            current_pos = end
286*e1fe3e4aSElliott Hughes            last_control = control
287*e1fe3e4aSElliott Hughes
288*e1fe3e4aSElliott Hughes        elif command == "A":
289*e1fe3e4aSElliott Hughes            rx = abs(float(elements.pop()))
290*e1fe3e4aSElliott Hughes            ry = abs(float(elements.pop()))
291*e1fe3e4aSElliott Hughes            rotation = float(elements.pop())
292*e1fe3e4aSElliott Hughes            arc_large = bool(int(elements.pop()))
293*e1fe3e4aSElliott Hughes            arc_sweep = bool(int(elements.pop()))
294*e1fe3e4aSElliott Hughes            end = float(elements.pop()) + float(elements.pop()) * 1j
295*e1fe3e4aSElliott Hughes
296*e1fe3e4aSElliott Hughes            if not absolute:
297*e1fe3e4aSElliott Hughes                end += current_pos
298*e1fe3e4aSElliott Hughes
299*e1fe3e4aSElliott Hughes            # if the pen supports arcs, pass the values unchanged, otherwise
300*e1fe3e4aSElliott Hughes            # approximate the arc with a series of cubic bezier curves
301*e1fe3e4aSElliott Hughes            if have_arcTo:
302*e1fe3e4aSElliott Hughes                pen.arcTo(
303*e1fe3e4aSElliott Hughes                    rx,
304*e1fe3e4aSElliott Hughes                    ry,
305*e1fe3e4aSElliott Hughes                    rotation,
306*e1fe3e4aSElliott Hughes                    arc_large,
307*e1fe3e4aSElliott Hughes                    arc_sweep,
308*e1fe3e4aSElliott Hughes                    (end.real, end.imag),
309*e1fe3e4aSElliott Hughes                )
310*e1fe3e4aSElliott Hughes            else:
311*e1fe3e4aSElliott Hughes                arc = arc_class(
312*e1fe3e4aSElliott Hughes                    current_pos, rx, ry, rotation, arc_large, arc_sweep, end
313*e1fe3e4aSElliott Hughes                )
314*e1fe3e4aSElliott Hughes                arc.draw(pen)
315*e1fe3e4aSElliott Hughes
316*e1fe3e4aSElliott Hughes            current_pos = end
317*e1fe3e4aSElliott Hughes
318*e1fe3e4aSElliott Hughes    # no final Z command, it's an open path
319*e1fe3e4aSElliott Hughes    if start_pos is not None:
320*e1fe3e4aSElliott Hughes        pen.endPath()
321