1from fontTools.ttLib import TTFont 2from fontTools.ttLib import ttGlyphSet 3from fontTools.ttLib.ttGlyphSet import LerpGlyphSet 4from fontTools.pens.recordingPen import ( 5 RecordingPen, 6 RecordingPointPen, 7 DecomposingRecordingPen, 8) 9from fontTools.misc.roundTools import otRound 10from fontTools.misc.transform import DecomposedTransform 11import os 12import pytest 13 14 15class TTGlyphSetTest(object): 16 @staticmethod 17 def getpath(testfile): 18 path = os.path.dirname(__file__) 19 return os.path.join(path, "data", testfile) 20 21 @pytest.mark.parametrize( 22 "fontfile, location, expected", 23 [ 24 ( 25 "I.ttf", 26 None, 27 [ 28 ("moveTo", ((175, 0),)), 29 ("lineTo", ((367, 0),)), 30 ("lineTo", ((367, 1456),)), 31 ("lineTo", ((175, 1456),)), 32 ("closePath", ()), 33 ], 34 ), 35 ( 36 "I.ttf", 37 {}, 38 [ 39 ("moveTo", ((175, 0),)), 40 ("lineTo", ((367, 0),)), 41 ("lineTo", ((367, 1456),)), 42 ("lineTo", ((175, 1456),)), 43 ("closePath", ()), 44 ], 45 ), 46 ( 47 "I.ttf", 48 {"wght": 100}, 49 [ 50 ("moveTo", ((175, 0),)), 51 ("lineTo", ((271, 0),)), 52 ("lineTo", ((271, 1456),)), 53 ("lineTo", ((175, 1456),)), 54 ("closePath", ()), 55 ], 56 ), 57 ( 58 "I.ttf", 59 {"wght": 1000}, 60 [ 61 ("moveTo", ((128, 0),)), 62 ("lineTo", ((550, 0),)), 63 ("lineTo", ((550, 1456),)), 64 ("lineTo", ((128, 1456),)), 65 ("closePath", ()), 66 ], 67 ), 68 ( 69 "I.ttf", 70 {"wght": 1000, "wdth": 25}, 71 [ 72 ("moveTo", ((140, 0),)), 73 ("lineTo", ((553, 0),)), 74 ("lineTo", ((553, 1456),)), 75 ("lineTo", ((140, 1456),)), 76 ("closePath", ()), 77 ], 78 ), 79 ( 80 "I.ttf", 81 {"wght": 1000, "wdth": 50}, 82 [ 83 ("moveTo", ((136, 0),)), 84 ("lineTo", ((552, 0),)), 85 ("lineTo", ((552, 1456),)), 86 ("lineTo", ((136, 1456),)), 87 ("closePath", ()), 88 ], 89 ), 90 ( 91 "I.otf", 92 {"wght": 1000}, 93 [ 94 ("moveTo", ((179, 74),)), 95 ("lineTo", ((28, 59),)), 96 ("lineTo", ((28, 0),)), 97 ("lineTo", ((367, 0),)), 98 ("lineTo", ((367, 59),)), 99 ("lineTo", ((212, 74),)), 100 ("lineTo", ((179, 74),)), 101 ("closePath", ()), 102 ("moveTo", ((179, 578),)), 103 ("lineTo", ((212, 578),)), 104 ("lineTo", ((367, 593),)), 105 ("lineTo", ((367, 652),)), 106 ("lineTo", ((28, 652),)), 107 ("lineTo", ((28, 593),)), 108 ("lineTo", ((179, 578),)), 109 ("closePath", ()), 110 ("moveTo", ((98, 310),)), 111 ("curveTo", ((98, 205), (98, 101), (95, 0))), 112 ("lineTo", ((299, 0),)), 113 ("curveTo", ((296, 103), (296, 207), (296, 311))), 114 ("lineTo", ((296, 342),)), 115 ("curveTo", ((296, 447), (296, 551), (299, 652))), 116 ("lineTo", ((95, 652),)), 117 ("curveTo", ((98, 549), (98, 445), (98, 342))), 118 ("lineTo", ((98, 310),)), 119 ("closePath", ()), 120 ], 121 ), 122 ( 123 # In this font, /I has an lsb of 30, but an xMin of 25, so an 124 # offset of 5 units needs to be applied when drawing the outline. 125 # See https://github.com/fonttools/fonttools/issues/2824 126 "issue2824.ttf", 127 None, 128 [ 129 ("moveTo", ((309, 180),)), 130 ("qCurveTo", ((274, 151), (187, 136), (104, 166), (74, 201))), 131 ("qCurveTo", ((45, 236), (30, 323), (59, 407), (95, 436))), 132 ("qCurveTo", ((130, 466), (217, 480), (301, 451), (330, 415))), 133 ("qCurveTo", ((360, 380), (374, 293), (345, 210), (309, 180))), 134 ("closePath", ()), 135 ], 136 ), 137 ], 138 ) 139 def test_glyphset(self, fontfile, location, expected): 140 font = TTFont(self.getpath(fontfile)) 141 glyphset = font.getGlyphSet(location=location) 142 143 assert isinstance(glyphset, ttGlyphSet._TTGlyphSet) 144 145 assert list(glyphset.keys()) == [".notdef", "I"] 146 147 assert "I" in glyphset 148 with pytest.deprecated_call(): 149 assert glyphset.has_key("I") # we should really get rid of this... 150 151 assert len(glyphset) == 2 152 153 pen = RecordingPen() 154 glyph = glyphset["I"] 155 156 assert glyphset.get("foobar") is None 157 158 assert isinstance(glyph, ttGlyphSet._TTGlyph) 159 is_glyf = fontfile.endswith(".ttf") 160 glyphType = ttGlyphSet._TTGlyphGlyf if is_glyf else ttGlyphSet._TTGlyphCFF 161 assert isinstance(glyph, glyphType) 162 163 glyph.draw(pen) 164 actual = pen.value 165 166 assert actual == expected, (location, actual, expected) 167 168 @pytest.mark.parametrize( 169 "fontfile, locations, factor, expected", 170 [ 171 ( 172 "I.ttf", 173 ({"wght": 400}, {"wght": 1000}), 174 0.5, 175 [ 176 ("moveTo", ((151.5, 0.0),)), 177 ("lineTo", ((458.5, 0.0),)), 178 ("lineTo", ((458.5, 1456.0),)), 179 ("lineTo", ((151.5, 1456.0),)), 180 ("closePath", ()), 181 ], 182 ), 183 ( 184 "I.ttf", 185 ({"wght": 400}, {"wght": 1000}), 186 0.25, 187 [ 188 ("moveTo", ((163.25, 0.0),)), 189 ("lineTo", ((412.75, 0.0),)), 190 ("lineTo", ((412.75, 1456.0),)), 191 ("lineTo", ((163.25, 1456.0),)), 192 ("closePath", ()), 193 ], 194 ), 195 ], 196 ) 197 def test_lerp_glyphset(self, fontfile, locations, factor, expected): 198 font = TTFont(self.getpath(fontfile)) 199 glyphset1 = font.getGlyphSet(location=locations[0]) 200 glyphset2 = font.getGlyphSet(location=locations[1]) 201 glyphset = LerpGlyphSet(glyphset1, glyphset2, factor) 202 203 assert "I" in glyphset 204 205 pen = RecordingPen() 206 glyph = glyphset["I"] 207 208 assert glyphset.get("foobar") is None 209 210 glyph.draw(pen) 211 actual = pen.value 212 213 assert actual == expected, (locations, actual, expected) 214 215 def test_glyphset_varComposite_components(self): 216 font = TTFont(self.getpath("varc-ac00-ac01.ttf")) 217 glyphset = font.getGlyphSet() 218 219 pen = RecordingPen() 220 glyph = glyphset["uniAC00"] 221 222 glyph.draw(pen) 223 actual = pen.value 224 225 expected = [ 226 ( 227 "addVarComponent", 228 ( 229 "glyph00003", 230 DecomposedTransform(460.0, 676.0, 0, 1, 1, 0, 0, 0, 0), 231 { 232 "0000": 0.84661865234375, 233 "0001": 0.98944091796875, 234 "0002": 0.47283935546875, 235 "0003": 0.446533203125, 236 }, 237 ), 238 ), 239 ( 240 "addVarComponent", 241 ( 242 "glyph00004", 243 DecomposedTransform(932.0, 382.0, 0, 1, 1, 0, 0, 0, 0), 244 { 245 "0000": 0.93359375, 246 "0001": 0.916015625, 247 "0002": 0.523193359375, 248 "0003": 0.32806396484375, 249 "0004": 0.85089111328125, 250 }, 251 ), 252 ), 253 ] 254 255 assert actual == expected, (actual, expected) 256 257 def test_glyphset_varComposite1(self): 258 font = TTFont(self.getpath("varc-ac00-ac01.ttf")) 259 glyphset = font.getGlyphSet(location={"wght": 600}) 260 261 pen = DecomposingRecordingPen(glyphset) 262 glyph = glyphset["uniAC00"] 263 264 glyph.draw(pen) 265 actual = pen.value 266 267 expected = [ 268 ("moveTo", ((432, 678),)), 269 ("lineTo", ((432, 620),)), 270 ( 271 "qCurveTo", 272 ( 273 (419, 620), 274 (374, 621), 275 (324, 619), 276 (275, 618), 277 (237, 617), 278 (228, 616), 279 ), 280 ), 281 ("qCurveTo", ((218, 616), (188, 612), (160, 605), (149, 601))), 282 ("qCurveTo", ((127, 611), (83, 639), (67, 654))), 283 ("qCurveTo", ((64, 657), (63, 662), (64, 666))), 284 ("lineTo", ((72, 678),)), 285 ("qCurveTo", ((93, 674), (144, 672), (164, 672))), 286 ( 287 "qCurveTo", 288 ( 289 (173, 672), 290 (213, 672), 291 (266, 673), 292 (323, 674), 293 (377, 675), 294 (421, 678), 295 (432, 678), 296 ), 297 ), 298 ("closePath", ()), 299 ("moveTo", ((525, 619),)), 300 ("lineTo", ((412, 620),)), 301 ("lineTo", ((429, 678),)), 302 ("lineTo", ((466, 697),)), 303 ("qCurveTo", ((470, 698), (482, 698), (486, 697))), 304 ("qCurveTo", ((494, 693), (515, 682), (536, 670), (541, 667))), 305 ("qCurveTo", ((545, 663), (545, 656), (543, 652))), 306 ("lineTo", ((525, 619),)), 307 ("closePath", ()), 308 ("moveTo", ((63, 118),)), 309 ("lineTo", ((47, 135),)), 310 ("qCurveTo", ((42, 141), (48, 146))), 311 ("qCurveTo", ((135, 213), (278, 373), (383, 541), (412, 620))), 312 ("lineTo", ((471, 642),)), 313 ("lineTo", ((525, 619),)), 314 ("qCurveTo", ((496, 529), (365, 342), (183, 179), (75, 121))), 315 ("qCurveTo", ((72, 119), (65, 118), (63, 118))), 316 ("closePath", ()), 317 ("moveTo", ((925, 372),)), 318 ("lineTo", ((739, 368),)), 319 ("lineTo", ((739, 427),)), 320 ("lineTo", ((822, 430),)), 321 ("lineTo", ((854, 451),)), 322 ("qCurveTo", ((878, 453), (930, 449), (944, 445))), 323 ("qCurveTo", ((961, 441), (962, 426))), 324 ("qCurveTo", ((964, 411), (956, 386), (951, 381))), 325 ("qCurveTo", ((947, 376), (931, 372), (925, 372))), 326 ("closePath", ()), 327 ("moveTo", ((729, -113),)), 328 ("lineTo", ((674, -113),)), 329 ("qCurveTo", ((671, -98), (669, -42), (666, 22), (665, 83), (665, 102))), 330 ("lineTo", ((665, 763),)), 331 ("qCurveTo", ((654, 780), (608, 810), (582, 820))), 332 ("lineTo", ((593, 850),)), 333 ("qCurveTo", ((594, 852), (599, 856), (607, 856))), 334 ("qCurveTo", ((628, 855), (684, 846), (736, 834), (752, 827))), 335 ("qCurveTo", ((766, 818), (766, 802))), 336 ("lineTo", ((762, 745),)), 337 ("lineTo", ((762, 134),)), 338 ("qCurveTo", ((762, 107), (757, 43), (749, -25), (737, -87), (729, -113))), 339 ("closePath", ()), 340 ] 341 342 actual = [ 343 (op, tuple((otRound(pt[0]), otRound(pt[1])) for pt in args)) 344 for op, args in actual 345 ] 346 347 assert actual == expected, (actual, expected) 348 349 # Test that drawing twice works, we accidentally don't change the component 350 pen = DecomposingRecordingPen(glyphset) 351 glyph.draw(pen) 352 actual = pen.value 353 actual = [ 354 (op, tuple((otRound(pt[0]), otRound(pt[1])) for pt in args)) 355 for op, args in actual 356 ] 357 assert actual == expected, (actual, expected) 358 359 pen = RecordingPointPen() 360 glyph.drawPoints(pen) 361 assert pen.value 362 363 def test_glyphset_varComposite2(self): 364 # This test font has axis variations 365 366 font = TTFont(self.getpath("varc-6868.ttf")) 367 glyphset = font.getGlyphSet(location={"wght": 600}) 368 369 pen = DecomposingRecordingPen(glyphset) 370 glyph = glyphset["uni6868"] 371 372 glyph.draw(pen) 373 actual = pen.value 374 375 expected = [ 376 ("moveTo", ((460, 565),)), 377 ( 378 "qCurveTo", 379 ( 380 (482, 577), 381 (526, 603), 382 (568, 632), 383 (607, 663), 384 (644, 698), 385 (678, 735), 386 (708, 775), 387 (721, 796), 388 ), 389 ), 390 ("lineTo", ((632, 835),)), 391 ( 392 "qCurveTo", 393 ( 394 (621, 817), 395 (595, 784), 396 (566, 753), 397 (534, 724), 398 (499, 698), 399 (462, 675), 400 (423, 653), 401 (403, 644), 402 ), 403 ), 404 ("closePath", ()), 405 ("moveTo", ((616, 765),)), 406 ("lineTo", ((590, 682),)), 407 ("lineTo", ((830, 682),)), 408 ("lineTo", ((833, 682),)), 409 ("lineTo", ((828, 693),)), 410 ( 411 "qCurveTo", 412 ( 413 (817, 671), 414 (775, 620), 415 (709, 571), 416 (615, 525), 417 (492, 490), 418 (413, 480), 419 ), 420 ), 421 ("lineTo", ((454, 386),)), 422 ( 423 "qCurveTo", 424 ( 425 (544, 403), 426 (687, 455), 427 (798, 519), 428 (877, 590), 429 (926, 655), 430 (937, 684), 431 ), 432 ), 433 ("lineTo", ((937, 765),)), 434 ("closePath", ()), 435 ("moveTo", ((723, 555),)), 436 ( 437 "qCurveTo", 438 ( 439 (713, 563), 440 (693, 579), 441 (672, 595), 442 (651, 610), 443 (629, 625), 444 (606, 638), 445 (583, 651), 446 (572, 657), 447 ), 448 ), 449 ("lineTo", ((514, 590),)), 450 ( 451 "qCurveTo", 452 ( 453 (525, 584), 454 (547, 572), 455 (568, 559), 456 (589, 545), 457 (609, 531), 458 (629, 516), 459 (648, 500), 460 (657, 492), 461 ), 462 ), 463 ("closePath", ()), 464 ("moveTo", ((387, 375),)), 465 ("lineTo", ((387, 830),)), 466 ("lineTo", ((289, 830),)), 467 ("lineTo", ((289, 375),)), 468 ("closePath", ()), 469 ("moveTo", ((96, 383),)), 470 ( 471 "qCurveTo", 472 ( 473 (116, 390), 474 (156, 408), 475 (194, 427), 476 (231, 449), 477 (268, 472), 478 (302, 497), 479 (335, 525), 480 (351, 539), 481 ), 482 ), 483 ("lineTo", ((307, 610),)), 484 ( 485 "qCurveTo", 486 ( 487 (291, 597), 488 (257, 572), 489 (221, 549), 490 (185, 528), 491 (147, 509), 492 (108, 492), 493 (69, 476), 494 (48, 469), 495 ), 496 ), 497 ("closePath", ()), 498 ("moveTo", ((290, 653),)), 499 ( 500 "qCurveTo", 501 ( 502 (281, 664), 503 (261, 687), 504 (240, 708), 505 (219, 729), 506 (196, 749), 507 (173, 768), 508 (148, 786), 509 (136, 794), 510 ), 511 ), 512 ("lineTo", ((69, 727),)), 513 ( 514 "qCurveTo", 515 ( 516 (81, 719), 517 (105, 702), 518 (129, 684), 519 (151, 665), 520 (173, 645), 521 (193, 625), 522 (213, 604), 523 (222, 593), 524 ), 525 ), 526 ("closePath", ()), 527 ("moveTo", ((913, -57),)), 528 ("lineTo", ((953, 30),)), 529 ( 530 "qCurveTo", 531 ( 532 (919, 41), 533 (854, 67), 534 (790, 98), 535 (729, 134), 536 (671, 173), 537 (616, 217), 538 (564, 264), 539 (540, 290), 540 ), 541 ), 542 ("lineTo", ((522, 286),)), 543 ("qCurveTo", ((511, 267), (498, 235), (493, 213), (492, 206))), 544 ("lineTo", ((515, 209),)), 545 ("qCurveTo", ((569, 146), (695, 44), (835, -32), (913, -57))), 546 ("closePath", ()), 547 ("moveTo", ((474, 274),)), 548 ("lineTo", ((452, 284),)), 549 ( 550 "qCurveTo", 551 ( 552 (428, 260), 553 (377, 214), 554 (323, 172), 555 (266, 135), 556 (206, 101), 557 (144, 71), 558 (80, 46), 559 (47, 36), 560 ), 561 ), 562 ("lineTo", ((89, -53),)), 563 ("qCurveTo", ((163, -29), (299, 46), (423, 142), (476, 201))), 564 ("lineTo", ((498, 196),)), 565 ("qCurveTo", ((498, 203), (494, 225), (482, 255), (474, 274))), 566 ("closePath", ()), 567 ("moveTo", ((450, 250),)), 568 ("lineTo", ((550, 250),)), 569 ("lineTo", ((550, 379),)), 570 ("lineTo", ((450, 379),)), 571 ("closePath", ()), 572 ("moveTo", ((68, 215),)), 573 ("lineTo", ((932, 215),)), 574 ("lineTo", ((932, 305),)), 575 ("lineTo", ((68, 305),)), 576 ("closePath", ()), 577 ("moveTo", ((450, -71),)), 578 ("lineTo", ((550, -71),)), 579 ("lineTo", ((550, -71),)), 580 ("lineTo", ((550, 267),)), 581 ("lineTo", ((450, 267),)), 582 ("lineTo", ((450, -71),)), 583 ("closePath", ()), 584 ] 585 586 actual = [ 587 (op, tuple((otRound(pt[0]), otRound(pt[1])) for pt in args)) 588 for op, args in actual 589 ] 590 591 assert actual == expected, (actual, expected) 592 593 pen = RecordingPointPen() 594 glyph.drawPoints(pen) 595 assert pen.value 596 597 def test_cubic_glyf(self): 598 font = TTFont(self.getpath("dot-cubic.ttf")) 599 glyphset = font.getGlyphSet() 600 601 expected = [ 602 ("moveTo", ((76, 181),)), 603 ("curveTo", ((103, 181), (125, 158), (125, 131))), 604 ("curveTo", ((125, 104), (103, 82), (76, 82))), 605 ("curveTo", ((48, 82), (26, 104), (26, 131))), 606 ("curveTo", ((26, 158), (48, 181), (76, 181))), 607 ("closePath", ()), 608 ] 609 610 pen = RecordingPen() 611 glyphset["one"].draw(pen) 612 assert pen.value == expected 613 614 expectedPoints = [ 615 ("beginPath", (), {}), 616 ("addPoint", ((76, 181), "curve", False, None), {}), 617 ("addPoint", ((103, 181), None, False, None), {}), 618 ("addPoint", ((125, 158), None, False, None), {}), 619 ("addPoint", ((125, 104), None, False, None), {}), 620 ("addPoint", ((103, 82), None, False, None), {}), 621 ("addPoint", ((76, 82), "curve", False, None), {}), 622 ("addPoint", ((48, 82), None, False, None), {}), 623 ("addPoint", ((26, 104), None, False, None), {}), 624 ("addPoint", ((26, 158), None, False, None), {}), 625 ("addPoint", ((48, 181), None, False, None), {}), 626 ("endPath", (), {}), 627 ] 628 pen = RecordingPointPen() 629 glyphset["one"].drawPoints(pen) 630 assert pen.value == expectedPoints 631 632 pen = RecordingPen() 633 glyphset["two"].draw(pen) 634 assert pen.value == expected 635 636 expectedPoints = [ 637 ("beginPath", (), {}), 638 ("addPoint", ((26, 158), None, False, None), {}), 639 ("addPoint", ((48, 181), None, False, None), {}), 640 ("addPoint", ((76, 181), "curve", False, None), {}), 641 ("addPoint", ((103, 181), None, False, None), {}), 642 ("addPoint", ((125, 158), None, False, None), {}), 643 ("addPoint", ((125, 104), None, False, None), {}), 644 ("addPoint", ((103, 82), None, False, None), {}), 645 ("addPoint", ((76, 82), "curve", False, None), {}), 646 ("addPoint", ((48, 82), None, False, None), {}), 647 ("addPoint", ((26, 104), None, False, None), {}), 648 ("endPath", (), {}), 649 ] 650 pen = RecordingPointPen() 651 glyphset["two"].drawPoints(pen) 652 assert pen.value == expectedPoints 653 654 pen = RecordingPen() 655 glyphset["three"].draw(pen) 656 assert pen.value == expected 657 658 expectedPoints = [ 659 ("beginPath", (), {}), 660 ("addPoint", ((48, 82), None, False, None), {}), 661 ("addPoint", ((26, 104), None, False, None), {}), 662 ("addPoint", ((26, 158), None, False, None), {}), 663 ("addPoint", ((48, 181), None, False, None), {}), 664 ("addPoint", ((76, 181), "curve", False, None), {}), 665 ("addPoint", ((103, 181), None, False, None), {}), 666 ("addPoint", ((125, 158), None, False, None), {}), 667 ("addPoint", ((125, 104), None, False, None), {}), 668 ("addPoint", ((103, 82), None, False, None), {}), 669 ("addPoint", ((76, 82), "curve", False, None), {}), 670 ("endPath", (), {}), 671 ] 672 pen = RecordingPointPen() 673 glyphset["three"].drawPoints(pen) 674 assert pen.value == expectedPoints 675 676 pen = RecordingPen() 677 glyphset["four"].draw(pen) 678 assert pen.value == [ 679 ("moveTo", ((75.5, 181),)), 680 ("curveTo", ((103, 181), (125, 158), (125, 131))), 681 ("curveTo", ((125, 104), (103, 82), (75.5, 82))), 682 ("curveTo", ((48, 82), (26, 104), (26, 131))), 683 ("curveTo", ((26, 158), (48, 181), (75.5, 181))), 684 ("closePath", ()), 685 ] 686 687 # Ouch! We can't represent all-cubic-offcurves in pointPen! 688 # https://github.com/fonttools/fonttools/issues/3191 689 expectedPoints = [ 690 ("beginPath", (), {}), 691 ("addPoint", ((103, 181), None, False, None), {}), 692 ("addPoint", ((125, 158), None, False, None), {}), 693 ("addPoint", ((125, 104), None, False, None), {}), 694 ("addPoint", ((103, 82), None, False, None), {}), 695 ("addPoint", ((48, 82), None, False, None), {}), 696 ("addPoint", ((26, 104), None, False, None), {}), 697 ("addPoint", ((26, 158), None, False, None), {}), 698 ("addPoint", ((48, 181), None, False, None), {}), 699 ("endPath", (), {}), 700 ] 701 pen = RecordingPointPen() 702 glyphset["four"].drawPoints(pen) 703 print(pen.value) 704 assert pen.value == expectedPoints 705