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