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