1from fontTools.misc.testTools import parseXML 2from fontTools.misc.timeTools import timestampSinceEpoch 3from fontTools.ttLib import TTFont, TTLibError 4from fontTools import ttx 5import getopt 6import logging 7import os 8import shutil 9import sys 10import tempfile 11import unittest 12 13import pytest 14 15try: 16 import zopfli 17except ImportError: 18 zopfli = None 19try: 20 try: 21 import brotlicffi as brotli 22 except ImportError: 23 import brotli 24except ImportError: 25 brotli = None 26 27 28class TTXTest(unittest.TestCase): 29 30 def __init__(self, methodName): 31 unittest.TestCase.__init__(self, methodName) 32 # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 33 # and fires deprecation warnings if a program uses the old name. 34 if not hasattr(self, "assertRaisesRegex"): 35 self.assertRaisesRegex = self.assertRaisesRegexp 36 37 def setUp(self): 38 self.tempdir = None 39 self.num_tempfiles = 0 40 41 def tearDown(self): 42 if self.tempdir: 43 shutil.rmtree(self.tempdir) 44 45 @staticmethod 46 def getpath(testfile): 47 path, _ = os.path.split(__file__) 48 return os.path.join(path, "data", testfile) 49 50 def temp_dir(self): 51 if not self.tempdir: 52 self.tempdir = tempfile.mkdtemp() 53 54 def temp_font(self, font_path, file_name): 55 self.temp_dir() 56 temppath = os.path.join(self.tempdir, file_name) 57 shutil.copy2(font_path, temppath) 58 return temppath 59 60 @staticmethod 61 def read_file(file_path): 62 with open(file_path, "r", encoding="utf-8") as f: 63 return f.readlines() 64 65 # ----- 66 # Tests 67 # ----- 68 69 def test_parseOptions_no_args(self): 70 with self.assertRaises(getopt.GetoptError) as cm: 71 ttx.parseOptions([]) 72 self.assertTrue( 73 "Must specify at least one input file" in str(cm.exception) 74 ) 75 76 def test_parseOptions_invalid_path(self): 77 file_path = "invalid_font_path" 78 with self.assertRaises(getopt.GetoptError) as cm: 79 ttx.parseOptions([file_path]) 80 self.assertTrue('File not found: "%s"' % file_path in str(cm.exception)) 81 82 def test_parseOptions_font2ttx_1st_time(self): 83 file_name = "TestOTF.otf" 84 font_path = self.getpath(file_name) 85 temp_path = self.temp_font(font_path, file_name) 86 jobs, _ = ttx.parseOptions([temp_path]) 87 self.assertEqual(jobs[0][0].__name__, "ttDump") 88 self.assertEqual( 89 jobs[0][1:], 90 ( 91 os.path.join(self.tempdir, file_name), 92 os.path.join(self.tempdir, file_name.split(".")[0] + ".ttx"), 93 ), 94 ) 95 96 def test_parseOptions_font2ttx_2nd_time(self): 97 file_name = "TestTTF.ttf" 98 font_path = self.getpath(file_name) 99 temp_path = self.temp_font(font_path, file_name) 100 _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake 101 jobs, _ = ttx.parseOptions([temp_path]) 102 self.assertEqual(jobs[0][0].__name__, "ttDump") 103 self.assertEqual( 104 jobs[0][1:], 105 ( 106 os.path.join(self.tempdir, file_name), 107 os.path.join(self.tempdir, file_name.split(".")[0] + "#1.ttx"), 108 ), 109 ) 110 111 def test_parseOptions_ttx2font_1st_time(self): 112 file_name = "TestTTF.ttx" 113 font_path = self.getpath(file_name) 114 temp_path = self.temp_font(font_path, file_name) 115 jobs, _ = ttx.parseOptions([temp_path]) 116 self.assertEqual(jobs[0][0].__name__, "ttCompile") 117 self.assertEqual( 118 jobs[0][1:], 119 ( 120 os.path.join(self.tempdir, file_name), 121 os.path.join(self.tempdir, file_name.split(".")[0] + ".ttf"), 122 ), 123 ) 124 125 def test_parseOptions_ttx2font_2nd_time(self): 126 file_name = "TestOTF.ttx" 127 font_path = self.getpath(file_name) 128 temp_path = self.temp_font(font_path, file_name) 129 _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake 130 jobs, _ = ttx.parseOptions([temp_path]) 131 self.assertEqual(jobs[0][0].__name__, "ttCompile") 132 self.assertEqual( 133 jobs[0][1:], 134 ( 135 os.path.join(self.tempdir, file_name), 136 os.path.join(self.tempdir, file_name.split(".")[0] + "#1.otf"), 137 ), 138 ) 139 140 def test_parseOptions_multiple_fonts(self): 141 file_names = ["TestOTF.otf", "TestTTF.ttf"] 142 font_paths = [self.getpath(file_name) for file_name in file_names] 143 temp_paths = [ 144 self.temp_font(font_path, file_name) 145 for font_path, file_name in zip(font_paths, file_names) 146 ] 147 jobs, _ = ttx.parseOptions(temp_paths) 148 for i in range(len(jobs)): 149 self.assertEqual(jobs[i][0].__name__, "ttDump") 150 self.assertEqual( 151 jobs[i][1:], 152 ( 153 os.path.join(self.tempdir, file_names[i]), 154 os.path.join( 155 self.tempdir, file_names[i].split(".")[0] + ".ttx" 156 ), 157 ), 158 ) 159 160 def test_parseOptions_mixed_files(self): 161 operations = ["ttDump", "ttCompile"] 162 extensions = [".ttx", ".ttf"] 163 file_names = ["TestOTF.otf", "TestTTF.ttx"] 164 font_paths = [self.getpath(file_name) for file_name in file_names] 165 temp_paths = [ 166 self.temp_font(font_path, file_name) 167 for font_path, file_name in zip(font_paths, file_names) 168 ] 169 jobs, _ = ttx.parseOptions(temp_paths) 170 for i in range(len(jobs)): 171 self.assertEqual(jobs[i][0].__name__, operations[i]) 172 self.assertEqual( 173 jobs[i][1:], 174 ( 175 os.path.join(self.tempdir, file_names[i]), 176 os.path.join( 177 self.tempdir, 178 file_names[i].split(".")[0] + extensions[i], 179 ), 180 ), 181 ) 182 183 def test_parseOptions_splitTables(self): 184 file_name = "TestTTF.ttf" 185 font_path = self.getpath(file_name) 186 temp_path = self.temp_font(font_path, file_name) 187 args = ["-s", temp_path] 188 189 jobs, options = ttx.parseOptions(args) 190 191 ttx_file_path = jobs[0][2] 192 temp_folder = os.path.dirname(ttx_file_path) 193 self.assertTrue(options.splitTables) 194 self.assertTrue(os.path.exists(ttx_file_path)) 195 196 ttx.process(jobs, options) 197 198 # Read the TTX file but strip the first two and the last lines: 199 # <?xml version="1.0" encoding="UTF-8"?> 200 # <ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.22"> 201 # ... 202 # </ttFont> 203 parsed_xml = parseXML(self.read_file(ttx_file_path)[2:-1]) 204 for item in parsed_xml: 205 if not isinstance(item, tuple): 206 continue 207 # the tuple looks like this: 208 # (u'head', {u'src': u'TestTTF._h_e_a_d.ttx'}, []) 209 table_file_name = item[1].get("src") 210 table_file_path = os.path.join(temp_folder, table_file_name) 211 self.assertTrue(os.path.exists(table_file_path)) 212 213 def test_parseOptions_splitGlyphs(self): 214 file_name = "TestTTF.ttf" 215 font_path = self.getpath(file_name) 216 temp_path = self.temp_font(font_path, file_name) 217 args = ["-g", temp_path] 218 219 jobs, options = ttx.parseOptions(args) 220 221 ttx_file_path = jobs[0][2] 222 temp_folder = os.path.dirname(ttx_file_path) 223 self.assertTrue(options.splitGlyphs) 224 # splitGlyphs also forces splitTables 225 self.assertTrue(options.splitTables) 226 self.assertTrue(os.path.exists(ttx_file_path)) 227 228 ttx.process(jobs, options) 229 230 # Read the TTX file but strip the first two and the last lines: 231 # <?xml version="1.0" encoding="UTF-8"?> 232 # <ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.22"> 233 # ... 234 # </ttFont> 235 for item in parseXML(self.read_file(ttx_file_path)[2:-1]): 236 if not isinstance(item, tuple): 237 continue 238 # the tuple looks like this: 239 # (u'head', {u'src': u'TestTTF._h_e_a_d.ttx'}, []) 240 table_tag = item[0] 241 table_file_name = item[1].get("src") 242 table_file_path = os.path.join(temp_folder, table_file_name) 243 self.assertTrue(os.path.exists(table_file_path)) 244 if table_tag != "glyf": 245 continue 246 # also strip the enclosing 'glyf' element 247 for item in parseXML(self.read_file(table_file_path)[4:-3]): 248 if not isinstance(item, tuple): 249 continue 250 # glyphs without outline data only have 'name' attribute 251 glyph_file_name = item[1].get("src") 252 if glyph_file_name is not None: 253 glyph_file_path = os.path.join(temp_folder, glyph_file_name) 254 self.assertTrue(os.path.exists(glyph_file_path)) 255 256 def test_guessFileType_ttf(self): 257 file_name = "TestTTF.ttf" 258 font_path = self.getpath(file_name) 259 self.assertEqual(ttx.guessFileType(font_path), "TTF") 260 261 def test_guessFileType_otf(self): 262 file_name = "TestOTF.otf" 263 font_path = self.getpath(file_name) 264 self.assertEqual(ttx.guessFileType(font_path), "OTF") 265 266 def test_guessFileType_woff(self): 267 file_name = "TestWOFF.woff" 268 font_path = self.getpath(file_name) 269 self.assertEqual(ttx.guessFileType(font_path), "WOFF") 270 271 def test_guessFileType_woff2(self): 272 file_name = "TestWOFF2.woff2" 273 font_path = self.getpath(file_name) 274 self.assertEqual(ttx.guessFileType(font_path), "WOFF2") 275 276 def test_guessFileType_ttc(self): 277 file_name = "TestTTC.ttc" 278 font_path = self.getpath(file_name) 279 self.assertEqual(ttx.guessFileType(font_path), "TTC") 280 281 def test_guessFileType_dfont(self): 282 file_name = "TestDFONT.dfont" 283 font_path = self.getpath(file_name) 284 self.assertEqual(ttx.guessFileType(font_path), "TTF") 285 286 def test_guessFileType_ttx_ttf(self): 287 file_name = "TestTTF.ttx" 288 font_path = self.getpath(file_name) 289 self.assertEqual(ttx.guessFileType(font_path), "TTX") 290 291 def test_guessFileType_ttx_otf(self): 292 file_name = "TestOTF.ttx" 293 font_path = self.getpath(file_name) 294 self.assertEqual(ttx.guessFileType(font_path), "OTX") 295 296 def test_guessFileType_ttx_bom(self): 297 file_name = "TestBOM.ttx" 298 font_path = self.getpath(file_name) 299 self.assertEqual(ttx.guessFileType(font_path), "TTX") 300 301 def test_guessFileType_ttx_no_sfntVersion(self): 302 file_name = "TestNoSFNT.ttx" 303 font_path = self.getpath(file_name) 304 self.assertEqual(ttx.guessFileType(font_path), "TTX") 305 306 def test_guessFileType_ttx_no_xml(self): 307 file_name = "TestNoXML.ttx" 308 font_path = self.getpath(file_name) 309 self.assertIsNone(ttx.guessFileType(font_path)) 310 311 def test_guessFileType_invalid_path(self): 312 font_path = "invalid_font_path" 313 self.assertIsNone(ttx.guessFileType(font_path)) 314 315 316# ----------------------- 317# ttx.Options class tests 318# ----------------------- 319 320 321def test_options_flag_h(capsys): 322 with pytest.raises(SystemExit): 323 ttx.Options([("-h", None)], 1) 324 325 out, err = capsys.readouterr() 326 assert "TTX -- From OpenType To XML And Back" in out 327 328 329def test_options_flag_version(capsys): 330 with pytest.raises(SystemExit): 331 ttx.Options([("--version", None)], 1) 332 333 out, err = capsys.readouterr() 334 version_list = out.split(".") 335 assert len(version_list) >= 3 336 assert version_list[0].isdigit() 337 assert version_list[1].isdigit() 338 assert version_list[2].strip().isdigit() 339 340 341def test_options_d_goodpath(tmpdir): 342 temp_dir_path = str(tmpdir) 343 tto = ttx.Options([("-d", temp_dir_path)], 1) 344 assert tto.outputDir == temp_dir_path 345 346 347def test_options_d_badpath(): 348 with pytest.raises(getopt.GetoptError): 349 ttx.Options([("-d", "bogusdir")], 1) 350 351 352def test_options_o(): 353 tto = ttx.Options([("-o", "testfile.ttx")], 1) 354 assert tto.outputFile == "testfile.ttx" 355 356 357def test_options_f(): 358 tto = ttx.Options([("-f", "")], 1) 359 assert tto.overWrite is True 360 361 362def test_options_v(): 363 tto = ttx.Options([("-v", "")], 1) 364 assert tto.verbose is True 365 assert tto.logLevel == logging.DEBUG 366 367 368def test_options_q(): 369 tto = ttx.Options([("-q", "")], 1) 370 assert tto.quiet is True 371 assert tto.logLevel == logging.WARNING 372 373 374def test_options_l(): 375 tto = ttx.Options([("-l", "")], 1) 376 assert tto.listTables is True 377 378 379def test_options_t_nopadding(): 380 tto = ttx.Options([("-t", "CFF2")], 1) 381 assert len(tto.onlyTables) == 1 382 assert tto.onlyTables[0] == "CFF2" 383 384 385def test_options_t_withpadding(): 386 tto = ttx.Options([("-t", "CFF")], 1) 387 assert len(tto.onlyTables) == 1 388 assert tto.onlyTables[0] == "CFF " 389 390 391def test_options_s(): 392 tto = ttx.Options([("-s", "")], 1) 393 assert tto.splitTables is True 394 assert tto.splitGlyphs is False 395 396 397def test_options_g(): 398 tto = ttx.Options([("-g", "")], 1) 399 assert tto.splitGlyphs is True 400 assert tto.splitTables is True 401 402 403def test_options_i(): 404 tto = ttx.Options([("-i", "")], 1) 405 assert tto.disassembleInstructions is False 406 407 408def test_options_z_validoptions(): 409 valid_options = ("raw", "row", "bitwise", "extfile") 410 for option in valid_options: 411 tto = ttx.Options([("-z", option)], 1) 412 assert tto.bitmapGlyphDataFormat == option 413 414 415def test_options_z_invalidoption(): 416 with pytest.raises(getopt.GetoptError): 417 ttx.Options([("-z", "bogus")], 1) 418 419 420def test_options_y_validvalue(): 421 tto = ttx.Options([("-y", "1")], 1) 422 assert tto.fontNumber == 1 423 424 425def test_options_y_invalidvalue(): 426 with pytest.raises(ValueError): 427 ttx.Options([("-y", "A")], 1) 428 429 430def test_options_m(): 431 tto = ttx.Options([("-m", "testfont.ttf")], 1) 432 assert tto.mergeFile == "testfont.ttf" 433 434 435def test_options_b(): 436 tto = ttx.Options([("-b", "")], 1) 437 assert tto.recalcBBoxes is False 438 439 440def test_options_a(): 441 tto = ttx.Options([("-a", "")], 1) 442 assert tto.allowVID is True 443 444 445def test_options_e(): 446 tto = ttx.Options([("-e", "")], 1) 447 assert tto.ignoreDecompileErrors is False 448 449 450def test_options_unicodedata(): 451 tto = ttx.Options([("--unicodedata", "UnicodeData.txt")], 1) 452 assert tto.unicodedata == "UnicodeData.txt" 453 454 455def test_options_newline_lf(): 456 tto = ttx.Options([("--newline", "LF")], 1) 457 assert tto.newlinestr == "\n" 458 459 460def test_options_newline_cr(): 461 tto = ttx.Options([("--newline", "CR")], 1) 462 assert tto.newlinestr == "\r" 463 464 465def test_options_newline_crlf(): 466 tto = ttx.Options([("--newline", "CRLF")], 1) 467 assert tto.newlinestr == "\r\n" 468 469 470def test_options_newline_invalid(): 471 with pytest.raises(getopt.GetoptError): 472 ttx.Options([("--newline", "BOGUS")], 1) 473 474 475def test_options_recalc_timestamp(): 476 tto = ttx.Options([("--recalc-timestamp", "")], 1) 477 assert tto.recalcTimestamp is True 478 479 480def test_options_recalc_timestamp(): 481 tto = ttx.Options([("--no-recalc-timestamp", "")], 1) 482 assert tto.recalcTimestamp is False 483 484 485def test_options_flavor(): 486 tto = ttx.Options([("--flavor", "woff")], 1) 487 assert tto.flavor == "woff" 488 489 490def test_options_with_zopfli(): 491 tto = ttx.Options([("--with-zopfli", ""), ("--flavor", "woff")], 1) 492 assert tto.useZopfli is True 493 494 495def test_options_with_zopfli_fails_without_woff_flavor(): 496 with pytest.raises(getopt.GetoptError): 497 ttx.Options([("--with-zopfli", "")], 1) 498 499 500def test_options_quiet_and_verbose_shouldfail(): 501 with pytest.raises(getopt.GetoptError): 502 ttx.Options([("-q", ""), ("-v", "")], 1) 503 504 505def test_options_mergefile_and_flavor_shouldfail(): 506 with pytest.raises(getopt.GetoptError): 507 ttx.Options([("-m", "testfont.ttf"), ("--flavor", "woff")], 1) 508 509 510def test_options_onlytables_and_skiptables_shouldfail(): 511 with pytest.raises(getopt.GetoptError): 512 ttx.Options([("-t", "CFF"), ("-x", "CFF2")], 1) 513 514 515def test_options_mergefile_and_multiplefiles_shouldfail(): 516 with pytest.raises(getopt.GetoptError): 517 ttx.Options([("-m", "testfont.ttf")], 2) 518 519 520def test_options_woff2_and_zopfli_shouldfail(): 521 with pytest.raises(getopt.GetoptError): 522 ttx.Options([("--with-zopfli", ""), ("--flavor", "woff2")], 1) 523 524 525# ---------------------------- 526# ttx.ttCompile function tests 527# ---------------------------- 528 529 530def test_ttcompile_otf_compile_default(tmpdir): 531 inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") 532 # outotf = os.path.join(str(tmpdir), "TestOTF.otf") 533 outotf = tmpdir.join("TestOTF.ttx") 534 default_options = ttx.Options([], 1) 535 ttx.ttCompile(inttx, str(outotf), default_options) 536 # confirm that font was built 537 assert outotf.check(file=True) 538 # confirm that it is valid OTF file, can instantiate a TTFont, has expected OpenType tables 539 ttf = TTFont(str(outotf)) 540 expected_tables = ( 541 "head", 542 "hhea", 543 "maxp", 544 "OS/2", 545 "name", 546 "cmap", 547 "post", 548 "CFF ", 549 "hmtx", 550 "DSIG", 551 ) 552 for table in expected_tables: 553 assert table in ttf 554 555 556def test_ttcompile_otf_to_woff_without_zopfli(tmpdir): 557 inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") 558 outwoff = tmpdir.join("TestOTF.woff") 559 options = ttx.Options([], 1) 560 options.flavor = "woff" 561 ttx.ttCompile(inttx, str(outwoff), options) 562 # confirm that font was built 563 assert outwoff.check(file=True) 564 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 565 ttf = TTFont(str(outwoff)) 566 expected_tables = ( 567 "head", 568 "hhea", 569 "maxp", 570 "OS/2", 571 "name", 572 "cmap", 573 "post", 574 "CFF ", 575 "hmtx", 576 "DSIG", 577 ) 578 for table in expected_tables: 579 assert table in ttf 580 581 582@pytest.mark.skipif(zopfli is None, reason="zopfli not installed") 583def test_ttcompile_otf_to_woff_with_zopfli(tmpdir): 584 inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") 585 outwoff = tmpdir.join("TestOTF.woff") 586 options = ttx.Options([], 1) 587 options.flavor = "woff" 588 options.useZopfli = True 589 ttx.ttCompile(inttx, str(outwoff), options) 590 # confirm that font was built 591 assert outwoff.check(file=True) 592 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 593 ttf = TTFont(str(outwoff)) 594 expected_tables = ( 595 "head", 596 "hhea", 597 "maxp", 598 "OS/2", 599 "name", 600 "cmap", 601 "post", 602 "CFF ", 603 "hmtx", 604 "DSIG", 605 ) 606 for table in expected_tables: 607 assert table in ttf 608 609 610@pytest.mark.skipif(brotli is None, reason="brotli not installed") 611def test_ttcompile_otf_to_woff2(tmpdir): 612 inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") 613 outwoff2 = tmpdir.join("TestTTF.woff2") 614 options = ttx.Options([], 1) 615 options.flavor = "woff2" 616 ttx.ttCompile(inttx, str(outwoff2), options) 617 # confirm that font was built 618 assert outwoff2.check(file=True) 619 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 620 ttf = TTFont(str(outwoff2)) 621 # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) 622 assert "DSIG" not in ttf 623 expected_tables = ( 624 "head", 625 "hhea", 626 "maxp", 627 "OS/2", 628 "name", 629 "cmap", 630 "post", 631 "CFF ", 632 "hmtx", 633 ) 634 for table in expected_tables: 635 assert table in ttf 636 637 638def test_ttcompile_ttf_compile_default(tmpdir): 639 inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 640 outttf = tmpdir.join("TestTTF.ttf") 641 default_options = ttx.Options([], 1) 642 ttx.ttCompile(inttx, str(outttf), default_options) 643 # confirm that font was built 644 assert outttf.check(file=True) 645 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 646 ttf = TTFont(str(outttf)) 647 expected_tables = ( 648 "head", 649 "hhea", 650 "maxp", 651 "OS/2", 652 "name", 653 "cmap", 654 "hmtx", 655 "fpgm", 656 "prep", 657 "cvt ", 658 "loca", 659 "glyf", 660 "post", 661 "gasp", 662 "DSIG", 663 ) 664 for table in expected_tables: 665 assert table in ttf 666 667 668def test_ttcompile_ttf_to_woff_without_zopfli(tmpdir): 669 inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 670 outwoff = tmpdir.join("TestTTF.woff") 671 options = ttx.Options([], 1) 672 options.flavor = "woff" 673 ttx.ttCompile(inttx, str(outwoff), options) 674 # confirm that font was built 675 assert outwoff.check(file=True) 676 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 677 ttf = TTFont(str(outwoff)) 678 expected_tables = ( 679 "head", 680 "hhea", 681 "maxp", 682 "OS/2", 683 "name", 684 "cmap", 685 "hmtx", 686 "fpgm", 687 "prep", 688 "cvt ", 689 "loca", 690 "glyf", 691 "post", 692 "gasp", 693 "DSIG", 694 ) 695 for table in expected_tables: 696 assert table in ttf 697 698 699@pytest.mark.skipif(zopfli is None, reason="zopfli not installed") 700def test_ttcompile_ttf_to_woff_with_zopfli(tmpdir): 701 inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 702 outwoff = tmpdir.join("TestTTF.woff") 703 options = ttx.Options([], 1) 704 options.flavor = "woff" 705 options.useZopfli = True 706 ttx.ttCompile(inttx, str(outwoff), options) 707 # confirm that font was built 708 assert outwoff.check(file=True) 709 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 710 ttf = TTFont(str(outwoff)) 711 expected_tables = ( 712 "head", 713 "hhea", 714 "maxp", 715 "OS/2", 716 "name", 717 "cmap", 718 "hmtx", 719 "fpgm", 720 "prep", 721 "cvt ", 722 "loca", 723 "glyf", 724 "post", 725 "gasp", 726 "DSIG", 727 ) 728 for table in expected_tables: 729 assert table in ttf 730 731 732@pytest.mark.skipif(brotli is None, reason="brotli not installed") 733def test_ttcompile_ttf_to_woff2(tmpdir): 734 inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 735 outwoff2 = tmpdir.join("TestTTF.woff2") 736 options = ttx.Options([], 1) 737 options.flavor = "woff2" 738 ttx.ttCompile(inttx, str(outwoff2), options) 739 # confirm that font was built 740 assert outwoff2.check(file=True) 741 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 742 ttf = TTFont(str(outwoff2)) 743 # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) 744 assert "DSIG" not in ttf 745 expected_tables = ( 746 "head", 747 "hhea", 748 "maxp", 749 "OS/2", 750 "name", 751 "cmap", 752 "hmtx", 753 "fpgm", 754 "prep", 755 "cvt ", 756 "loca", 757 "glyf", 758 "post", 759 "gasp", 760 ) 761 for table in expected_tables: 762 assert table in ttf 763 764 765@pytest.mark.parametrize( 766 "inpath, outpath1, outpath2", 767 [ 768 ("TestTTF.ttx", "TestTTF1.ttf", "TestTTF2.ttf"), 769 ("TestOTF.ttx", "TestOTF1.otf", "TestOTF2.otf"), 770 ], 771) 772def test_ttcompile_timestamp_calcs(inpath, outpath1, outpath2, tmpdir): 773 inttx = os.path.join("Tests", "ttx", "data", inpath) 774 outttf1 = tmpdir.join(outpath1) 775 outttf2 = tmpdir.join(outpath2) 776 options = ttx.Options([], 1) 777 # build with default options = do not recalculate timestamp 778 ttx.ttCompile(inttx, str(outttf1), options) 779 # confirm that font was built 780 assert outttf1.check(file=True) 781 # confirm that timestamp is same as modified time on ttx file 782 mtime = os.path.getmtime(inttx) 783 epochtime = timestampSinceEpoch(mtime) 784 ttf = TTFont(str(outttf1)) 785 assert ttf["head"].modified == epochtime 786 787 # reset options to recalculate the timestamp and compile new font 788 options.recalcTimestamp = True 789 ttx.ttCompile(inttx, str(outttf2), options) 790 # confirm that font was built 791 assert outttf2.check(file=True) 792 # confirm that timestamp is more recent than modified time on ttx file 793 mtime = os.path.getmtime(inttx) 794 epochtime = timestampSinceEpoch(mtime) 795 ttf = TTFont(str(outttf2)) 796 assert ttf["head"].modified > epochtime 797 798 # --no-recalc-timestamp will keep original timestamp 799 options.recalcTimestamp = False 800 ttx.ttCompile(inttx, str(outttf2), options) 801 assert outttf2.check(file=True) 802 inttf = TTFont() 803 inttf.importXML(inttx) 804 assert inttf["head"].modified == TTFont(str(outttf2))["head"].modified 805 806 807# ------------------------- 808# ttx.ttList function tests 809# ------------------------- 810 811 812def test_ttlist_ttf(capsys, tmpdir): 813 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") 814 fakeoutpath = tmpdir.join("TestTTF.ttx") 815 options = ttx.Options([], 1) 816 options.listTables = True 817 ttx.ttList(inpath, str(fakeoutpath), options) 818 out, err = capsys.readouterr() 819 expected_tables = ( 820 "head", 821 "hhea", 822 "maxp", 823 "OS/2", 824 "name", 825 "cmap", 826 "hmtx", 827 "fpgm", 828 "prep", 829 "cvt ", 830 "loca", 831 "glyf", 832 "post", 833 "gasp", 834 "DSIG", 835 ) 836 # confirm that expected tables are printed to stdout 837 for table in expected_tables: 838 assert table in out 839 # test for one of the expected tag/checksum/length/offset strings 840 assert "OS/2 0x67230FF8 96 376" in out 841 842 843def test_ttlist_otf(capsys, tmpdir): 844 inpath = os.path.join("Tests", "ttx", "data", "TestOTF.otf") 845 fakeoutpath = tmpdir.join("TestOTF.ttx") 846 options = ttx.Options([], 1) 847 options.listTables = True 848 ttx.ttList(inpath, str(fakeoutpath), options) 849 out, err = capsys.readouterr() 850 expected_tables = ( 851 "head", 852 "hhea", 853 "maxp", 854 "OS/2", 855 "name", 856 "cmap", 857 "post", 858 "CFF ", 859 "hmtx", 860 "DSIG", 861 ) 862 # confirm that expected tables are printed to stdout 863 for table in expected_tables: 864 assert table in out 865 # test for one of the expected tag/checksum/length/offset strings 866 assert "OS/2 0x67230FF8 96 272" in out 867 868 869def test_ttlist_woff(capsys, tmpdir): 870 inpath = os.path.join("Tests", "ttx", "data", "TestWOFF.woff") 871 fakeoutpath = tmpdir.join("TestWOFF.ttx") 872 options = ttx.Options([], 1) 873 options.listTables = True 874 options.flavor = "woff" 875 ttx.ttList(inpath, str(fakeoutpath), options) 876 out, err = capsys.readouterr() 877 expected_tables = ( 878 "head", 879 "hhea", 880 "maxp", 881 "OS/2", 882 "name", 883 "cmap", 884 "post", 885 "CFF ", 886 "hmtx", 887 "DSIG", 888 ) 889 # confirm that expected tables are printed to stdout 890 for table in expected_tables: 891 assert table in out 892 # test for one of the expected tag/checksum/length/offset strings 893 assert "OS/2 0x67230FF8 84 340" in out 894 895 896@pytest.mark.skipif(brotli is None, reason="brotli not installed") 897def test_ttlist_woff2(capsys, tmpdir): 898 inpath = os.path.join("Tests", "ttx", "data", "TestWOFF2.woff2") 899 fakeoutpath = tmpdir.join("TestWOFF2.ttx") 900 options = ttx.Options([], 1) 901 options.listTables = True 902 options.flavor = "woff2" 903 ttx.ttList(inpath, str(fakeoutpath), options) 904 out, err = capsys.readouterr() 905 expected_tables = ( 906 "head", 907 "hhea", 908 "maxp", 909 "OS/2", 910 "name", 911 "cmap", 912 "hmtx", 913 "fpgm", 914 "prep", 915 "cvt ", 916 "loca", 917 "glyf", 918 "post", 919 "gasp", 920 ) 921 # confirm that expected tables are printed to stdout 922 for table in expected_tables: 923 assert table in out 924 # test for one of the expected tag/checksum/length/offset strings 925 assert "OS/2 0x67230FF8 96 0" in out 926 927 928# ------------------- 929# main function tests 930# ------------------- 931 932 933def test_main_default_ttf_dump_to_ttx(tmpdir): 934 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") 935 outpath = tmpdir.join("TestTTF.ttx") 936 args = ["-o", str(outpath), inpath] 937 ttx.main(args) 938 assert outpath.check(file=True) 939 940 941def test_main_default_ttx_compile_to_ttf(tmpdir): 942 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 943 outpath = tmpdir.join("TestTTF.ttf") 944 args = ["-o", str(outpath), inpath] 945 ttx.main(args) 946 assert outpath.check(file=True) 947 948 949def test_main_getopterror_missing_directory(): 950 with pytest.raises(SystemExit): 951 with pytest.raises(getopt.GetoptError): 952 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") 953 args = ["-d", "bogusdir", inpath] 954 ttx.main(args) 955 956 957def test_main_keyboard_interrupt(tmpdir, monkeypatch, caplog): 958 with pytest.raises(SystemExit): 959 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 960 outpath = tmpdir.join("TestTTF.ttf") 961 args = ["-o", str(outpath), inpath] 962 monkeypatch.setattr( 963 ttx, "process", (lambda x, y: raise_exception(KeyboardInterrupt)) 964 ) 965 ttx.main(args) 966 967 assert "(Cancelled.)" in caplog.text 968 969 970@pytest.mark.skipif( 971 sys.platform == "win32", 972 reason="waitForKeyPress function causes test to hang on Windows platform", 973) 974def test_main_system_exit(tmpdir, monkeypatch): 975 with pytest.raises(SystemExit): 976 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 977 outpath = tmpdir.join("TestTTF.ttf") 978 args = ["-o", str(outpath), inpath] 979 monkeypatch.setattr( 980 ttx, "process", (lambda x, y: raise_exception(SystemExit)) 981 ) 982 ttx.main(args) 983 984 985def test_main_ttlib_error(tmpdir, monkeypatch, caplog): 986 with pytest.raises(SystemExit): 987 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 988 outpath = tmpdir.join("TestTTF.ttf") 989 args = ["-o", str(outpath), inpath] 990 monkeypatch.setattr( 991 ttx, 992 "process", 993 (lambda x, y: raise_exception(TTLibError("Test error"))), 994 ) 995 ttx.main(args) 996 997 assert "Test error" in caplog.text 998 999 1000@pytest.mark.skipif( 1001 sys.platform == "win32", 1002 reason="waitForKeyPress function causes test to hang on Windows platform", 1003) 1004def test_main_base_exception(tmpdir, monkeypatch, caplog): 1005 with pytest.raises(SystemExit): 1006 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 1007 outpath = tmpdir.join("TestTTF.ttf") 1008 args = ["-o", str(outpath), inpath] 1009 monkeypatch.setattr( 1010 ttx, 1011 "process", 1012 (lambda x, y: raise_exception(Exception("Test error"))), 1013 ) 1014 ttx.main(args) 1015 1016 assert "Unhandled exception has occurred" in caplog.text 1017 1018 1019# --------------------------- 1020# support functions for tests 1021# --------------------------- 1022 1023 1024def raise_exception(exception): 1025 raise exception 1026