1import unittest 2 3from fontTools.pens.basePen import AbstractPen 4from fontTools.pens.pointPen import ( 5 AbstractPointPen, 6 PointToSegmentPen, 7 SegmentToPointPen, 8 GuessSmoothPointPen, 9 ReverseContourPointPen, 10) 11 12 13class _TestSegmentPen(AbstractPen): 14 def __init__(self): 15 self._commands = [] 16 17 def __repr__(self): 18 return " ".join(self._commands) 19 20 def moveTo(self, pt): 21 self._commands.append("%s %s moveto" % (pt[0], pt[1])) 22 23 def lineTo(self, pt): 24 self._commands.append("%s %s lineto" % (pt[0], pt[1])) 25 26 def curveTo(self, *pts): 27 pts = ["%s %s" % pt for pt in pts] 28 self._commands.append("%s curveto" % " ".join(pts)) 29 30 def qCurveTo(self, *pts): 31 pts = ["%s %s" % pt if pt is not None else "None" for pt in pts] 32 self._commands.append("%s qcurveto" % " ".join(pts)) 33 34 def closePath(self): 35 self._commands.append("closepath") 36 37 def endPath(self): 38 self._commands.append("endpath") 39 40 def addComponent(self, glyphName, transformation): 41 self._commands.append("'%s' %s addcomponent" % (glyphName, transformation)) 42 43 44def _reprKwargs(kwargs): 45 items = [] 46 for key in sorted(kwargs): 47 value = kwargs[key] 48 if isinstance(value, str): 49 items.append("%s='%s'" % (key, value)) 50 else: 51 items.append("%s=%s" % (key, value)) 52 return items 53 54 55class _TestPointPen(AbstractPointPen): 56 def __init__(self): 57 self._commands = [] 58 59 def __repr__(self): 60 return " ".join(self._commands) 61 62 def beginPath(self, identifier=None, **kwargs): 63 items = [] 64 if identifier is not None: 65 items.append("identifier='%s'" % identifier) 66 items.extend(_reprKwargs(kwargs)) 67 self._commands.append("beginPath(%s)" % ", ".join(items)) 68 69 def addPoint( 70 self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs 71 ): 72 items = ["%s" % (pt,)] 73 if segmentType is not None: 74 items.append("segmentType='%s'" % segmentType) 75 if smooth: 76 items.append("smooth=True") 77 if name is not None: 78 items.append("name='%s'" % name) 79 if identifier is not None: 80 items.append("identifier='%s'" % identifier) 81 items.extend(_reprKwargs(kwargs)) 82 self._commands.append("addPoint(%s)" % ", ".join(items)) 83 84 def endPath(self): 85 self._commands.append("endPath()") 86 87 def addComponent(self, glyphName, transform, identifier=None, **kwargs): 88 items = ["'%s'" % glyphName, "%s" % transform] 89 if identifier is not None: 90 items.append("identifier='%s'" % identifier) 91 items.extend(_reprKwargs(kwargs)) 92 self._commands.append("addComponent(%s)" % ", ".join(items)) 93 94 95class PointToSegmentPenTest(unittest.TestCase): 96 def test_open(self): 97 pen = _TestSegmentPen() 98 ppen = PointToSegmentPen(pen) 99 ppen.beginPath() 100 ppen.addPoint((10, 10), "move") 101 ppen.addPoint((10, 20), "line") 102 ppen.endPath() 103 self.assertEqual("10 10 moveto 10 20 lineto endpath", repr(pen)) 104 105 def test_closed(self): 106 pen = _TestSegmentPen() 107 ppen = PointToSegmentPen(pen) 108 ppen.beginPath() 109 ppen.addPoint((10, 10), "line") 110 ppen.addPoint((10, 20), "line") 111 ppen.addPoint((20, 20), "line") 112 ppen.endPath() 113 self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(pen)) 114 115 def test_cubic(self): 116 pen = _TestSegmentPen() 117 ppen = PointToSegmentPen(pen) 118 ppen.beginPath() 119 ppen.addPoint((10, 10), "line") 120 ppen.addPoint((10, 20)) 121 ppen.addPoint((20, 20)) 122 ppen.addPoint((20, 40), "curve") 123 ppen.endPath() 124 self.assertEqual("10 10 moveto 10 20 20 20 20 40 curveto closepath", repr(pen)) 125 126 def test_quad(self): 127 pen = _TestSegmentPen() 128 ppen = PointToSegmentPen(pen) 129 ppen.beginPath(identifier="foo") 130 ppen.addPoint((10, 10), "line") 131 ppen.addPoint((10, 40)) 132 ppen.addPoint((40, 40)) 133 ppen.addPoint((10, 40), "qcurve") 134 ppen.endPath() 135 self.assertEqual("10 10 moveto 10 40 40 40 10 40 qcurveto closepath", repr(pen)) 136 137 def test_quad_onlyOffCurvePoints(self): 138 pen = _TestSegmentPen() 139 ppen = PointToSegmentPen(pen) 140 ppen.beginPath() 141 ppen.addPoint((10, 10)) 142 ppen.addPoint((10, 40)) 143 ppen.addPoint((40, 40)) 144 ppen.endPath() 145 self.assertEqual("10 10 10 40 40 40 None qcurveto closepath", repr(pen)) 146 147 def test_roundTrip1(self): 148 tpen = _TestPointPen() 149 ppen = PointToSegmentPen(SegmentToPointPen(tpen)) 150 ppen.beginPath() 151 ppen.addPoint((10, 10), "line") 152 ppen.addPoint((10, 20)) 153 ppen.addPoint((20, 20)) 154 ppen.addPoint((20, 40), "curve") 155 ppen.endPath() 156 self.assertEqual( 157 "beginPath() addPoint((10, 10), segmentType='line') addPoint((10, 20)) " 158 "addPoint((20, 20)) addPoint((20, 40), segmentType='curve') endPath()", 159 repr(tpen), 160 ) 161 162 def test_closed_outputImpliedClosingLine(self): 163 tpen = _TestSegmentPen() 164 ppen = PointToSegmentPen(tpen, outputImpliedClosingLine=True) 165 ppen.beginPath() 166 ppen.addPoint((10, 10), "line") 167 ppen.addPoint((10, 20), "line") 168 ppen.addPoint((20, 20), "line") 169 ppen.endPath() 170 self.assertEqual( 171 "10 10 moveto " 172 "10 20 lineto " 173 "20 20 lineto " 174 "10 10 lineto " # explicit closing line 175 "closepath", 176 repr(tpen), 177 ) 178 179 def test_closed_line_overlapping_start_end_points(self): 180 # Test case from https://github.com/googlefonts/fontmake/issues/572. 181 tpen = _TestSegmentPen() 182 ppen = PointToSegmentPen(tpen, outputImpliedClosingLine=False) 183 # The last oncurve point on this closed contour is a "line" segment and has 184 # same coordinates as the starting point. 185 ppen.beginPath() 186 ppen.addPoint((0, 651), segmentType="line") 187 ppen.addPoint((0, 101), segmentType="line") 188 ppen.addPoint((0, 101), segmentType="line") 189 ppen.addPoint((0, 651), segmentType="line") 190 ppen.endPath() 191 # Check that we always output an explicit 'lineTo' segment at the end, 192 # regardless of the value of 'outputImpliedClosingLine', to disambiguate 193 # the duplicate point from the implied closing line. 194 self.assertEqual( 195 "0 651 moveto " 196 "0 101 lineto " 197 "0 101 lineto " 198 "0 651 lineto " 199 "0 651 lineto " 200 "closepath", 201 repr(tpen), 202 ) 203 204 def test_roundTrip2(self): 205 tpen = _TestPointPen() 206 ppen = PointToSegmentPen(SegmentToPointPen(tpen)) 207 ppen.beginPath() 208 ppen.addPoint((0, 651), segmentType="line") 209 ppen.addPoint((0, 101), segmentType="line") 210 ppen.addPoint((0, 101), segmentType="line") 211 ppen.addPoint((0, 651), segmentType="line") 212 ppen.endPath() 213 self.assertEqual( 214 "beginPath() " 215 "addPoint((0, 651), segmentType='line') " 216 "addPoint((0, 101), segmentType='line') " 217 "addPoint((0, 101), segmentType='line') " 218 "addPoint((0, 651), segmentType='line') " 219 "endPath()", 220 repr(tpen), 221 ) 222 223 224class TestSegmentToPointPen(unittest.TestCase): 225 def test_move(self): 226 tpen = _TestPointPen() 227 pen = SegmentToPointPen(tpen) 228 pen.moveTo((10, 10)) 229 pen.endPath() 230 self.assertEqual( 231 "beginPath() addPoint((10, 10), segmentType='move') endPath()", repr(tpen) 232 ) 233 234 def test_poly(self): 235 tpen = _TestPointPen() 236 pen = SegmentToPointPen(tpen) 237 pen.moveTo((10, 10)) 238 pen.lineTo((10, 20)) 239 pen.lineTo((20, 20)) 240 pen.closePath() 241 self.assertEqual( 242 "beginPath() addPoint((10, 10), segmentType='line') " 243 "addPoint((10, 20), segmentType='line') " 244 "addPoint((20, 20), segmentType='line') endPath()", 245 repr(tpen), 246 ) 247 248 def test_cubic(self): 249 tpen = _TestPointPen() 250 pen = SegmentToPointPen(tpen) 251 pen.moveTo((10, 10)) 252 pen.curveTo((10, 20), (20, 20), (20, 10)) 253 pen.closePath() 254 self.assertEqual( 255 "beginPath() addPoint((10, 10), segmentType='line') " 256 "addPoint((10, 20)) addPoint((20, 20)) addPoint((20, 10), " 257 "segmentType='curve') endPath()", 258 repr(tpen), 259 ) 260 261 def test_quad(self): 262 tpen = _TestPointPen() 263 pen = SegmentToPointPen(tpen) 264 pen.moveTo((10, 10)) 265 pen.qCurveTo((10, 20), (20, 20), (20, 10)) 266 pen.closePath() 267 self.assertEqual( 268 "beginPath() addPoint((10, 10), segmentType='line') " 269 "addPoint((10, 20)) addPoint((20, 20)) " 270 "addPoint((20, 10), segmentType='qcurve') endPath()", 271 repr(tpen), 272 ) 273 274 def test_quad2(self): 275 tpen = _TestPointPen() 276 pen = SegmentToPointPen(tpen) 277 pen.qCurveTo((10, 20), (20, 20), (20, 10), (10, 10), None) 278 pen.closePath() 279 self.assertEqual( 280 "beginPath() addPoint((10, 20)) addPoint((20, 20)) " 281 "addPoint((20, 10)) addPoint((10, 10)) endPath()", 282 repr(tpen), 283 ) 284 285 def test_roundTrip1(self): 286 spen = _TestSegmentPen() 287 pen = SegmentToPointPen(PointToSegmentPen(spen)) 288 pen.moveTo((10, 10)) 289 pen.lineTo((10, 20)) 290 pen.lineTo((20, 20)) 291 pen.closePath() 292 self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(spen)) 293 294 def test_roundTrip2(self): 295 spen = _TestSegmentPen() 296 pen = SegmentToPointPen(PointToSegmentPen(spen)) 297 pen.qCurveTo((10, 20), (20, 20), (20, 10), (10, 10), None) 298 pen.closePath() 299 pen.addComponent("base", [1, 0, 0, 1, 0, 0]) 300 self.assertEqual( 301 "10 20 20 20 20 10 10 10 None qcurveto closepath " 302 "'base' [1, 0, 0, 1, 0, 0] addcomponent", 303 repr(spen), 304 ) 305 306 307class TestGuessSmoothPointPen(unittest.TestCase): 308 def test_guessSmooth_exact(self): 309 tpen = _TestPointPen() 310 pen = GuessSmoothPointPen(tpen) 311 pen.beginPath(identifier="foo") 312 pen.addPoint((0, 100), segmentType="curve") 313 pen.addPoint((0, 200)) 314 pen.addPoint((400, 200), identifier="bar") 315 pen.addPoint((400, 100), segmentType="curve") 316 pen.addPoint((400, 0)) 317 pen.addPoint((0, 0)) 318 pen.endPath() 319 self.assertEqual( 320 "beginPath(identifier='foo') " 321 "addPoint((0, 100), segmentType='curve', smooth=True) " 322 "addPoint((0, 200)) addPoint((400, 200), identifier='bar') " 323 "addPoint((400, 100), segmentType='curve', smooth=True) " 324 "addPoint((400, 0)) addPoint((0, 0)) endPath()", 325 repr(tpen), 326 ) 327 328 def test_guessSmooth_almost(self): 329 tpen = _TestPointPen() 330 pen = GuessSmoothPointPen(tpen) 331 pen.beginPath() 332 pen.addPoint((0, 100), segmentType="curve") 333 pen.addPoint((1, 200)) 334 pen.addPoint((395, 200)) 335 pen.addPoint((400, 100), segmentType="curve") 336 pen.addPoint((400, 0)) 337 pen.addPoint((0, 0)) 338 pen.endPath() 339 self.assertEqual( 340 "beginPath() addPoint((0, 100), segmentType='curve', smooth=True) " 341 "addPoint((1, 200)) addPoint((395, 200)) " 342 "addPoint((400, 100), segmentType='curve', smooth=True) " 343 "addPoint((400, 0)) addPoint((0, 0)) endPath()", 344 repr(tpen), 345 ) 346 347 def test_guessSmooth_tangent(self): 348 tpen = _TestPointPen() 349 pen = GuessSmoothPointPen(tpen) 350 pen.beginPath() 351 pen.addPoint((0, 0), segmentType="move") 352 pen.addPoint((0, 100), segmentType="line") 353 pen.addPoint((3, 200)) 354 pen.addPoint((300, 200)) 355 pen.addPoint((400, 200), segmentType="curve") 356 pen.endPath() 357 self.assertEqual( 358 "beginPath() addPoint((0, 0), segmentType='move') " 359 "addPoint((0, 100), segmentType='line', smooth=True) " 360 "addPoint((3, 200)) addPoint((300, 200)) " 361 "addPoint((400, 200), segmentType='curve') endPath()", 362 repr(tpen), 363 ) 364 365 366class TestReverseContourPointPen(unittest.TestCase): 367 def test_singlePoint(self): 368 tpen = _TestPointPen() 369 pen = ReverseContourPointPen(tpen) 370 pen.beginPath() 371 pen.addPoint((0, 0), segmentType="move") 372 pen.endPath() 373 self.assertEqual( 374 "beginPath() " "addPoint((0, 0), segmentType='move') " "endPath()", 375 repr(tpen), 376 ) 377 378 def test_line(self): 379 tpen = _TestPointPen() 380 pen = ReverseContourPointPen(tpen) 381 pen.beginPath() 382 pen.addPoint((0, 0), segmentType="move") 383 pen.addPoint((0, 100), segmentType="line") 384 pen.endPath() 385 self.assertEqual( 386 "beginPath() " 387 "addPoint((0, 100), segmentType='move') " 388 "addPoint((0, 0), segmentType='line') " 389 "endPath()", 390 repr(tpen), 391 ) 392 393 def test_triangle(self): 394 tpen = _TestPointPen() 395 pen = ReverseContourPointPen(tpen) 396 pen.beginPath() 397 pen.addPoint((0, 0), segmentType="line") 398 pen.addPoint((0, 100), segmentType="line") 399 pen.addPoint((100, 100), segmentType="line") 400 pen.endPath() 401 self.assertEqual( 402 "beginPath() " 403 "addPoint((0, 0), segmentType='line') " 404 "addPoint((100, 100), segmentType='line') " 405 "addPoint((0, 100), segmentType='line') " 406 "endPath()", 407 repr(tpen), 408 ) 409 410 def test_cubicOpen(self): 411 tpen = _TestPointPen() 412 pen = ReverseContourPointPen(tpen) 413 pen.beginPath() 414 pen.addPoint((0, 0), segmentType="move") 415 pen.addPoint((0, 100)) 416 pen.addPoint((100, 200)) 417 pen.addPoint((200, 200), segmentType="curve") 418 pen.endPath() 419 self.assertEqual( 420 "beginPath() " 421 "addPoint((200, 200), segmentType='move') " 422 "addPoint((100, 200)) " 423 "addPoint((0, 100)) " 424 "addPoint((0, 0), segmentType='curve') " 425 "endPath()", 426 repr(tpen), 427 ) 428 429 def test_quadOpen(self): 430 tpen = _TestPointPen() 431 pen = ReverseContourPointPen(tpen) 432 pen.beginPath() 433 pen.addPoint((0, 0), segmentType="move") 434 pen.addPoint((0, 100)) 435 pen.addPoint((100, 200)) 436 pen.addPoint((200, 200), segmentType="qcurve") 437 pen.endPath() 438 self.assertEqual( 439 "beginPath() " 440 "addPoint((200, 200), segmentType='move') " 441 "addPoint((100, 200)) " 442 "addPoint((0, 100)) " 443 "addPoint((0, 0), segmentType='qcurve') " 444 "endPath()", 445 repr(tpen), 446 ) 447 448 def test_cubicClosed(self): 449 tpen = _TestPointPen() 450 pen = ReverseContourPointPen(tpen) 451 pen.beginPath() 452 pen.addPoint((0, 0), segmentType="line") 453 pen.addPoint((0, 100)) 454 pen.addPoint((100, 200)) 455 pen.addPoint((200, 200), segmentType="curve") 456 pen.endPath() 457 self.assertEqual( 458 "beginPath() " 459 "addPoint((0, 0), segmentType='curve') " 460 "addPoint((200, 200), segmentType='line') " 461 "addPoint((100, 200)) " 462 "addPoint((0, 100)) " 463 "endPath()", 464 repr(tpen), 465 ) 466 467 def test_quadClosedOffCurveStart(self): 468 tpen = _TestPointPen() 469 pen = ReverseContourPointPen(tpen) 470 pen.beginPath() 471 pen.addPoint((100, 200)) 472 pen.addPoint((200, 200), segmentType="qcurve") 473 pen.addPoint((0, 0), segmentType="line") 474 pen.addPoint((0, 100)) 475 pen.endPath() 476 self.assertEqual( 477 "beginPath() " 478 "addPoint((100, 200)) " 479 "addPoint((0, 100)) " 480 "addPoint((0, 0), segmentType='qcurve') " 481 "addPoint((200, 200), segmentType='line') " 482 "endPath()", 483 repr(tpen), 484 ) 485 486 def test_quadNoOnCurve(self): 487 tpen = _TestPointPen() 488 pen = ReverseContourPointPen(tpen) 489 pen.beginPath(identifier="bar") 490 pen.addPoint((0, 0)) 491 pen.addPoint((0, 100), identifier="foo", arbitrary="foo") 492 pen.addPoint((100, 200), arbitrary=123) 493 pen.addPoint((200, 200)) 494 pen.endPath() 495 pen.addComponent("base", [1, 0, 0, 1, 0, 0], identifier="foo") 496 self.assertEqual( 497 "beginPath(identifier='bar') " 498 "addPoint((0, 0)) " 499 "addPoint((200, 200)) " 500 "addPoint((100, 200), arbitrary=123) " 501 "addPoint((0, 100), identifier='foo', arbitrary='foo') " 502 "endPath() " 503 "addComponent('base', [1, 0, 0, 1, 0, 0], identifier='foo')", 504 repr(tpen), 505 ) 506 507 def test_closed_line_overlapping_start_end_points(self): 508 # Test case from https://github.com/googlefonts/fontmake/issues/572 509 tpen = _TestPointPen() 510 pen = ReverseContourPointPen(tpen) 511 pen.beginPath() 512 pen.addPoint((0, 651), segmentType="line") 513 pen.addPoint((0, 101), segmentType="line") 514 pen.addPoint((0, 101), segmentType="line") 515 pen.addPoint((0, 651), segmentType="line") 516 pen.endPath() 517 self.assertEqual( 518 "beginPath() " 519 "addPoint((0, 651), segmentType='line') " 520 "addPoint((0, 651), segmentType='line') " 521 "addPoint((0, 101), segmentType='line') " 522 "addPoint((0, 101), segmentType='line') " 523 "endPath()", 524 repr(tpen), 525 ) 526