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