xref: /aosp_15_r20/external/fonttools/Tests/svgLib/path/parser_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.pens.recordingPen import RecordingPen
2from fontTools.svgLib import parse_path
3
4import pytest
5
6
7@pytest.mark.parametrize(
8    "pathdef, expected",
9    [
10        # Examples from the SVG spec
11        (
12            "M 100 100 L 300 100 L 200 300 z",
13            [
14                ("moveTo", ((100.0, 100.0),)),
15                ("lineTo", ((300.0, 100.0),)),
16                ("lineTo", ((200.0, 300.0),)),
17                ("lineTo", ((100.0, 100.0),)),
18                ("closePath", ()),
19            ],
20        ),
21        # for Z command behavior when there is multiple subpaths
22        (
23            "M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z",
24            [
25                ("moveTo", ((0.0, 0.0),)),
26                ("lineTo", ((50.0, 20.0),)),
27                ("endPath", ()),
28                ("moveTo", ((100.0, 100.0),)),
29                ("lineTo", ((300.0, 100.0),)),
30                ("lineTo", ((200.0, 300.0),)),
31                ("lineTo", ((100.0, 100.0),)),
32                ("closePath", ()),
33            ],
34        ),
35        (
36            "M100,200 C100,100 250,100 250,200 S400,300 400,200",
37            [
38                ("moveTo", ((100.0, 200.0),)),
39                ("curveTo", ((100.0, 100.0), (250.0, 100.0), (250.0, 200.0))),
40                ("curveTo", ((250.0, 300.0), (400.0, 300.0), (400.0, 200.0))),
41                ("endPath", ()),
42            ],
43        ),
44        (
45            "M100,200 C100,100 400,100 400,200",
46            [
47                ("moveTo", ((100.0, 200.0),)),
48                ("curveTo", ((100.0, 100.0), (400.0, 100.0), (400.0, 200.0))),
49                ("endPath", ()),
50            ],
51        ),
52        (
53            "M100,500 C25,400 475,400 400,500",
54            [
55                ("moveTo", ((100.0, 500.0),)),
56                ("curveTo", ((25.0, 400.0), (475.0, 400.0), (400.0, 500.0))),
57                ("endPath", ()),
58            ],
59        ),
60        (
61            "M100,800 C175,700 325,700 400,800",
62            [
63                ("moveTo", ((100.0, 800.0),)),
64                ("curveTo", ((175.0, 700.0), (325.0, 700.0), (400.0, 800.0))),
65                ("endPath", ()),
66            ],
67        ),
68        (
69            "M600,200 C675,100 975,100 900,200",
70            [
71                ("moveTo", ((600.0, 200.0),)),
72                ("curveTo", ((675.0, 100.0), (975.0, 100.0), (900.0, 200.0))),
73                ("endPath", ()),
74            ],
75        ),
76        (
77            "M600,500 C600,350 900,650 900,500",
78            [
79                ("moveTo", ((600.0, 500.0),)),
80                ("curveTo", ((600.0, 350.0), (900.0, 650.0), (900.0, 500.0))),
81                ("endPath", ()),
82            ],
83        ),
84        (
85            "M600,800 C625,700 725,700 750,800 S875,900 900,800",
86            [
87                ("moveTo", ((600.0, 800.0),)),
88                ("curveTo", ((625.0, 700.0), (725.0, 700.0), (750.0, 800.0))),
89                ("curveTo", ((775.0, 900.0), (875.0, 900.0), (900.0, 800.0))),
90                ("endPath", ()),
91            ],
92        ),
93        (
94            "M200,300 Q400,50 600,300 T1000,300",
95            [
96                ("moveTo", ((200.0, 300.0),)),
97                ("qCurveTo", ((400.0, 50.0), (600.0, 300.0))),
98                ("qCurveTo", ((800.0, 550.0), (1000.0, 300.0))),
99                ("endPath", ()),
100            ],
101        ),
102        # End examples from SVG spec
103        # Relative moveto
104        (
105            "M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z",
106            [
107                ("moveTo", ((0.0, 0.0),)),
108                ("lineTo", ((50.0, 20.0),)),
109                ("endPath", ()),
110                ("moveTo", ((100.0, 100.0),)),
111                ("lineTo", ((300.0, 100.0),)),
112                ("lineTo", ((200.0, 300.0),)),
113                ("lineTo", ((100.0, 100.0),)),
114                ("closePath", ()),
115            ],
116        ),
117        # Initial smooth and relative curveTo
118        (
119            "M100,200 s 150,-100 150,0",
120            [
121                ("moveTo", ((100.0, 200.0),)),
122                ("curveTo", ((100.0, 200.0), (250.0, 100.0), (250.0, 200.0))),
123                ("endPath", ()),
124            ],
125        ),
126        # Initial smooth and relative qCurveTo
127        (
128            "M100,200 t 150,0",
129            [
130                ("moveTo", ((100.0, 200.0),)),
131                ("qCurveTo", ((100.0, 200.0), (250.0, 200.0))),
132                ("endPath", ()),
133            ],
134        ),
135        # relative l command
136        (
137            "M 100 100 L 300 100 l -100 200 z",
138            [
139                ("moveTo", ((100.0, 100.0),)),
140                ("lineTo", ((300.0, 100.0),)),
141                ("lineTo", ((200.0, 300.0),)),
142                ("lineTo", ((100.0, 100.0),)),
143                ("closePath", ()),
144            ],
145        ),
146        # relative q command
147        (
148            "M200,300 q200,-250 400,0",
149            [
150                ("moveTo", ((200.0, 300.0),)),
151                ("qCurveTo", ((400.0, 50.0), (600.0, 300.0))),
152                ("endPath", ()),
153            ],
154        ),
155        # absolute H command
156        (
157            "M 100 100 H 300 L 200 300 z",
158            [
159                ("moveTo", ((100.0, 100.0),)),
160                ("lineTo", ((300.0, 100.0),)),
161                ("lineTo", ((200.0, 300.0),)),
162                ("lineTo", ((100.0, 100.0),)),
163                ("closePath", ()),
164            ],
165        ),
166        # relative h command
167        (
168            "M 100 100 h 200 L 200 300 z",
169            [
170                ("moveTo", ((100.0, 100.0),)),
171                ("lineTo", ((300.0, 100.0),)),
172                ("lineTo", ((200.0, 300.0),)),
173                ("lineTo", ((100.0, 100.0),)),
174                ("closePath", ()),
175            ],
176        ),
177        # absolute V command
178        (
179            "M 100 100 V 300 L 200 300 z",
180            [
181                ("moveTo", ((100.0, 100.0),)),
182                ("lineTo", ((100.0, 300.0),)),
183                ("lineTo", ((200.0, 300.0),)),
184                ("lineTo", ((100.0, 100.0),)),
185                ("closePath", ()),
186            ],
187        ),
188        # relative v command
189        (
190            "M 100 100 v 200 L 200 300 z",
191            [
192                ("moveTo", ((100.0, 100.0),)),
193                ("lineTo", ((100.0, 300.0),)),
194                ("lineTo", ((200.0, 300.0),)),
195                ("lineTo", ((100.0, 100.0),)),
196                ("closePath", ()),
197            ],
198        ),
199    ],
200)
201def test_parse_path(pathdef, expected):
202    pen = RecordingPen()
203    parse_path(pathdef, pen)
204
205    assert pen.value == expected
206
207
208@pytest.mark.parametrize(
209    "pathdef1, pathdef2",
210    [
211        # don't need spaces between numbers and commands
212        (
213            "M 100 100 L 200 200",
214            "M100 100L200 200",
215        ),
216        # repeated implicit command
217        ("M 100 200 L 200 100 L -100 -200", "M 100 200 L 200 100 -100 -200"),
218        # don't need spaces before a minus-sign
219        ("M100,200c10-5,20-10,30-20", "M 100 200 c 10 -5 20 -10 30 -20"),
220        # closed paths have an implicit lineTo if they don't
221        # end on the same point as the initial moveTo
222        (
223            "M 100 100 L 300 100 L 200 300 z",
224            "M 100 100 L 300 100 L 200 300 L 100 100 z",
225        ),
226    ],
227)
228def test_equivalent_paths(pathdef1, pathdef2):
229    pen1 = RecordingPen()
230    parse_path(pathdef1, pen1)
231
232    pen2 = RecordingPen()
233    parse_path(pathdef2, pen2)
234
235    assert pen1.value == pen2.value
236
237
238def test_exponents():
239    # It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported.
240    pen = RecordingPen()
241    parse_path("M-3.4e38 3.4E+38L-3.4E-38,3.4e-38", pen)
242    expected = [
243        ("moveTo", ((-3.4e38, 3.4e38),)),
244        ("lineTo", ((-3.4e-38, 3.4e-38),)),
245        ("endPath", ()),
246    ]
247
248    assert pen.value == expected
249
250    pen = RecordingPen()
251    parse_path("M-3e38 3E+38L-3E-38,3e-38", pen)
252    expected = [
253        ("moveTo", ((-3e38, 3e38),)),
254        ("lineTo", ((-3e-38, 3e-38),)),
255        ("endPath", ()),
256    ]
257
258    assert pen.value == expected
259
260
261def test_invalid_implicit_command():
262    with pytest.raises(ValueError) as exc_info:
263        parse_path("M 100 100 L 200 200 Z 100 200", RecordingPen())
264    assert exc_info.match("Unallowed implicit command")
265
266
267def test_arc_to_cubic_bezier():
268    pen = RecordingPen()
269    parse_path("M300,200 h-150 a150,150 0 1,0 150,-150 z", pen)
270    expected = [
271        ("moveTo", ((300.0, 200.0),)),
272        ("lineTo", ((150.0, 200.0),)),
273        ("curveTo", ((150.0, 282.842), (217.157, 350.0), (300.0, 350.0))),
274        ("curveTo", ((382.842, 350.0), (450.0, 282.842), (450.0, 200.0))),
275        ("curveTo", ((450.0, 117.157), (382.842, 50.0), (300.0, 50.0))),
276        ("lineTo", ((300.0, 200.0),)),
277        ("closePath", ()),
278    ]
279
280    result = list(pen.value)
281    assert len(result) == len(expected)
282    for (cmd1, points1), (cmd2, points2) in zip(result, expected):
283        assert cmd1 == cmd2
284        assert len(points1) == len(points2)
285        for pt1, pt2 in zip(points1, points2):
286            assert pt1 == pytest.approx(pt2, rel=1e-5)
287
288
289class ArcRecordingPen(RecordingPen):
290    def arcTo(self, rx, ry, rotation, arc_large, arc_sweep, end_point):
291        self.value.append(
292            ("arcTo", (rx, ry, rotation, arc_large, arc_sweep, end_point))
293        )
294
295
296def test_arc_pen_with_arcTo():
297    pen = ArcRecordingPen()
298    parse_path("M300,200 h-150 a150,150 0 1,0 150,-150 z", pen)
299    expected = [
300        ("moveTo", ((300.0, 200.0),)),
301        ("lineTo", ((150.0, 200.0),)),
302        ("arcTo", (150.0, 150.0, 0.0, True, False, (300.0, 50.0))),
303        ("lineTo", ((300.0, 200.0),)),
304        ("closePath", ()),
305    ]
306
307    assert pen.value == expected
308
309
310@pytest.mark.parametrize(
311    "path, expected",
312    [
313        (
314            "M1-2A3-4-1.0 01.5.7",
315            [
316                ("moveTo", ((1.0, -2.0),)),
317                ("arcTo", (3.0, 4.0, -1.0, False, True, (0.5, 0.7))),
318                ("endPath", ()),
319            ],
320        ),
321        (
322            "M21.58 7.19a2.51 2.51 0 10-1.77-1.77",
323            [
324                ("moveTo", ((21.58, 7.19),)),
325                ("arcTo", (2.51, 2.51, 0.0, True, False, (19.81, 5.42))),
326                ("endPath", ()),
327            ],
328        ),
329        (
330            "M22 12a25.87 25.87 0 00-.42-4.81",
331            [
332                ("moveTo", ((22.0, 12.0),)),
333                ("arcTo", (25.87, 25.87, 0.0, False, False, (21.58, 7.19))),
334                ("endPath", ()),
335            ],
336        ),
337        (
338            "M0,0 A1.2 1.2 0 012 15.8",
339            [
340                ("moveTo", ((0.0, 0.0),)),
341                ("arcTo", (1.2, 1.2, 0.0, False, True, (2.0, 15.8))),
342                ("endPath", ()),
343            ],
344        ),
345        (
346            "M12 7a5 5 0 105 5 5 5 0 00-5-5",
347            [
348                ("moveTo", ((12.0, 7.0),)),
349                ("arcTo", (5.0, 5.0, 0.0, True, False, (17.0, 12.0))),
350                ("arcTo", (5.0, 5.0, 0.0, False, False, (12.0, 7.0))),
351                ("endPath", ()),
352            ],
353        ),
354    ],
355)
356def test_arc_flags_without_spaces(path, expected):
357    pen = ArcRecordingPen()
358    parse_path(path, pen)
359    assert pen.value == expected
360
361
362@pytest.mark.parametrize("path", ["A", "A0,0,0,0,0,0", "A 0 0 0 0 0 0 0 0 0 0 0 0 0"])
363def test_invalid_arc_not_enough_args(path):
364    pen = ArcRecordingPen()
365    with pytest.raises(ValueError, match="Invalid arc command") as e:
366        parse_path(path, pen)
367
368    assert isinstance(e.value.__cause__, ValueError)
369    assert "Not enough arguments" in str(e.value.__cause__)
370
371
372def test_invalid_arc_argument_value():
373    pen = ArcRecordingPen()
374    with pytest.raises(ValueError, match="Invalid arc command") as e:
375        parse_path("M0,0 A0,0,0,2,0,0,0", pen)
376
377    cause = e.value.__cause__
378    assert isinstance(cause, ValueError)
379    assert "Invalid argument for 'large-arc-flag' parameter: '2'" in str(cause)
380
381    pen = ArcRecordingPen()
382    with pytest.raises(ValueError, match="Invalid arc command") as e:
383        parse_path("M0,0 A0,0,0,0,-2.0,0,0", pen)
384
385    cause = e.value.__cause__
386    assert isinstance(cause, ValueError)
387    assert "Invalid argument for 'sweep-flag' parameter: '-2.0'" in str(cause)
388