1from fontTools.pens.recordingPen import RecordingPen 2from fontTools.pens.reverseContourPen import ReverseContourPen 3import pytest 4 5 6TEST_DATA = [ 7 ( 8 [ 9 ("moveTo", ((0, 0),)), 10 ("lineTo", ((1, 1),)), 11 ("lineTo", ((2, 2),)), 12 ("lineTo", ((3, 3),)), # last not on move, line is implied 13 ("closePath", ()), 14 ], 15 False, # outputImpliedClosingLine 16 [ 17 ("moveTo", ((0, 0),)), 18 ("lineTo", ((3, 3),)), 19 ("lineTo", ((2, 2),)), 20 ("lineTo", ((1, 1),)), 21 ("closePath", ()), 22 ], 23 ), 24 ( 25 [ 26 ("moveTo", ((0, 0),)), 27 ("lineTo", ((1, 1),)), 28 ("lineTo", ((2, 2),)), 29 ("lineTo", ((3, 3),)), # last line does not overlap move... 30 ("closePath", ()), 31 ], 32 True, # outputImpliedClosingLine 33 [ 34 ("moveTo", ((0, 0),)), 35 ("lineTo", ((3, 3),)), 36 ("lineTo", ((2, 2),)), 37 ("lineTo", ((1, 1),)), 38 ("lineTo", ((0, 0),)), # ... but closing line is NOT implied 39 ("closePath", ()), 40 ], 41 ), 42 ( 43 [ 44 ("moveTo", ((0, 0),)), 45 ("lineTo", ((1, 1),)), 46 ("lineTo", ((2, 2),)), 47 ("lineTo", ((0, 0),)), # last line overlaps move, explicit line 48 ("closePath", ()), 49 ], 50 False, 51 [ 52 ("moveTo", ((0, 0),)), 53 ("lineTo", ((2, 2),)), 54 ("lineTo", ((1, 1),)), 55 ("closePath", ()), # closing line implied 56 ], 57 ), 58 ( 59 [ 60 ("moveTo", ((0, 0),)), 61 ("lineTo", ((1, 1),)), 62 ("lineTo", ((2, 2),)), 63 ("lineTo", ((0, 0),)), # last line overlaps move... 64 ("closePath", ()), 65 ], 66 True, 67 [ 68 ("moveTo", ((0, 0),)), 69 ("lineTo", ((2, 2),)), 70 ("lineTo", ((1, 1),)), 71 ("lineTo", ((0, 0),)), # ... but line is NOT implied 72 ("closePath", ()), 73 ], 74 ), 75 ( 76 [ 77 ("moveTo", ((0, 0),)), 78 ("lineTo", ((0, 0),)), # duplicate lineTo following moveTo 79 ("lineTo", ((1, 1),)), 80 ("lineTo", ((2, 2),)), 81 ("closePath", ()), 82 ], 83 False, 84 [ 85 ("moveTo", ((0, 0),)), 86 ("lineTo", ((2, 2),)), 87 ("lineTo", ((1, 1),)), 88 ("lineTo", ((0, 0),)), # extra explicit lineTo is always emitted to 89 ("lineTo", ((0, 0),)), # disambiguate from an implicit closing line 90 ("closePath", ()), 91 ], 92 ), 93 ( 94 [ 95 ("moveTo", ((0, 0),)), 96 ("lineTo", ((0, 0),)), # duplicate lineTo following moveTo 97 ("lineTo", ((1, 1),)), 98 ("lineTo", ((2, 2),)), 99 ("closePath", ()), 100 ], 101 True, 102 [ 103 ("moveTo", ((0, 0),)), 104 ("lineTo", ((2, 2),)), 105 ("lineTo", ((1, 1),)), 106 ("lineTo", ((0, 0),)), # duplicate lineTo is retained also in this case, 107 ("lineTo", ((0, 0),)), # same result as with outputImpliedClosingLine=False 108 ("closePath", ()), 109 ], 110 ), 111 ( 112 [ 113 ("moveTo", ((0, 0),)), 114 ("lineTo", ((1, 1),)), 115 ("closePath", ()), 116 ], 117 False, 118 [ 119 ("moveTo", ((0, 0),)), 120 ("lineTo", ((1, 1),)), 121 ("closePath", ()), 122 ], 123 ), 124 ( 125 [ 126 ("moveTo", ((0, 0),)), 127 ("lineTo", ((1, 1),)), 128 ("closePath", ()), 129 ], 130 True, 131 [ 132 ("moveTo", ((0, 0),)), 133 ("lineTo", ((1, 1),)), 134 ("lineTo", ((0, 0),)), 135 ("closePath", ()), 136 ], 137 ), 138 ( 139 [ 140 ("moveTo", ((0, 0),)), 141 ("curveTo", ((1, 1), (2, 2), (3, 3))), 142 ("curveTo", ((4, 4), (5, 5), (0, 0))), # closed curveTo overlaps moveTo 143 ("closePath", ()), 144 ], 145 False, 146 [ 147 ("moveTo", ((0, 0),)), # no extra lineTo added here 148 ("curveTo", ((5, 5), (4, 4), (3, 3))), 149 ("curveTo", ((2, 2), (1, 1), (0, 0))), 150 ("closePath", ()), 151 ], 152 ), 153 ( 154 [ 155 ("moveTo", ((0, 0),)), 156 ("curveTo", ((1, 1), (2, 2), (3, 3))), 157 ("curveTo", ((4, 4), (5, 5), (0, 0))), # closed curveTo overlaps moveTo 158 ("closePath", ()), 159 ], 160 True, 161 [ 162 ("moveTo", ((0, 0),)), # no extra lineTo added here, same as preceding 163 ("curveTo", ((5, 5), (4, 4), (3, 3))), 164 ("curveTo", ((2, 2), (1, 1), (0, 0))), 165 ("closePath", ()), 166 ], 167 ), 168 ( 169 [ 170 ("moveTo", ((0, 0),)), 171 ("curveTo", ((1, 1), (2, 2), (3, 3))), 172 ("curveTo", ((4, 4), (5, 5), (6, 6))), # closed curve not overlapping move 173 ("closePath", ()), 174 ], 175 False, 176 [ 177 ("moveTo", ((0, 0),)), 178 ("lineTo", ((6, 6),)), # the previously implied line 179 ("curveTo", ((5, 5), (4, 4), (3, 3))), 180 ("curveTo", ((2, 2), (1, 1), (0, 0))), 181 ("closePath", ()), 182 ], 183 ), 184 ( 185 [ 186 ("moveTo", ((0, 0),)), 187 ("curveTo", ((1, 1), (2, 2), (3, 3))), 188 ("curveTo", ((4, 4), (5, 5), (6, 6))), # closed curve not overlapping move 189 ("closePath", ()), 190 ], 191 True, 192 [ 193 ("moveTo", ((0, 0),)), 194 ("lineTo", ((6, 6),)), # the previously implied line (same as above) 195 ("curveTo", ((5, 5), (4, 4), (3, 3))), 196 ("curveTo", ((2, 2), (1, 1), (0, 0))), 197 ("closePath", ()), 198 ], 199 ), 200 ( 201 [ 202 ("moveTo", ((0, 0),)), 203 ("lineTo", ((1, 1),)), # this line becomes implied 204 ("curveTo", ((2, 2), (3, 3), (4, 4))), 205 ("curveTo", ((5, 5), (6, 6), (7, 7))), 206 ("closePath", ()), 207 ], 208 False, 209 [ 210 ("moveTo", ((0, 0),)), 211 ("lineTo", ((7, 7),)), 212 ("curveTo", ((6, 6), (5, 5), (4, 4))), 213 ("curveTo", ((3, 3), (2, 2), (1, 1))), 214 ("closePath", ()), 215 ], 216 ), 217 ( 218 [ 219 ("moveTo", ((0, 0),)), 220 ("lineTo", ((1, 1),)), # this line... 221 ("curveTo", ((2, 2), (3, 3), (4, 4))), 222 ("curveTo", ((5, 5), (6, 6), (7, 7))), 223 ("closePath", ()), 224 ], 225 True, 226 [ 227 ("moveTo", ((0, 0),)), 228 ("lineTo", ((7, 7),)), 229 ("curveTo", ((6, 6), (5, 5), (4, 4))), 230 ("curveTo", ((3, 3), (2, 2), (1, 1))), 231 ("lineTo", ((0, 0),)), # ... does NOT become implied 232 ("closePath", ()), 233 ], 234 ), 235 ( 236 [ 237 ("moveTo", ((0, 0),)), 238 ("qCurveTo", ((1, 1), (2, 2))), 239 ("qCurveTo", ((3, 3), (0, 0))), # closed qCurve overlaps move 240 ("closePath", ()), 241 ], 242 False, 243 [ 244 ("moveTo", ((0, 0),)), # no extra lineTo added here 245 ("qCurveTo", ((3, 3), (2, 2))), 246 ("qCurveTo", ((1, 1), (0, 0))), 247 ("closePath", ()), 248 ], 249 ), 250 ( 251 [ 252 ("moveTo", ((0, 0),)), 253 ("qCurveTo", ((1, 1), (2, 2))), 254 ("qCurveTo", ((3, 3), (0, 0))), # closed qCurve overlaps move 255 ("closePath", ()), 256 ], 257 True, # <-- 258 [ 259 ("moveTo", ((0, 0),)), # no extra lineTo added here, same as above 260 ("qCurveTo", ((3, 3), (2, 2))), 261 ("qCurveTo", ((1, 1), (0, 0))), 262 ("closePath", ()), 263 ], 264 ), 265 ( 266 [ 267 ("moveTo", ((0, 0),)), 268 ("qCurveTo", ((1, 1), (2, 2))), 269 ("qCurveTo", ((3, 3), (4, 4))), # closed qCurve not overlapping move 270 ("closePath", ()), 271 ], 272 False, 273 [ 274 ("moveTo", ((0, 0),)), 275 ("lineTo", ((4, 4),)), # the previously implied line 276 ("qCurveTo", ((3, 3), (2, 2))), 277 ("qCurveTo", ((1, 1), (0, 0))), 278 ("closePath", ()), 279 ], 280 ), 281 ( 282 [ 283 ("moveTo", ((0, 0),)), 284 ("qCurveTo", ((1, 1), (2, 2))), 285 ("qCurveTo", ((3, 3), (4, 4))), # closed qCurve not overlapping move 286 ("closePath", ()), 287 ], 288 True, 289 [ 290 ("moveTo", ((0, 0),)), 291 ("lineTo", ((4, 4),)), # the previously implied line (same as above) 292 ("qCurveTo", ((3, 3), (2, 2))), 293 ("qCurveTo", ((1, 1), (0, 0))), 294 ("closePath", ()), 295 ], 296 ), 297 ( 298 [ 299 ("moveTo", ((0, 0),)), 300 ("lineTo", ((1, 1),)), 301 ("qCurveTo", ((2, 2), (3, 3))), 302 ("closePath", ()), 303 ], 304 False, 305 [ 306 ("moveTo", ((0, 0),)), 307 ("lineTo", ((3, 3),)), 308 ("qCurveTo", ((2, 2), (1, 1))), 309 ("closePath", ()), 310 ], 311 ), 312 ( 313 [("addComponent", ("a", (1, 0, 0, 1, 0, 0)))], 314 False, 315 [("addComponent", ("a", (1, 0, 0, 1, 0, 0)))], 316 ), 317 ([], False, []), 318 ( 319 [ 320 ("moveTo", ((0, 0),)), 321 ("endPath", ()), 322 ], 323 False, 324 [ 325 ("moveTo", ((0, 0),)), 326 ("endPath", ()), 327 ], 328 ), 329 ( 330 [ 331 ("moveTo", ((0, 0),)), 332 ("closePath", ()), 333 ], 334 False, 335 [ 336 ("moveTo", ((0, 0),)), 337 ("endPath", ()), # single-point paths is always open 338 ], 339 ), 340 ( 341 [("moveTo", ((0, 0),)), ("lineTo", ((1, 1),)), ("endPath", ())], 342 False, 343 [("moveTo", ((1, 1),)), ("lineTo", ((0, 0),)), ("endPath", ())], 344 ), 345 ( 346 [("moveTo", ((0, 0),)), ("curveTo", ((1, 1), (2, 2), (3, 3))), ("endPath", ())], 347 False, 348 [("moveTo", ((3, 3),)), ("curveTo", ((2, 2), (1, 1), (0, 0))), ("endPath", ())], 349 ), 350 ( 351 [ 352 ("moveTo", ((0, 0),)), 353 ("curveTo", ((1, 1), (2, 2), (3, 3))), 354 ("lineTo", ((4, 4),)), 355 ("endPath", ()), 356 ], 357 False, 358 [ 359 ("moveTo", ((4, 4),)), 360 ("lineTo", ((3, 3),)), 361 ("curveTo", ((2, 2), (1, 1), (0, 0))), 362 ("endPath", ()), 363 ], 364 ), 365 ( 366 [ 367 ("moveTo", ((0, 0),)), 368 ("lineTo", ((1, 1),)), 369 ("curveTo", ((2, 2), (3, 3), (4, 4))), 370 ("endPath", ()), 371 ], 372 False, 373 [ 374 ("moveTo", ((4, 4),)), 375 ("curveTo", ((3, 3), (2, 2), (1, 1))), 376 ("lineTo", ((0, 0),)), 377 ("endPath", ()), 378 ], 379 ), 380 ( 381 [("qCurveTo", ((0, 0), (1, 1), (2, 2), None)), ("closePath", ())], 382 False, 383 [("qCurveTo", ((0, 0), (2, 2), (1, 1), None)), ("closePath", ())], 384 ), 385 ( 386 [("qCurveTo", ((0, 0), (1, 1), (2, 2), None)), ("endPath", ())], 387 False, 388 [ 389 ("qCurveTo", ((0, 0), (2, 2), (1, 1), None)), 390 ("closePath", ()), # this is always "closed" 391 ], 392 ), 393 # Test case from: 394 # https://github.com/googlei18n/cu2qu/issues/51#issue-179370514 395 ( 396 [ 397 ("moveTo", ((848, 348),)), 398 ("lineTo", ((848, 348),)), # duplicate lineTo point after moveTo 399 ("qCurveTo", ((848, 526), (649, 704), (449, 704))), 400 ("qCurveTo", ((449, 704), (248, 704), (50, 526), (50, 348))), 401 ("lineTo", ((50, 348),)), 402 ("qCurveTo", ((50, 348), (50, 171), (248, -3), (449, -3))), 403 ("qCurveTo", ((449, -3), (649, -3), (848, 171), (848, 348))), 404 ("closePath", ()), 405 ], 406 False, 407 [ 408 ("moveTo", ((848, 348),)), 409 ("qCurveTo", ((848, 171), (649, -3), (449, -3), (449, -3))), 410 ("qCurveTo", ((248, -3), (50, 171), (50, 348), (50, 348))), 411 ("lineTo", ((50, 348),)), 412 ("qCurveTo", ((50, 526), (248, 704), (449, 704), (449, 704))), 413 ("qCurveTo", ((649, 704), (848, 526), (848, 348))), 414 ("lineTo", ((848, 348),)), # the duplicate point is kept 415 ("closePath", ()), 416 ], 417 ), 418 # Test case from https://github.com/googlefonts/fontmake/issues/572 419 # An additional closing lineTo is required to disambiguate a duplicate 420 # point at the end of a contour from the implied closing line. 421 ( 422 [ 423 ("moveTo", ((0, 651),)), 424 ("lineTo", ((0, 101),)), 425 ("lineTo", ((0, 101),)), 426 ("lineTo", ((0, 651),)), 427 ("lineTo", ((0, 651),)), 428 ("closePath", ()), 429 ], 430 False, 431 [ 432 ("moveTo", ((0, 651),)), 433 ("lineTo", ((0, 651),)), 434 ("lineTo", ((0, 101),)), 435 ("lineTo", ((0, 101),)), 436 ("closePath", ()), 437 ], 438 ), 439 ( 440 [ 441 ("moveTo", ((0, 651),)), 442 ("lineTo", ((0, 101),)), 443 ("lineTo", ((0, 101),)), 444 ("lineTo", ((0, 651),)), 445 ("lineTo", ((0, 651),)), 446 ("closePath", ()), 447 ], 448 True, 449 [ 450 ("moveTo", ((0, 651),)), 451 ("lineTo", ((0, 651),)), 452 ("lineTo", ((0, 101),)), 453 ("lineTo", ((0, 101),)), 454 ("lineTo", ((0, 651),)), # closing line not implied 455 ("closePath", ()), 456 ], 457 ), 458] 459 460 461@pytest.mark.parametrize("contour, outputImpliedClosingLine, expected", TEST_DATA) 462def test_reverse_pen(contour, outputImpliedClosingLine, expected): 463 recpen = RecordingPen() 464 revpen = ReverseContourPen(recpen, outputImpliedClosingLine) 465 for operator, operands in contour: 466 getattr(revpen, operator)(*operands) 467 assert recpen.value == expected 468 469 470def test_reverse_pen_outputImpliedClosingLine(): 471 recpen = RecordingPen() 472 revpen = ReverseContourPen(recpen) 473 revpen.moveTo((0, 0)) 474 revpen.lineTo((10, 0)) 475 revpen.lineTo((0, 10)) 476 revpen.lineTo((0, 0)) 477 revpen.closePath() 478 assert recpen.value == [ 479 ("moveTo", ((0, 0),)), 480 ("lineTo", ((0, 10),)), 481 ("lineTo", ((10, 0),)), 482 # ("lineTo", ((0, 0),)), # implied 483 ("closePath", ()), 484 ] 485 486 recpen = RecordingPen() 487 revpen = ReverseContourPen(recpen, outputImpliedClosingLine=True) 488 revpen.moveTo((0, 0)) 489 revpen.lineTo((10, 0)) 490 revpen.lineTo((0, 10)) 491 revpen.lineTo((0, 0)) 492 revpen.closePath() 493 assert recpen.value == [ 494 ("moveTo", ((0, 0),)), 495 ("lineTo", ((0, 10),)), 496 ("lineTo", ((10, 0),)), 497 ("lineTo", ((0, 0),)), # not implied 498 ("closePath", ()), 499 ] 500 501 502@pytest.mark.parametrize("contour, outputImpliedClosingLine, expected", TEST_DATA) 503def test_reverse_point_pen(contour, outputImpliedClosingLine, expected): 504 from fontTools.pens.pointPen import ( 505 ReverseContourPointPen, 506 PointToSegmentPen, 507 SegmentToPointPen, 508 ) 509 510 recpen = RecordingPen() 511 pt2seg = PointToSegmentPen(recpen, outputImpliedClosingLine) 512 revpen = ReverseContourPointPen(pt2seg) 513 seg2pt = SegmentToPointPen(revpen) 514 for operator, operands in contour: 515 getattr(seg2pt, operator)(*operands) 516 517 assert recpen.value == expected 518