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