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