• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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