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