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