1import io 2import os 3import re 4import random 5from fontTools.feaLib.builder import addOpenTypeFeaturesFromString 6from fontTools.ttLib import TTFont, newTable, registerCustomTableClass, unregisterCustomTableClass 7from fontTools.ttLib.tables.DefaultTable import DefaultTable 8from fontTools.ttLib.tables._c_m_a_p import CmapSubtable 9import pytest 10 11 12DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data") 13 14 15class CustomTableClass(DefaultTable): 16 17 def decompile(self, data, ttFont): 18 self.numbers = list(data) 19 20 def compile(self, ttFont): 21 return bytes(self.numbers) 22 23 # not testing XML read/write 24 25 26table_C_U_S_T_ = CustomTableClass # alias for testing 27 28 29TABLETAG = "CUST" 30 31 32def normalize_TTX(string): 33 string = re.sub(' ttLibVersion=".*"', "", string) 34 string = re.sub('checkSumAdjustment value=".*"', "", string) 35 string = re.sub('modified value=".*"', "", string) 36 return string 37 38 39def test_registerCustomTableClass(): 40 font = TTFont() 41 font[TABLETAG] = newTable(TABLETAG) 42 font[TABLETAG].data = b"\x00\x01\xff" 43 f = io.BytesIO() 44 font.save(f) 45 f.seek(0) 46 assert font[TABLETAG].data == b"\x00\x01\xff" 47 registerCustomTableClass(TABLETAG, "ttFont_test", "CustomTableClass") 48 try: 49 font = TTFont(f) 50 assert font[TABLETAG].numbers == [0, 1, 255] 51 assert font[TABLETAG].compile(font) == b"\x00\x01\xff" 52 finally: 53 unregisterCustomTableClass(TABLETAG) 54 55 56def test_registerCustomTableClassStandardName(): 57 registerCustomTableClass(TABLETAG, "ttFont_test") 58 try: 59 font = TTFont() 60 font[TABLETAG] = newTable(TABLETAG) 61 font[TABLETAG].numbers = [4, 5, 6] 62 assert font[TABLETAG].compile(font) == b"\x04\x05\x06" 63 finally: 64 unregisterCustomTableClass(TABLETAG) 65 66 67ttxTTF = r"""<?xml version="1.0" encoding="UTF-8"?> 68<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.9.0"> 69 <hmtx> 70 <mtx name=".notdef" width="300" lsb="0"/> 71 </hmtx> 72</ttFont> 73""" 74 75 76ttxOTF = """<?xml version="1.0" encoding="UTF-8"?> 77<ttFont sfntVersion="OTTO" ttLibVersion="4.9.0"> 78 <hmtx> 79 <mtx name=".notdef" width="300" lsb="0"/> 80 </hmtx> 81</ttFont> 82""" 83 84 85def test_sfntVersionFromTTX(): 86 # https://github.com/fonttools/fonttools/issues/2370 87 font = TTFont() 88 assert font.sfntVersion == "\x00\x01\x00\x00" 89 ttx = io.StringIO(ttxOTF) 90 # Font is "empty", TTX file will determine sfntVersion 91 font.importXML(ttx) 92 assert font.sfntVersion == "OTTO" 93 ttx = io.StringIO(ttxTTF) 94 # Font is not "empty", sfntVersion in TTX file will be ignored 95 font.importXML(ttx) 96 assert font.sfntVersion == "OTTO" 97 98 99def test_virtualGlyphId(): 100 otfpath = os.path.join(DATA_DIR, "TestVGID-Regular.otf") 101 ttxpath = os.path.join(DATA_DIR, "TestVGID-Regular.ttx") 102 103 otf = TTFont(otfpath) 104 105 ttx = TTFont() 106 ttx.importXML(ttxpath) 107 108 with open(ttxpath, encoding="utf-8") as fp: 109 xml = normalize_TTX(fp.read()).splitlines() 110 111 for font in (otf, ttx): 112 GSUB = font["GSUB"].table 113 assert GSUB.LookupList.LookupCount == 37 114 lookup = GSUB.LookupList.Lookup[32] 115 assert lookup.LookupType == 8 116 subtable = lookup.SubTable[0] 117 assert subtable.LookAheadGlyphCount == 1 118 lookahead = subtable.LookAheadCoverage[0] 119 assert len(lookahead.glyphs) == 46 120 assert "glyph00453" in lookahead.glyphs 121 122 out = io.StringIO() 123 font.saveXML(out) 124 outxml = normalize_TTX(out.getvalue()).splitlines() 125 assert xml == outxml 126 127 128def test_setGlyphOrder_also_updates_glyf_glyphOrder(): 129 # https://github.com/fonttools/fonttools/issues/2060#issuecomment-1063932428 130 font = TTFont() 131 font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx")) 132 current_order = font.getGlyphOrder() 133 134 assert current_order == font["glyf"].glyphOrder 135 136 new_order = list(current_order) 137 while new_order == current_order: 138 random.shuffle(new_order) 139 140 font.setGlyphOrder(new_order) 141 142 assert font.getGlyphOrder() == new_order 143 assert font["glyf"].glyphOrder == new_order 144 145 146@pytest.mark.parametrize("lazy", [None, True, False]) 147def test_ensureDecompiled(lazy): 148 # test that no matter the lazy value, ensureDecompiled decompiles all tables 149 font = TTFont() 150 font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx")) 151 # test font has no OTL so we add some, as an example of otData-driven tables 152 addOpenTypeFeaturesFromString( 153 font, 154 """ 155 feature calt { 156 sub period' period' period' space by ellipsis; 157 } calt; 158 159 feature dist { 160 pos period period -30; 161 } dist; 162 """ 163 ) 164 # also add an additional cmap subtable that will be lazily-loaded 165 cm = CmapSubtable.newSubtable(14) 166 cm.platformID = 0 167 cm.platEncID = 5 168 cm.language = 0 169 cm.cmap = {} 170 cm.uvsDict = {0xFE00: [(0x002e, None)]} 171 font["cmap"].tables.append(cm) 172 173 # save and reload, potentially lazily 174 buf = io.BytesIO() 175 font.save(buf) 176 buf.seek(0) 177 font = TTFont(buf, lazy=lazy) 178 179 # check no table is loaded until/unless requested, no matter the laziness 180 for tag in font.keys(): 181 assert not font.isLoaded(tag) 182 183 if lazy is not False: 184 # additional cmap doesn't get decompiled automatically unless lazy=False; 185 # can't use hasattr or else cmap's maginc __getattr__ kicks in... 186 cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14) 187 assert cm.data is not None 188 assert "uvsDict" not in cm.__dict__ 189 # glyf glyphs are not expanded unless lazy=False 190 assert font["glyf"].glyphs["period"].data is not None 191 assert not hasattr(font["glyf"].glyphs["period"], "coordinates") 192 193 if lazy is True: 194 # OTL tables hold a 'reader' to lazily load when lazy=True 195 assert "reader" in font["GSUB"].table.LookupList.__dict__ 196 assert "reader" in font["GPOS"].table.LookupList.__dict__ 197 198 font.ensureDecompiled() 199 200 # all tables are decompiled now 201 for tag in font.keys(): 202 assert font.isLoaded(tag) 203 # including the additional cmap 204 cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14) 205 assert cm.data is None 206 assert "uvsDict" in cm.__dict__ 207 # expanded glyf glyphs lost the 'data' attribute 208 assert not hasattr(font["glyf"].glyphs["period"], "data") 209 assert hasattr(font["glyf"].glyphs["period"], "coordinates") 210 # and OTL tables have read their 'reader' 211 assert "reader" not in font["GSUB"].table.LookupList.__dict__ 212 assert "Lookup" in font["GSUB"].table.LookupList.__dict__ 213 assert "reader" not in font["GPOS"].table.LookupList.__dict__ 214 assert "Lookup" in font["GPOS"].table.LookupList.__dict__ 215