1from fontTools.misc.testTools import parseXML 2from fontTools.misc.timeTools import timestampSinceEpoch 3from fontTools.ttLib import TTFont, TTLibError 4from fontTools.ttLib.tables.DefaultTable import DefaultTable 5from fontTools import ttx 6import base64 7import getopt 8import logging 9import os 10import shutil 11import subprocess 12import sys 13import tempfile 14import unittest 15from pathlib import Path 16 17import pytest 18 19try: 20 import zopfli 21except ImportError: 22 zopfli = None 23try: 24 try: 25 import brotlicffi as brotli 26 except ImportError: 27 import brotli 28except ImportError: 29 brotli = None 30 31 32class TTXTest(unittest.TestCase): 33 def __init__(self, methodName): 34 unittest.TestCase.__init__(self, methodName) 35 # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 36 # and fires deprecation warnings if a program uses the old name. 37 if not hasattr(self, "assertRaisesRegex"): 38 self.assertRaisesRegex = self.assertRaisesRegexp 39 40 def setUp(self): 41 self.tempdir = None 42 self.num_tempfiles = 0 43 44 def tearDown(self): 45 if self.tempdir: 46 shutil.rmtree(self.tempdir) 47 48 @staticmethod 49 def getpath(testfile): 50 path, _ = os.path.split(__file__) 51 return os.path.join(path, "data", testfile) 52 53 def temp_dir(self): 54 if not self.tempdir: 55 self.tempdir = tempfile.mkdtemp() 56 57 def temp_font(self, font_path, file_name): 58 self.temp_dir() 59 temppath = os.path.join(self.tempdir, file_name) 60 shutil.copy2(font_path, temppath) 61 return temppath 62 63 @staticmethod 64 def read_file(file_path): 65 with open(file_path, "r", encoding="utf-8") as f: 66 return f.readlines() 67 68 # ----- 69 # Tests 70 # ----- 71 72 def test_parseOptions_no_args(self): 73 with self.assertRaises(getopt.GetoptError) as cm: 74 ttx.parseOptions([]) 75 self.assertTrue("Must specify at least one input file" in str(cm.exception)) 76 77 def test_parseOptions_invalid_path(self): 78 file_path = "invalid_font_path" 79 with self.assertRaises(getopt.GetoptError) as cm: 80 ttx.parseOptions([file_path]) 81 self.assertTrue('File not found: "%s"' % file_path in str(cm.exception)) 82 83 def test_parseOptions_font2ttx_1st_time(self): 84 file_name = "TestOTF.otf" 85 font_path = self.getpath(file_name) 86 temp_path = self.temp_font(font_path, file_name) 87 jobs, _ = ttx.parseOptions([temp_path]) 88 self.assertEqual(jobs[0][0].__name__, "ttDump") 89 self.assertEqual( 90 jobs[0][1:], 91 ( 92 os.path.join(self.tempdir, file_name), 93 os.path.join(self.tempdir, file_name.split(".")[0] + ".ttx"), 94 ), 95 ) 96 97 def test_parseOptions_font2ttx_2nd_time(self): 98 file_name = "TestTTF.ttf" 99 font_path = self.getpath(file_name) 100 temp_path = self.temp_font(font_path, file_name) 101 _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake 102 jobs, _ = ttx.parseOptions([temp_path]) 103 self.assertEqual(jobs[0][0].__name__, "ttDump") 104 self.assertEqual( 105 jobs[0][1:], 106 ( 107 os.path.join(self.tempdir, file_name), 108 os.path.join(self.tempdir, file_name.split(".")[0] + "#1.ttx"), 109 ), 110 ) 111 112 def test_parseOptions_ttx2font_1st_time(self): 113 file_name = "TestTTF.ttx" 114 font_path = self.getpath(file_name) 115 temp_path = self.temp_font(font_path, file_name) 116 jobs, _ = ttx.parseOptions([temp_path]) 117 self.assertEqual(jobs[0][0].__name__, "ttCompile") 118 self.assertEqual( 119 jobs[0][1:], 120 ( 121 os.path.join(self.tempdir, file_name), 122 os.path.join(self.tempdir, file_name.split(".")[0] + ".ttf"), 123 ), 124 ) 125 126 def test_parseOptions_ttx2font_2nd_time(self): 127 file_name = "TestOTF.ttx" 128 font_path = self.getpath(file_name) 129 temp_path = self.temp_font(font_path, file_name) 130 _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake 131 jobs, _ = ttx.parseOptions([temp_path]) 132 self.assertEqual(jobs[0][0].__name__, "ttCompile") 133 self.assertEqual( 134 jobs[0][1:], 135 ( 136 os.path.join(self.tempdir, file_name), 137 os.path.join(self.tempdir, file_name.split(".")[0] + "#1.otf"), 138 ), 139 ) 140 141 def test_parseOptions_multiple_fonts(self): 142 file_names = ["TestOTF.otf", "TestTTF.ttf"] 143 font_paths = [self.getpath(file_name) for file_name in file_names] 144 temp_paths = [ 145 self.temp_font(font_path, file_name) 146 for font_path, file_name in zip(font_paths, file_names) 147 ] 148 jobs, _ = ttx.parseOptions(temp_paths) 149 for i in range(len(jobs)): 150 self.assertEqual(jobs[i][0].__name__, "ttDump") 151 self.assertEqual( 152 jobs[i][1:], 153 ( 154 os.path.join(self.tempdir, file_names[i]), 155 os.path.join(self.tempdir, file_names[i].split(".")[0] + ".ttx"), 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_e(): 440 tto = ttx.Options([("-e", "")], 1) 441 assert tto.ignoreDecompileErrors is False 442 443 444def test_options_unicodedata(): 445 tto = ttx.Options([("--unicodedata", "UnicodeData.txt")], 1) 446 assert tto.unicodedata == "UnicodeData.txt" 447 448 449def test_options_newline_lf(): 450 tto = ttx.Options([("--newline", "LF")], 1) 451 assert tto.newlinestr == "\n" 452 453 454def test_options_newline_cr(): 455 tto = ttx.Options([("--newline", "CR")], 1) 456 assert tto.newlinestr == "\r" 457 458 459def test_options_newline_crlf(): 460 tto = ttx.Options([("--newline", "CRLF")], 1) 461 assert tto.newlinestr == "\r\n" 462 463 464def test_options_newline_invalid(): 465 with pytest.raises(getopt.GetoptError): 466 ttx.Options([("--newline", "BOGUS")], 1) 467 468 469def test_options_recalc_timestamp(): 470 tto = ttx.Options([("--recalc-timestamp", "")], 1) 471 assert tto.recalcTimestamp is True 472 473 474def test_options_recalc_timestamp(): 475 tto = ttx.Options([("--no-recalc-timestamp", "")], 1) 476 assert tto.recalcTimestamp is False 477 478 479def test_options_flavor(): 480 tto = ttx.Options([("--flavor", "woff")], 1) 481 assert tto.flavor == "woff" 482 483 484def test_options_with_zopfli(): 485 tto = ttx.Options([("--with-zopfli", ""), ("--flavor", "woff")], 1) 486 assert tto.useZopfli is True 487 488 489def test_options_with_zopfli_fails_without_woff_flavor(): 490 with pytest.raises(getopt.GetoptError): 491 ttx.Options([("--with-zopfli", "")], 1) 492 493 494def test_options_quiet_and_verbose_shouldfail(): 495 with pytest.raises(getopt.GetoptError): 496 ttx.Options([("-q", ""), ("-v", "")], 1) 497 498 499def test_options_mergefile_and_flavor_shouldfail(): 500 with pytest.raises(getopt.GetoptError): 501 ttx.Options([("-m", "testfont.ttf"), ("--flavor", "woff")], 1) 502 503 504def test_options_onlytables_and_skiptables_shouldfail(): 505 with pytest.raises(getopt.GetoptError): 506 ttx.Options([("-t", "CFF"), ("-x", "CFF2")], 1) 507 508 509def test_options_mergefile_and_multiplefiles_shouldfail(): 510 with pytest.raises(getopt.GetoptError): 511 ttx.Options([("-m", "testfont.ttf")], 2) 512 513 514def test_options_woff2_and_zopfli_shouldfail(): 515 with pytest.raises(getopt.GetoptError): 516 ttx.Options([("--with-zopfli", ""), ("--flavor", "woff2")], 1) 517 518 519# ---------------------------- 520# ttx.ttCompile function tests 521# ---------------------------- 522 523 524def test_ttcompile_otf_compile_default(tmpdir): 525 inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") 526 # outotf = os.path.join(str(tmpdir), "TestOTF.otf") 527 outotf = tmpdir.join("TestOTF.ttx") 528 default_options = ttx.Options([], 1) 529 ttx.ttCompile(inttx, str(outotf), default_options) 530 # confirm that font was built 531 assert outotf.check(file=True) 532 # confirm that it is valid OTF file, can instantiate a TTFont, has expected OpenType tables 533 ttf = TTFont(str(outotf)) 534 expected_tables = ( 535 "head", 536 "hhea", 537 "maxp", 538 "OS/2", 539 "name", 540 "cmap", 541 "post", 542 "CFF ", 543 "hmtx", 544 "DSIG", 545 ) 546 for table in expected_tables: 547 assert table in ttf 548 549 550def test_ttcompile_otf_to_woff_without_zopfli(tmpdir): 551 inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") 552 outwoff = tmpdir.join("TestOTF.woff") 553 options = ttx.Options([], 1) 554 options.flavor = "woff" 555 ttx.ttCompile(inttx, str(outwoff), options) 556 # confirm that font was built 557 assert outwoff.check(file=True) 558 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 559 ttf = TTFont(str(outwoff)) 560 expected_tables = ( 561 "head", 562 "hhea", 563 "maxp", 564 "OS/2", 565 "name", 566 "cmap", 567 "post", 568 "CFF ", 569 "hmtx", 570 "DSIG", 571 ) 572 for table in expected_tables: 573 assert table in ttf 574 575 576@pytest.mark.skipif(zopfli is None, reason="zopfli not installed") 577def test_ttcompile_otf_to_woff_with_zopfli(tmpdir): 578 inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") 579 outwoff = tmpdir.join("TestOTF.woff") 580 options = ttx.Options([], 1) 581 options.flavor = "woff" 582 options.useZopfli = True 583 ttx.ttCompile(inttx, str(outwoff), options) 584 # confirm that font was built 585 assert outwoff.check(file=True) 586 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 587 ttf = TTFont(str(outwoff)) 588 expected_tables = ( 589 "head", 590 "hhea", 591 "maxp", 592 "OS/2", 593 "name", 594 "cmap", 595 "post", 596 "CFF ", 597 "hmtx", 598 "DSIG", 599 ) 600 for table in expected_tables: 601 assert table in ttf 602 603 604@pytest.mark.skipif(brotli is None, reason="brotli not installed") 605def test_ttcompile_otf_to_woff2(tmpdir): 606 inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") 607 outwoff2 = tmpdir.join("TestTTF.woff2") 608 options = ttx.Options([], 1) 609 options.flavor = "woff2" 610 ttx.ttCompile(inttx, str(outwoff2), options) 611 # confirm that font was built 612 assert outwoff2.check(file=True) 613 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 614 ttf = TTFont(str(outwoff2)) 615 # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) 616 assert "DSIG" not in ttf 617 expected_tables = ( 618 "head", 619 "hhea", 620 "maxp", 621 "OS/2", 622 "name", 623 "cmap", 624 "post", 625 "CFF ", 626 "hmtx", 627 ) 628 for table in expected_tables: 629 assert table in ttf 630 631 632def test_ttcompile_ttf_compile_default(tmpdir): 633 inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 634 outttf = tmpdir.join("TestTTF.ttf") 635 default_options = ttx.Options([], 1) 636 ttx.ttCompile(inttx, str(outttf), default_options) 637 # confirm that font was built 638 assert outttf.check(file=True) 639 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 640 ttf = TTFont(str(outttf)) 641 expected_tables = ( 642 "head", 643 "hhea", 644 "maxp", 645 "OS/2", 646 "name", 647 "cmap", 648 "hmtx", 649 "fpgm", 650 "prep", 651 "cvt ", 652 "loca", 653 "glyf", 654 "post", 655 "gasp", 656 "DSIG", 657 ) 658 for table in expected_tables: 659 assert table in ttf 660 661 662def test_ttcompile_ttf_to_woff_without_zopfli(tmpdir): 663 inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 664 outwoff = tmpdir.join("TestTTF.woff") 665 options = ttx.Options([], 1) 666 options.flavor = "woff" 667 ttx.ttCompile(inttx, str(outwoff), options) 668 # confirm that font was built 669 assert outwoff.check(file=True) 670 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 671 ttf = TTFont(str(outwoff)) 672 expected_tables = ( 673 "head", 674 "hhea", 675 "maxp", 676 "OS/2", 677 "name", 678 "cmap", 679 "hmtx", 680 "fpgm", 681 "prep", 682 "cvt ", 683 "loca", 684 "glyf", 685 "post", 686 "gasp", 687 "DSIG", 688 ) 689 for table in expected_tables: 690 assert table in ttf 691 692 693@pytest.mark.skipif(zopfli is None, reason="zopfli not installed") 694def test_ttcompile_ttf_to_woff_with_zopfli(tmpdir): 695 inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 696 outwoff = tmpdir.join("TestTTF.woff") 697 options = ttx.Options([], 1) 698 options.flavor = "woff" 699 options.useZopfli = True 700 ttx.ttCompile(inttx, str(outwoff), options) 701 # confirm that font was built 702 assert outwoff.check(file=True) 703 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 704 ttf = TTFont(str(outwoff)) 705 expected_tables = ( 706 "head", 707 "hhea", 708 "maxp", 709 "OS/2", 710 "name", 711 "cmap", 712 "hmtx", 713 "fpgm", 714 "prep", 715 "cvt ", 716 "loca", 717 "glyf", 718 "post", 719 "gasp", 720 "DSIG", 721 ) 722 for table in expected_tables: 723 assert table in ttf 724 725 726@pytest.mark.skipif(brotli is None, reason="brotli not installed") 727def test_ttcompile_ttf_to_woff2(tmpdir): 728 inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 729 outwoff2 = tmpdir.join("TestTTF.woff2") 730 options = ttx.Options([], 1) 731 options.flavor = "woff2" 732 ttx.ttCompile(inttx, str(outwoff2), options) 733 # confirm that font was built 734 assert outwoff2.check(file=True) 735 # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables 736 ttf = TTFont(str(outwoff2)) 737 # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) 738 assert "DSIG" not in ttf 739 expected_tables = ( 740 "head", 741 "hhea", 742 "maxp", 743 "OS/2", 744 "name", 745 "cmap", 746 "hmtx", 747 "fpgm", 748 "prep", 749 "cvt ", 750 "loca", 751 "glyf", 752 "post", 753 "gasp", 754 ) 755 for table in expected_tables: 756 assert table in ttf 757 758 759@pytest.mark.parametrize( 760 "inpath, outpath1, outpath2", 761 [ 762 ("TestTTF.ttx", "TestTTF1.ttf", "TestTTF2.ttf"), 763 ("TestOTF.ttx", "TestOTF1.otf", "TestOTF2.otf"), 764 ], 765) 766def test_ttcompile_timestamp_calcs(inpath, outpath1, outpath2, tmpdir): 767 inttx = os.path.join("Tests", "ttx", "data", inpath) 768 outttf1 = tmpdir.join(outpath1) 769 outttf2 = tmpdir.join(outpath2) 770 options = ttx.Options([], 1) 771 # build with default options = do not recalculate timestamp 772 ttx.ttCompile(inttx, str(outttf1), options) 773 # confirm that font was built 774 assert outttf1.check(file=True) 775 # confirm that timestamp is same as modified time on ttx file 776 mtime = os.path.getmtime(inttx) 777 epochtime = timestampSinceEpoch(mtime) 778 ttf = TTFont(str(outttf1)) 779 assert ttf["head"].modified == epochtime 780 781 # reset options to recalculate the timestamp and compile new font 782 options.recalcTimestamp = True 783 ttx.ttCompile(inttx, str(outttf2), options) 784 # confirm that font was built 785 assert outttf2.check(file=True) 786 # confirm that timestamp is more recent than modified time on ttx file 787 mtime = os.path.getmtime(inttx) 788 epochtime = timestampSinceEpoch(mtime) 789 ttf = TTFont(str(outttf2)) 790 assert ttf["head"].modified > epochtime 791 792 # --no-recalc-timestamp will keep original timestamp 793 options.recalcTimestamp = False 794 ttx.ttCompile(inttx, str(outttf2), options) 795 assert outttf2.check(file=True) 796 inttf = TTFont() 797 inttf.importXML(inttx) 798 assert inttf["head"].modified == TTFont(str(outttf2))["head"].modified 799 800 801# ------------------------- 802# ttx.ttList function tests 803# ------------------------- 804 805 806def test_ttlist_ttf(capsys, tmpdir): 807 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") 808 fakeoutpath = tmpdir.join("TestTTF.ttx") 809 options = ttx.Options([], 1) 810 options.listTables = True 811 ttx.ttList(inpath, str(fakeoutpath), options) 812 out, err = capsys.readouterr() 813 expected_tables = ( 814 "head", 815 "hhea", 816 "maxp", 817 "OS/2", 818 "name", 819 "cmap", 820 "hmtx", 821 "fpgm", 822 "prep", 823 "cvt ", 824 "loca", 825 "glyf", 826 "post", 827 "gasp", 828 "DSIG", 829 ) 830 # confirm that expected tables are printed to stdout 831 for table in expected_tables: 832 assert table in out 833 # test for one of the expected tag/checksum/length/offset strings 834 assert "OS/2 0x67230FF8 96 376" in out 835 836 837def test_ttlist_otf(capsys, tmpdir): 838 inpath = os.path.join("Tests", "ttx", "data", "TestOTF.otf") 839 fakeoutpath = tmpdir.join("TestOTF.ttx") 840 options = ttx.Options([], 1) 841 options.listTables = True 842 ttx.ttList(inpath, str(fakeoutpath), options) 843 out, err = capsys.readouterr() 844 expected_tables = ( 845 "head", 846 "hhea", 847 "maxp", 848 "OS/2", 849 "name", 850 "cmap", 851 "post", 852 "CFF ", 853 "hmtx", 854 "DSIG", 855 ) 856 # confirm that expected tables are printed to stdout 857 for table in expected_tables: 858 assert table in out 859 # test for one of the expected tag/checksum/length/offset strings 860 assert "OS/2 0x67230FF8 96 272" in out 861 862 863def test_ttlist_woff(capsys, tmpdir): 864 inpath = os.path.join("Tests", "ttx", "data", "TestWOFF.woff") 865 fakeoutpath = tmpdir.join("TestWOFF.ttx") 866 options = ttx.Options([], 1) 867 options.listTables = True 868 options.flavor = "woff" 869 ttx.ttList(inpath, str(fakeoutpath), options) 870 out, err = capsys.readouterr() 871 expected_tables = ( 872 "head", 873 "hhea", 874 "maxp", 875 "OS/2", 876 "name", 877 "cmap", 878 "post", 879 "CFF ", 880 "hmtx", 881 "DSIG", 882 ) 883 # confirm that expected tables are printed to stdout 884 for table in expected_tables: 885 assert table in out 886 # test for one of the expected tag/checksum/length/offset strings 887 assert "OS/2 0x67230FF8 84 340" in out 888 889 890@pytest.mark.skipif(brotli is None, reason="brotli not installed") 891def test_ttlist_woff2(capsys, tmpdir): 892 inpath = os.path.join("Tests", "ttx", "data", "TestWOFF2.woff2") 893 fakeoutpath = tmpdir.join("TestWOFF2.ttx") 894 options = ttx.Options([], 1) 895 options.listTables = True 896 options.flavor = "woff2" 897 ttx.ttList(inpath, str(fakeoutpath), options) 898 out, err = capsys.readouterr() 899 expected_tables = ( 900 "head", 901 "hhea", 902 "maxp", 903 "OS/2", 904 "name", 905 "cmap", 906 "hmtx", 907 "fpgm", 908 "prep", 909 "cvt ", 910 "loca", 911 "glyf", 912 "post", 913 "gasp", 914 ) 915 # confirm that expected tables are printed to stdout 916 for table in expected_tables: 917 assert table in out 918 # test for one of the expected tag/checksum/length/offset strings 919 assert "OS/2 0x67230FF8 96 0" in out 920 921 922# ------------------- 923# main function tests 924# ------------------- 925 926 927def test_main_default_ttf_dump_to_ttx(tmpdir): 928 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") 929 outpath = tmpdir.join("TestTTF.ttx") 930 args = ["-o", str(outpath), inpath] 931 ttx.main(args) 932 assert outpath.check(file=True) 933 934 935def test_main_default_ttx_compile_to_ttf(tmpdir): 936 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 937 outpath = tmpdir.join("TestTTF.ttf") 938 args = ["-o", str(outpath), inpath] 939 ttx.main(args) 940 assert outpath.check(file=True) 941 942 943def test_main_getopterror_missing_directory(): 944 with pytest.raises(SystemExit): 945 with pytest.raises(getopt.GetoptError): 946 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") 947 args = ["-d", "bogusdir", inpath] 948 ttx.main(args) 949 950 951def test_main_keyboard_interrupt(tmpdir, monkeypatch, caplog): 952 with pytest.raises(SystemExit): 953 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 954 outpath = tmpdir.join("TestTTF.ttf") 955 args = ["-o", str(outpath), inpath] 956 monkeypatch.setattr( 957 ttx, "process", (lambda x, y: raise_exception(KeyboardInterrupt)) 958 ) 959 ttx.main(args) 960 961 assert "(Cancelled.)" in caplog.text 962 963 964def test_main_system_exit(tmpdir, monkeypatch): 965 with pytest.raises(SystemExit): 966 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 967 outpath = tmpdir.join("TestTTF.ttf") 968 args = ["-o", str(outpath), inpath] 969 monkeypatch.setattr(ttx, "process", (lambda x, y: raise_exception(SystemExit))) 970 ttx.main(args) 971 972 973def test_main_ttlib_error(tmpdir, monkeypatch, caplog): 974 with pytest.raises(SystemExit): 975 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 976 outpath = tmpdir.join("TestTTF.ttf") 977 args = ["-o", str(outpath), inpath] 978 monkeypatch.setattr( 979 ttx, 980 "process", 981 (lambda x, y: raise_exception(TTLibError("Test error"))), 982 ) 983 ttx.main(args) 984 985 assert "Test error" in caplog.text 986 987 988def test_main_base_exception(tmpdir, monkeypatch, caplog): 989 with pytest.raises(SystemExit): 990 inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") 991 outpath = tmpdir.join("TestTTF.ttf") 992 args = ["-o", str(outpath), inpath] 993 monkeypatch.setattr( 994 ttx, 995 "process", 996 (lambda x, y: raise_exception(Exception("Test error"))), 997 ) 998 ttx.main(args) 999 1000 assert "Unhandled exception has occurred" in caplog.text 1001 1002 1003def test_main_ttf_dump_stdin_to_stdout(tmp_path): 1004 inpath = Path("Tests").joinpath("ttx", "data", "TestTTF.ttf") 1005 outpath = tmp_path / "TestTTF.ttx" 1006 args = [sys.executable, "-m", "fontTools.ttx", "-q", "-o", "-", "-"] 1007 with inpath.open("rb") as infile, outpath.open("w", encoding="utf-8") as outfile: 1008 subprocess.run(args, check=True, stdin=infile, stdout=outfile) 1009 assert outpath.is_file() 1010 1011 1012def test_main_ttx_compile_stdin_to_stdout(tmp_path): 1013 inpath = Path("Tests").joinpath("ttx", "data", "TestTTF.ttx") 1014 outpath = tmp_path / "TestTTF.ttf" 1015 args = [sys.executable, "-m", "fontTools.ttx", "-q", "-o", "-", "-"] 1016 with inpath.open("r", encoding="utf-8") as infile, outpath.open("wb") as outfile: 1017 subprocess.run(args, check=True, stdin=infile, stdout=outfile) 1018 assert outpath.is_file() 1019 1020 1021def test_roundtrip_DSIG_split_at_XML_parse_buffer_size(tmp_path): 1022 inpath = Path("Tests").joinpath( 1023 "ttx", "data", "roundtrip_DSIG_split_at_XML_parse_buffer_size.ttx" 1024 ) 1025 font = TTFont() 1026 font.importXML(inpath) 1027 font["DMMY"] = DefaultTable(tag="DMMY") 1028 # just enough dummy bytes to hit the cut off point whereby DSIG data gets 1029 # split into two chunks and triggers the bug from 1030 # https://github.com/fonttools/fonttools/issues/2614 1031 font["DMMY"].data = b"\x01\x02\x03\x04" * 2438 1032 font.saveXML(tmp_path / "roundtrip_DSIG_split_at_XML_parse_buffer_size.ttx") 1033 1034 outpath = tmp_path / "font.ttf" 1035 args = [ 1036 sys.executable, 1037 "-m", 1038 "fontTools.ttx", 1039 "-q", 1040 "-o", 1041 str(outpath), 1042 str(tmp_path / "roundtrip_DSIG_split_at_XML_parse_buffer_size.ttx"), 1043 ] 1044 subprocess.run(args, check=True) 1045 1046 assert outpath.is_file() 1047 assert TTFont(outpath)["DSIG"].signatureRecords[0].pkcs7 == base64.b64decode( 1048 b"0000000100000000" 1049 ) 1050 1051 1052# --------------------------- 1053# support functions for tests 1054# --------------------------- 1055 1056 1057def raise_exception(exception): 1058 raise exception 1059