1from fontTools.misc.xmlWriter import XMLWriter 2from fontTools.ttLib import TTFont 3from fontTools import mtiLib 4import difflib 5from io import StringIO 6import os 7import sys 8import unittest 9 10 11class MtiTest(unittest.TestCase): 12 13 GLYPH_ORDER = ['.notdef', 14 'a', 'b', 'pakannada', 'phakannada', 'vakannada', 'pevowelkannada', 15 'phevowelkannada', 'vevowelkannada', 'uvowelsignkannada', 'uuvowelsignkannada', 16 'uvowelsignaltkannada', 'uuvowelsignaltkannada', 'uuvowelsignsinh', 17 'uvowelsignsinh', 'rakarsinh', 'zero', 'one', 'two', 'three', 'four', 'five', 18 'six', 'seven', 'eight', 'nine', 'slash', 'fraction', 'A', 'B', 'C', 'fi', 19 'fl', 'breve', 'acute', 'uniFB01', 'ffi', 'grave', 'commaacent', 'dotbelow', 20 'dotabove', 'cedilla', 'commaaccent', 'Acircumflex', 'V', 'T', 'acircumflex', 21 'Aacute', 'Agrave', 'O', 'Oacute', 'Ograve', 'Ocircumflex', 'aacute', 'agrave', 22 'aimatrabindigurmukhi', 'aimatragurmukhi', 'aimatratippigurmukhi', 23 'aumatrabindigurmukhi', 'aumatragurmukhi', 'bindigurmukhi', 24 'eematrabindigurmukhi', 'eematragurmukhi', 'eematratippigurmukhi', 25 'oomatrabindigurmukhi', 'oomatragurmukhi', 'oomatratippigurmukhi', 26 'lagurmukhi', 'lanuktagurmukhi', 'nagurmukhi', 'nanuktagurmukhi', 27 'ngagurmukhi', 'nganuktagurmukhi', 'nnagurmukhi', 'nnanuktagurmukhi', 28 'tthagurmukhi', 'tthanuktagurmukhi', 'bsuperior', 'isuperior', 'vsuperior', 29 'wsuperior', 'periodsuperior', 'osuperior', 'tsuperior', 'dollarsuperior', 30 'fsuperior', 'gsuperior', 'zsuperior', 'dsuperior', 'psuperior', 'hsuperior', 31 'oesuperior', 'aesuperior', 'centsuperior', 'esuperior', 'lsuperior', 32 'qsuperior', 'csuperior', 'asuperior', 'commasuperior', 'xsuperior', 33 'egravesuperior', 'usuperior', 'rsuperior', 'nsuperior', 'ssuperior', 34 'msuperior', 'jsuperior', 'ysuperior', 'ksuperior', 'guilsinglright', 35 'guilsinglleft', 'uniF737', 'uniE11C', 'uniE11D', 'uniE11A', 'uni2077', 36 'uni2087', 'uniE11B', 'uniE119', 'uniE0DD', 'uniE0DE', 'uniF736', 'uniE121', 37 'uniE122', 'uniE11F', 'uni2076', 'uni2086', 'uniE120', 'uniE11E', 'uniE0DB', 38 'uniE0DC', 'uniF733', 'uniE12B', 'uniE12C', 'uniE129', 'uni00B3', 'uni2083', 39 'uniE12A', 'uniE128', 'uniF732', 'uniE133', 'uniE134', 'uniE131', 'uni00B2', 40 'uni2082', 'uniE132', 'uniE130', 'uniE0F9', 'uniF734', 'uniE0D4', 'uniE0D5', 41 'uniE0D2', 'uni2074', 'uni2084', 'uniE0D3', 'uniE0D1', 'uniF730', 'uniE13D', 42 'uniE13E', 'uniE13A', 'uni2070', 'uni2080', 'uniE13B', 'uniE139', 'uniE13C', 43 'uniF739', 'uniE0EC', 'uniE0ED', 'uniE0EA', 'uni2079', 'uni2089', 'uniE0EB', 44 'uniE0E9', 'uniF735', 'uniE0CD', 'uniE0CE', 'uniE0CB', 'uni2075', 'uni2085', 45 'uniE0CC', 'uniE0CA', 'uniF731', 'uniE0F3', 'uniE0F4', 'uniE0F1', 'uni00B9', 46 'uni2081', 'uniE0F2', 'uniE0F0', 'uniE0F8', 'uniF738', 'uniE0C0', 'uniE0C1', 47 'uniE0BE', 'uni2078', 'uni2088', 'uniE0BF', 'uniE0BD', 'I', 'Ismall', 't', 'i', 48 'f', 'IJ', 'J', 'IJsmall', 'Jsmall', 'tt', 'ij', 'j', 'ffb', 'ffh', 'h', 'ffk', 49 'k', 'ffl', 'l', 'fft', 'fb', 'ff', 'fh', 'fj', 'fk', 'ft', 'janyevoweltelugu', 50 'kassevoweltelugu', 'jaivoweltelugu', 'nyasubscripttelugu', 'kaivoweltelugu', 51 'ssasubscripttelugu', 'bayi1', 'jeemi1', 'kafi1', 'ghafi1', 'laami1', 'kafm1', 52 'ghafm1', 'laamm1', 'rayf2', 'reyf2', 'yayf2', 'zayf2', 'fayi1', 'ayehf2', 53 'hamzayeharabf2', 'hamzayehf2', 'yehf2', 'ray', 'rey', 'zay', 'yay', 'dal', 54 'del', 'zal', 'rayf1', 'reyf1', 'yayf1', 'zayf1', 'ayehf1', 'hamzayeharabf1', 55 'hamzayehf1', 'yehf1', 'dal1', 'del1', 'zal1', 'onehalf', 'onehalf.alt', 56 'onequarter', 'onequarter.alt', 'threequarters', 'threequarters.alt', 57 'AlefSuperiorNS', 'DammaNS', 'DammaRflxNS', 'DammatanNS', 'Fatha2dotsNS', 58 'FathaNS', 'FathatanNS', 'FourDotsAboveNS', 'HamzaAboveNS', 'MaddaNS', 59 'OneDotAbove2NS', 'OneDotAboveNS', 'ShaddaAlefNS', 'ShaddaDammaNS', 60 'ShaddaDammatanNS', 'ShaddaFathatanNS', 'ShaddaKasraNS', 'ShaddaKasratanNS', 61 'ShaddaNS', 'SharetKafNS', 'SukunNS', 'ThreeDotsDownAboveNS', 62 'ThreeDotsUpAboveNS', 'TwoDotsAboveNS', 'TwoDotsVerticalAboveNS', 'UltapeshNS', 63 'WaslaNS', 'AinIni.12m_MeemFin.02', 'AinIni_YehBarreeFin', 64 'AinMed_YehBarreeFin', 'BehxIni_MeemFin', 'BehxIni_NoonGhunnaFin', 65 'BehxIni_RehFin', 'BehxIni_RehFin.b', 'BehxMed_MeemFin.py', 66 'BehxMed_NoonGhunnaFin', 'BehxMed_NoonGhunnaFin.cup', 'BehxMed_RehFin', 67 'BehxMed_RehFin.cup', 'BehxMed_YehxFin', 'FehxMed_YehBarreeFin', 68 'HahIni_YehBarreeFin', 'KafIni_YehBarreeFin', 'KafMed.12_YehxFin.01', 69 'KafMed_MeemFin', 'KafMed_YehBarreeFin', 'LamAlefFin', 'LamAlefFin.cup', 70 'LamAlefFin.cut', 'LamAlefFin.short', 'LamAlefSep', 'LamIni_MeemFin', 71 'LamIni_YehBarreeFin', 'LamMed_MeemFin', 'LamMed_MeemFin.b', 'LamMed_YehxFin', 72 'LamMed_YehxFin.cup', 'TahIni_YehBarreeFin', 'null', 'CR', 'space', 73 'exclam', 'quotedbl', 'numbersign', 74 ] 75 76 # Feature files in data/*.txt; output gets compared to data/*.ttx. 77 TESTS = { 78 None: ( 79 'mti/cmap', 80 ), 81 'cmap': ( 82 'mti/cmap', 83 ), 84 'GSUB': ( 85 'featurename-backward', 86 'featurename-forward', 87 'lookupnames-backward', 88 'lookupnames-forward', 89 'mixed-toplevels', 90 91 'mti/scripttable', 92 'mti/chainedclass', 93 'mti/chainedcoverage', 94 'mti/chained-glyph', 95 'mti/gsubalternate', 96 'mti/gsubligature', 97 'mti/gsubmultiple', 98 'mti/gsubreversechanined', 99 'mti/gsubsingle', 100 ), 101 'GPOS': ( 102 'mti/scripttable', 103 'mti/chained-glyph', 104 'mti/gposcursive', 105 'mti/gposkernset', 106 'mti/gposmarktobase', 107 'mti/gpospairclass', 108 'mti/gpospairglyph', 109 'mti/gpossingle', 110 'mti/mark-to-ligature', 111 ), 112 'GDEF': ( 113 'mti/gdefattach', 114 'mti/gdefclasses', 115 'mti/gdefligcaret', 116 'mti/gdefmarkattach', 117 'mti/gdefmarkfilter', 118 ), 119 } 120 # TODO: 121 # https://github.com/Monotype/OpenType_Table_Source/issues/12 122 # 123 # 'mti/featuretable' 124 # 'mti/contextclass' 125 # 'mti/contextcoverage' 126 # 'mti/context-glyph' 127 128 def __init__(self, methodName): 129 unittest.TestCase.__init__(self, methodName) 130 # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 131 # and fires deprecation warnings if a program uses the old name. 132 if not hasattr(self, "assertRaisesRegex"): 133 self.assertRaisesRegex = self.assertRaisesRegexp 134 135 def setUp(self): 136 pass 137 138 def tearDown(self): 139 pass 140 141 @staticmethod 142 def getpath(testfile): 143 path, _ = os.path.split(__file__) 144 return os.path.join(path, "data", testfile) 145 146 def expect_ttx(self, expected_ttx, actual_ttx, fromfile=None, tofile=None): 147 expected = [l+'\n' for l in expected_ttx.split('\n')] 148 actual = [l+'\n' for l in actual_ttx.split('\n')] 149 if actual != expected: 150 sys.stderr.write('\n') 151 for line in difflib.unified_diff( 152 expected, actual, fromfile=fromfile, tofile=tofile): 153 sys.stderr.write(line) 154 self.fail("TTX output is different from expected") 155 156 @classmethod 157 def create_font(celf): 158 font = TTFont() 159 font.setGlyphOrder(celf.GLYPH_ORDER) 160 return font 161 162 def check_mti_file(self, name, tableTag=None): 163 164 xml_expected_path = self.getpath("%s.ttx" % name + ('.'+tableTag if tableTag is not None else '')) 165 with open(xml_expected_path, 'rt', encoding="utf-8") as xml_expected_file: 166 xml_expected = xml_expected_file.read() 167 168 font = self.create_font() 169 170 with open(self.getpath("%s.txt" % name), 'rt', encoding="utf-8") as f: 171 table = mtiLib.build(f, font, tableTag=tableTag) 172 173 if tableTag is not None: 174 self.assertEqual(tableTag, table.tableTag) 175 tableTag = table.tableTag 176 177 # Make sure it compiles. 178 blob = table.compile(font) 179 180 # Make sure it decompiles. 181 decompiled = table.__class__() 182 decompiled.decompile(blob, font) 183 184 # XML from built object. 185 writer = XMLWriter(StringIO()) 186 writer.begintag(tableTag); writer.newline() 187 table.toXML(writer, font) 188 writer.endtag(tableTag); writer.newline() 189 xml_built = writer.file.getvalue() 190 191 # XML from decompiled object. 192 writer = XMLWriter(StringIO()) 193 writer.begintag(tableTag); writer.newline() 194 decompiled.toXML(writer, font) 195 writer.endtag(tableTag); writer.newline() 196 xml_binary = writer.file.getvalue() 197 198 self.expect_ttx(xml_binary, xml_built, fromfile='decompiled', tofile='built') 199 self.expect_ttx(xml_expected, xml_built, fromfile=xml_expected_path, tofile='built') 200 201 from fontTools.misc import xmlReader 202 f = StringIO() 203 f.write(xml_expected) 204 f.seek(0) 205 font2 = TTFont() 206 font2.setGlyphOrder(font.getGlyphOrder()) 207 reader = xmlReader.XMLReader(f, font2) 208 reader.read(rootless=True) 209 210 # XML from object read from XML. 211 writer = XMLWriter(StringIO()) 212 writer.begintag(tableTag); writer.newline() 213 font2[tableTag].toXML(writer, font) 214 writer.endtag(tableTag); writer.newline() 215 xml_fromxml = writer.file.getvalue() 216 217 self.expect_ttx(xml_expected, xml_fromxml, fromfile=xml_expected_path, tofile='fromxml') 218 219def generate_mti_file_test(name, tableTag=None): 220 return lambda self: self.check_mti_file(os.path.join(*name.split('/')), tableTag=tableTag) 221 222 223for tableTag,tests in MtiTest.TESTS.items(): 224 for name in tests: 225 setattr(MtiTest, "test_MtiFile_%s%s" % (name, '_'+tableTag if tableTag else ''), 226 generate_mti_file_test(name, tableTag=tableTag)) 227 228 229if __name__ == "__main__": 230 if len(sys.argv) > 1: 231 from fontTools.mtiLib import main 232 font = MtiTest.create_font() 233 sys.exit(main(sys.argv[1:], font)) 234 sys.exit(unittest.main()) 235