xref: /aosp_15_r20/external/fonttools/Tests/mtiLib/mti_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.misc.xmlWriter import XMLWriter
2from fontTools.ttLib import TTFont
3from fontTools.feaLib.lookupDebugInfo import LOOKUP_DEBUG_ENV_VAR
4from fontTools import mtiLib
5import difflib
6from io import StringIO
7import os
8import sys
9import pytest
10
11
12@pytest.fixture(autouse=True)
13def set_lookup_debug_env_var(monkeypatch):
14    monkeypatch.setenv(LOOKUP_DEBUG_ENV_VAR, "1")
15
16
17class MtiTest:
18    GLYPH_ORDER = [
19        ".notdef",
20        "a",
21        "b",
22        "pakannada",
23        "phakannada",
24        "vakannada",
25        "pevowelkannada",
26        "phevowelkannada",
27        "vevowelkannada",
28        "uvowelsignkannada",
29        "uuvowelsignkannada",
30        "uvowelsignaltkannada",
31        "uuvowelsignaltkannada",
32        "uuvowelsignsinh",
33        "uvowelsignsinh",
34        "rakarsinh",
35        "zero",
36        "one",
37        "two",
38        "three",
39        "four",
40        "five",
41        "six",
42        "seven",
43        "eight",
44        "nine",
45        "slash",
46        "fraction",
47        "A",
48        "B",
49        "C",
50        "fi",
51        "fl",
52        "breve",
53        "acute",
54        "uniFB01",
55        "ffi",
56        "grave",
57        "commaacent",
58        "dotbelow",
59        "dotabove",
60        "cedilla",
61        "commaaccent",
62        "Acircumflex",
63        "V",
64        "T",
65        "acircumflex",
66        "Aacute",
67        "Agrave",
68        "O",
69        "Oacute",
70        "Ograve",
71        "Ocircumflex",
72        "aacute",
73        "agrave",
74        "aimatrabindigurmukhi",
75        "aimatragurmukhi",
76        "aimatratippigurmukhi",
77        "aumatrabindigurmukhi",
78        "aumatragurmukhi",
79        "bindigurmukhi",
80        "eematrabindigurmukhi",
81        "eematragurmukhi",
82        "eematratippigurmukhi",
83        "oomatrabindigurmukhi",
84        "oomatragurmukhi",
85        "oomatratippigurmukhi",
86        "lagurmukhi",
87        "lanuktagurmukhi",
88        "nagurmukhi",
89        "nanuktagurmukhi",
90        "ngagurmukhi",
91        "nganuktagurmukhi",
92        "nnagurmukhi",
93        "nnanuktagurmukhi",
94        "tthagurmukhi",
95        "tthanuktagurmukhi",
96        "bsuperior",
97        "isuperior",
98        "vsuperior",
99        "wsuperior",
100        "periodsuperior",
101        "osuperior",
102        "tsuperior",
103        "dollarsuperior",
104        "fsuperior",
105        "gsuperior",
106        "zsuperior",
107        "dsuperior",
108        "psuperior",
109        "hsuperior",
110        "oesuperior",
111        "aesuperior",
112        "centsuperior",
113        "esuperior",
114        "lsuperior",
115        "qsuperior",
116        "csuperior",
117        "asuperior",
118        "commasuperior",
119        "xsuperior",
120        "egravesuperior",
121        "usuperior",
122        "rsuperior",
123        "nsuperior",
124        "ssuperior",
125        "msuperior",
126        "jsuperior",
127        "ysuperior",
128        "ksuperior",
129        "guilsinglright",
130        "guilsinglleft",
131        "uniF737",
132        "uniE11C",
133        "uniE11D",
134        "uniE11A",
135        "uni2077",
136        "uni2087",
137        "uniE11B",
138        "uniE119",
139        "uniE0DD",
140        "uniE0DE",
141        "uniF736",
142        "uniE121",
143        "uniE122",
144        "uniE11F",
145        "uni2076",
146        "uni2086",
147        "uniE120",
148        "uniE11E",
149        "uniE0DB",
150        "uniE0DC",
151        "uniF733",
152        "uniE12B",
153        "uniE12C",
154        "uniE129",
155        "uni00B3",
156        "uni2083",
157        "uniE12A",
158        "uniE128",
159        "uniF732",
160        "uniE133",
161        "uniE134",
162        "uniE131",
163        "uni00B2",
164        "uni2082",
165        "uniE132",
166        "uniE130",
167        "uniE0F9",
168        "uniF734",
169        "uniE0D4",
170        "uniE0D5",
171        "uniE0D2",
172        "uni2074",
173        "uni2084",
174        "uniE0D3",
175        "uniE0D1",
176        "uniF730",
177        "uniE13D",
178        "uniE13E",
179        "uniE13A",
180        "uni2070",
181        "uni2080",
182        "uniE13B",
183        "uniE139",
184        "uniE13C",
185        "uniF739",
186        "uniE0EC",
187        "uniE0ED",
188        "uniE0EA",
189        "uni2079",
190        "uni2089",
191        "uniE0EB",
192        "uniE0E9",
193        "uniF735",
194        "uniE0CD",
195        "uniE0CE",
196        "uniE0CB",
197        "uni2075",
198        "uni2085",
199        "uniE0CC",
200        "uniE0CA",
201        "uniF731",
202        "uniE0F3",
203        "uniE0F4",
204        "uniE0F1",
205        "uni00B9",
206        "uni2081",
207        "uniE0F2",
208        "uniE0F0",
209        "uniE0F8",
210        "uniF738",
211        "uniE0C0",
212        "uniE0C1",
213        "uniE0BE",
214        "uni2078",
215        "uni2088",
216        "uniE0BF",
217        "uniE0BD",
218        "I",
219        "Ismall",
220        "t",
221        "i",
222        "f",
223        "IJ",
224        "J",
225        "IJsmall",
226        "Jsmall",
227        "tt",
228        "ij",
229        "j",
230        "ffb",
231        "ffh",
232        "h",
233        "ffk",
234        "k",
235        "ffl",
236        "l",
237        "fft",
238        "fb",
239        "ff",
240        "fh",
241        "fj",
242        "fk",
243        "ft",
244        "janyevoweltelugu",
245        "kassevoweltelugu",
246        "jaivoweltelugu",
247        "nyasubscripttelugu",
248        "kaivoweltelugu",
249        "ssasubscripttelugu",
250        "bayi1",
251        "jeemi1",
252        "kafi1",
253        "ghafi1",
254        "laami1",
255        "kafm1",
256        "ghafm1",
257        "laamm1",
258        "rayf2",
259        "reyf2",
260        "yayf2",
261        "zayf2",
262        "fayi1",
263        "ayehf2",
264        "hamzayeharabf2",
265        "hamzayehf2",
266        "yehf2",
267        "ray",
268        "rey",
269        "zay",
270        "yay",
271        "dal",
272        "del",
273        "zal",
274        "rayf1",
275        "reyf1",
276        "yayf1",
277        "zayf1",
278        "ayehf1",
279        "hamzayeharabf1",
280        "hamzayehf1",
281        "yehf1",
282        "dal1",
283        "del1",
284        "zal1",
285        "onehalf",
286        "onehalf.alt",
287        "onequarter",
288        "onequarter.alt",
289        "threequarters",
290        "threequarters.alt",
291        "AlefSuperiorNS",
292        "DammaNS",
293        "DammaRflxNS",
294        "DammatanNS",
295        "Fatha2dotsNS",
296        "FathaNS",
297        "FathatanNS",
298        "FourDotsAboveNS",
299        "HamzaAboveNS",
300        "MaddaNS",
301        "OneDotAbove2NS",
302        "OneDotAboveNS",
303        "ShaddaAlefNS",
304        "ShaddaDammaNS",
305        "ShaddaDammatanNS",
306        "ShaddaFathatanNS",
307        "ShaddaKasraNS",
308        "ShaddaKasratanNS",
309        "ShaddaNS",
310        "SharetKafNS",
311        "SukunNS",
312        "ThreeDotsDownAboveNS",
313        "ThreeDotsUpAboveNS",
314        "TwoDotsAboveNS",
315        "TwoDotsVerticalAboveNS",
316        "UltapeshNS",
317        "WaslaNS",
318        "AinIni.12m_MeemFin.02",
319        "AinIni_YehBarreeFin",
320        "AinMed_YehBarreeFin",
321        "BehxIni_MeemFin",
322        "BehxIni_NoonGhunnaFin",
323        "BehxIni_RehFin",
324        "BehxIni_RehFin.b",
325        "BehxMed_MeemFin.py",
326        "BehxMed_NoonGhunnaFin",
327        "BehxMed_NoonGhunnaFin.cup",
328        "BehxMed_RehFin",
329        "BehxMed_RehFin.cup",
330        "BehxMed_YehxFin",
331        "FehxMed_YehBarreeFin",
332        "HahIni_YehBarreeFin",
333        "KafIni_YehBarreeFin",
334        "KafMed.12_YehxFin.01",
335        "KafMed_MeemFin",
336        "KafMed_YehBarreeFin",
337        "LamAlefFin",
338        "LamAlefFin.cup",
339        "LamAlefFin.cut",
340        "LamAlefFin.short",
341        "LamAlefSep",
342        "LamIni_MeemFin",
343        "LamIni_YehBarreeFin",
344        "LamMed_MeemFin",
345        "LamMed_MeemFin.b",
346        "LamMed_YehxFin",
347        "LamMed_YehxFin.cup",
348        "TahIni_YehBarreeFin",
349        "null",
350        "CR",
351        "space",
352        "exclam",
353        "quotedbl",
354        "numbersign",
355    ]
356
357    # Feature files in data/*.txt; output gets compared to data/*.ttx.
358    TESTS = {
359        None: ("mti/cmap",),
360        "cmap": ("mti/cmap",),
361        "GSUB": (
362            "featurename-backward",
363            "featurename-forward",
364            "lookupnames-backward",
365            "lookupnames-forward",
366            "mixed-toplevels",
367            "mti/scripttable",
368            "mti/chainedclass",
369            "mti/chainedcoverage",
370            "mti/chained-glyph",
371            "mti/gsubalternate",
372            "mti/gsubligature",
373            "mti/gsubmultiple",
374            "mti/gsubreversechanined",
375            "mti/gsubsingle",
376        ),
377        "GPOS": (
378            "mti/scripttable",
379            "mti/chained-glyph",
380            "mti/gposcursive",
381            "mti/gposkernset",
382            "mti/gposmarktobase",
383            "mti/gpospairclass",
384            "mti/gpospairglyph",
385            "mti/gpossingle",
386            "mti/mark-to-ligature",
387        ),
388        "GDEF": (
389            "mti/gdefattach",
390            "mti/gdefclasses",
391            "mti/gdefligcaret",
392            "mti/gdefmarkattach",
393            "mti/gdefmarkfilter",
394        ),
395    }
396    # TODO:
397    # https://github.com/Monotype/OpenType_Table_Source/issues/12
398    #
399    #        'mti/featuretable'
400    #        'mti/contextclass'
401    #        'mti/contextcoverage'
402    #        'mti/context-glyph'
403
404    @staticmethod
405    def getpath(testfile):
406        path, _ = os.path.split(__file__)
407        return os.path.join(path, "data", testfile)
408
409    def expect_ttx(self, expected_ttx, actual_ttx, fromfile=None, tofile=None):
410        expected = [l + "\n" for l in expected_ttx.split("\n")]
411        actual = [l + "\n" for l in actual_ttx.split("\n")]
412        if actual != expected:
413            sys.stderr.write("\n")
414            for line in difflib.unified_diff(
415                expected, actual, fromfile=fromfile, tofile=tofile
416            ):
417                sys.stderr.write(line)
418            pytest.fail("TTX output is different from expected")
419
420    @classmethod
421    def create_font(celf):
422        font = TTFont()
423        font.setGlyphOrder(celf.GLYPH_ORDER)
424        return font
425
426    def check_mti_file(self, name, tableTag=None):
427        xml_expected_path = self.getpath(
428            "%s.ttx" % name + ("." + tableTag if tableTag is not None else "")
429        )
430        with open(xml_expected_path, "rt", encoding="utf-8") as xml_expected_file:
431            xml_expected = xml_expected_file.read()
432
433        font = self.create_font()
434
435        with open(self.getpath("%s.txt" % name), "rt", encoding="utf-8") as f:
436            table = mtiLib.build(f, font, tableTag=tableTag)
437
438        if tableTag is not None:
439            assert tableTag == table.tableTag
440        tableTag = table.tableTag
441
442        # Make sure it compiles.
443        blob = table.compile(font)
444
445        # Make sure it decompiles.
446        decompiled = table.__class__()
447        decompiled.decompile(blob, font)
448
449        # XML from built object.
450        writer = XMLWriter(StringIO())
451        writer.begintag(tableTag)
452        writer.newline()
453        table.toXML(writer, font)
454        writer.endtag(tableTag)
455        writer.newline()
456        xml_built = writer.file.getvalue()
457
458        # XML from decompiled object.
459        writer = XMLWriter(StringIO())
460        writer.begintag(tableTag)
461        writer.newline()
462        decompiled.toXML(writer, font)
463        writer.endtag(tableTag)
464        writer.newline()
465        xml_binary = writer.file.getvalue()
466
467        self.expect_ttx(xml_binary, xml_built, fromfile="decompiled", tofile="built")
468        self.expect_ttx(
469            xml_expected, xml_built, fromfile=xml_expected_path, tofile="built"
470        )
471
472        from fontTools.misc import xmlReader
473
474        f = StringIO()
475        f.write(xml_expected)
476        f.seek(0)
477        font2 = TTFont()
478        font2.setGlyphOrder(font.getGlyphOrder())
479        reader = xmlReader.XMLReader(f, font2)
480        reader.read(rootless=True)
481
482        # XML from object read from XML.
483        writer = XMLWriter(StringIO())
484        writer.begintag(tableTag)
485        writer.newline()
486        font2[tableTag].toXML(writer, font)
487        writer.endtag(tableTag)
488        writer.newline()
489        xml_fromxml = writer.file.getvalue()
490
491        self.expect_ttx(
492            xml_expected, xml_fromxml, fromfile=xml_expected_path, tofile="fromxml"
493        )
494
495
496def generate_mti_file_test(name, tableTag=None):
497    return lambda self: self.check_mti_file(
498        os.path.join(*name.split("/")), tableTag=tableTag
499    )
500
501
502for tableTag, tests in MtiTest.TESTS.items():
503    for name in tests:
504        setattr(
505            MtiTest,
506            "test_MtiFile_%s%s" % (name, "_" + tableTag if tableTag else ""),
507            generate_mti_file_test(name, tableTag=tableTag),
508        )
509
510
511if __name__ == "__main__":
512    if len(sys.argv) > 1:
513        from fontTools.mtiLib import main
514
515        font = MtiTest.create_font()
516        sys.exit(main(sys.argv[1:], font))
517    sys.exit(pytest.main(sys.argv))
518