1*e1fe3e4aSElliott Hughesimport io 2*e1fe3e4aSElliott Hughesimport os 3*e1fe3e4aSElliott Hughesimport re 4*e1fe3e4aSElliott Hughesimport random 5*e1fe3e4aSElliott Hughesimport tempfile 6*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.builder import addOpenTypeFeaturesFromString 7*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import ( 8*e1fe3e4aSElliott Hughes TTFont, 9*e1fe3e4aSElliott Hughes TTLibError, 10*e1fe3e4aSElliott Hughes newTable, 11*e1fe3e4aSElliott Hughes registerCustomTableClass, 12*e1fe3e4aSElliott Hughes unregisterCustomTableClass, 13*e1fe3e4aSElliott Hughes) 14*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.standardGlyphOrder import standardGlyphOrder 15*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables.DefaultTable import DefaultTable 16*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._c_m_a_p import CmapSubtable 17*e1fe3e4aSElliott Hughesimport pytest 18*e1fe3e4aSElliott Hughes 19*e1fe3e4aSElliott Hughes 20*e1fe3e4aSElliott HughesDATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data") 21*e1fe3e4aSElliott Hughes 22*e1fe3e4aSElliott Hughes 23*e1fe3e4aSElliott Hughesclass CustomTableClass(DefaultTable): 24*e1fe3e4aSElliott Hughes def decompile(self, data, ttFont): 25*e1fe3e4aSElliott Hughes self.numbers = list(data) 26*e1fe3e4aSElliott Hughes 27*e1fe3e4aSElliott Hughes def compile(self, ttFont): 28*e1fe3e4aSElliott Hughes return bytes(self.numbers) 29*e1fe3e4aSElliott Hughes 30*e1fe3e4aSElliott Hughes # not testing XML read/write 31*e1fe3e4aSElliott Hughes 32*e1fe3e4aSElliott Hughes 33*e1fe3e4aSElliott Hughestable_C_U_S_T_ = CustomTableClass # alias for testing 34*e1fe3e4aSElliott Hughes 35*e1fe3e4aSElliott Hughes 36*e1fe3e4aSElliott HughesTABLETAG = "CUST" 37*e1fe3e4aSElliott Hughes 38*e1fe3e4aSElliott Hughes 39*e1fe3e4aSElliott Hughesdef normalize_TTX(string): 40*e1fe3e4aSElliott Hughes string = re.sub(' ttLibVersion=".*"', "", string) 41*e1fe3e4aSElliott Hughes string = re.sub('checkSumAdjustment value=".*"', "", string) 42*e1fe3e4aSElliott Hughes string = re.sub('modified value=".*"', "", string) 43*e1fe3e4aSElliott Hughes return string 44*e1fe3e4aSElliott Hughes 45*e1fe3e4aSElliott Hughes 46*e1fe3e4aSElliott Hughesdef test_registerCustomTableClass(): 47*e1fe3e4aSElliott Hughes font = TTFont() 48*e1fe3e4aSElliott Hughes font[TABLETAG] = newTable(TABLETAG) 49*e1fe3e4aSElliott Hughes font[TABLETAG].data = b"\x00\x01\xff" 50*e1fe3e4aSElliott Hughes f = io.BytesIO() 51*e1fe3e4aSElliott Hughes font.save(f) 52*e1fe3e4aSElliott Hughes f.seek(0) 53*e1fe3e4aSElliott Hughes assert font[TABLETAG].data == b"\x00\x01\xff" 54*e1fe3e4aSElliott Hughes registerCustomTableClass(TABLETAG, "ttFont_test", "CustomTableClass") 55*e1fe3e4aSElliott Hughes try: 56*e1fe3e4aSElliott Hughes font = TTFont(f) 57*e1fe3e4aSElliott Hughes assert font[TABLETAG].numbers == [0, 1, 255] 58*e1fe3e4aSElliott Hughes assert font[TABLETAG].compile(font) == b"\x00\x01\xff" 59*e1fe3e4aSElliott Hughes finally: 60*e1fe3e4aSElliott Hughes unregisterCustomTableClass(TABLETAG) 61*e1fe3e4aSElliott Hughes 62*e1fe3e4aSElliott Hughes 63*e1fe3e4aSElliott Hughesdef test_registerCustomTableClassStandardName(): 64*e1fe3e4aSElliott Hughes registerCustomTableClass(TABLETAG, "ttFont_test") 65*e1fe3e4aSElliott Hughes try: 66*e1fe3e4aSElliott Hughes font = TTFont() 67*e1fe3e4aSElliott Hughes font[TABLETAG] = newTable(TABLETAG) 68*e1fe3e4aSElliott Hughes font[TABLETAG].numbers = [4, 5, 6] 69*e1fe3e4aSElliott Hughes assert font[TABLETAG].compile(font) == b"\x04\x05\x06" 70*e1fe3e4aSElliott Hughes finally: 71*e1fe3e4aSElliott Hughes unregisterCustomTableClass(TABLETAG) 72*e1fe3e4aSElliott Hughes 73*e1fe3e4aSElliott Hughes 74*e1fe3e4aSElliott HughesttxTTF = r"""<?xml version="1.0" encoding="UTF-8"?> 75*e1fe3e4aSElliott Hughes<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.9.0"> 76*e1fe3e4aSElliott Hughes <hmtx> 77*e1fe3e4aSElliott Hughes <mtx name=".notdef" width="300" lsb="0"/> 78*e1fe3e4aSElliott Hughes </hmtx> 79*e1fe3e4aSElliott Hughes</ttFont> 80*e1fe3e4aSElliott Hughes""" 81*e1fe3e4aSElliott Hughes 82*e1fe3e4aSElliott Hughes 83*e1fe3e4aSElliott HughesttxOTF = """<?xml version="1.0" encoding="UTF-8"?> 84*e1fe3e4aSElliott Hughes<ttFont sfntVersion="OTTO" ttLibVersion="4.9.0"> 85*e1fe3e4aSElliott Hughes <hmtx> 86*e1fe3e4aSElliott Hughes <mtx name=".notdef" width="300" lsb="0"/> 87*e1fe3e4aSElliott Hughes </hmtx> 88*e1fe3e4aSElliott Hughes</ttFont> 89*e1fe3e4aSElliott Hughes""" 90*e1fe3e4aSElliott Hughes 91*e1fe3e4aSElliott Hughes 92*e1fe3e4aSElliott Hughesdef test_sfntVersionFromTTX(): 93*e1fe3e4aSElliott Hughes # https://github.com/fonttools/fonttools/issues/2370 94*e1fe3e4aSElliott Hughes font = TTFont() 95*e1fe3e4aSElliott Hughes assert font.sfntVersion == "\x00\x01\x00\x00" 96*e1fe3e4aSElliott Hughes ttx = io.StringIO(ttxOTF) 97*e1fe3e4aSElliott Hughes # Font is "empty", TTX file will determine sfntVersion 98*e1fe3e4aSElliott Hughes font.importXML(ttx) 99*e1fe3e4aSElliott Hughes assert font.sfntVersion == "OTTO" 100*e1fe3e4aSElliott Hughes ttx = io.StringIO(ttxTTF) 101*e1fe3e4aSElliott Hughes # Font is not "empty", sfntVersion in TTX file will be ignored 102*e1fe3e4aSElliott Hughes font.importXML(ttx) 103*e1fe3e4aSElliott Hughes assert font.sfntVersion == "OTTO" 104*e1fe3e4aSElliott Hughes 105*e1fe3e4aSElliott Hughes 106*e1fe3e4aSElliott Hughesdef test_virtualGlyphId(): 107*e1fe3e4aSElliott Hughes otfpath = os.path.join(DATA_DIR, "TestVGID-Regular.otf") 108*e1fe3e4aSElliott Hughes ttxpath = os.path.join(DATA_DIR, "TestVGID-Regular.ttx") 109*e1fe3e4aSElliott Hughes 110*e1fe3e4aSElliott Hughes otf = TTFont(otfpath) 111*e1fe3e4aSElliott Hughes 112*e1fe3e4aSElliott Hughes ttx = TTFont() 113*e1fe3e4aSElliott Hughes ttx.importXML(ttxpath) 114*e1fe3e4aSElliott Hughes 115*e1fe3e4aSElliott Hughes with open(ttxpath, encoding="utf-8") as fp: 116*e1fe3e4aSElliott Hughes xml = normalize_TTX(fp.read()).splitlines() 117*e1fe3e4aSElliott Hughes 118*e1fe3e4aSElliott Hughes for font in (otf, ttx): 119*e1fe3e4aSElliott Hughes GSUB = font["GSUB"].table 120*e1fe3e4aSElliott Hughes assert GSUB.LookupList.LookupCount == 37 121*e1fe3e4aSElliott Hughes lookup = GSUB.LookupList.Lookup[32] 122*e1fe3e4aSElliott Hughes assert lookup.LookupType == 8 123*e1fe3e4aSElliott Hughes subtable = lookup.SubTable[0] 124*e1fe3e4aSElliott Hughes assert subtable.LookAheadGlyphCount == 1 125*e1fe3e4aSElliott Hughes lookahead = subtable.LookAheadCoverage[0] 126*e1fe3e4aSElliott Hughes assert len(lookahead.glyphs) == 46 127*e1fe3e4aSElliott Hughes assert "glyph00453" in lookahead.glyphs 128*e1fe3e4aSElliott Hughes 129*e1fe3e4aSElliott Hughes out = io.StringIO() 130*e1fe3e4aSElliott Hughes font.saveXML(out) 131*e1fe3e4aSElliott Hughes outxml = normalize_TTX(out.getvalue()).splitlines() 132*e1fe3e4aSElliott Hughes assert xml == outxml 133*e1fe3e4aSElliott Hughes 134*e1fe3e4aSElliott Hughes 135*e1fe3e4aSElliott Hughesdef test_setGlyphOrder_also_updates_glyf_glyphOrder(): 136*e1fe3e4aSElliott Hughes # https://github.com/fonttools/fonttools/issues/2060#issuecomment-1063932428 137*e1fe3e4aSElliott Hughes font = TTFont() 138*e1fe3e4aSElliott Hughes font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx")) 139*e1fe3e4aSElliott Hughes current_order = font.getGlyphOrder() 140*e1fe3e4aSElliott Hughes 141*e1fe3e4aSElliott Hughes assert current_order == font["glyf"].glyphOrder 142*e1fe3e4aSElliott Hughes 143*e1fe3e4aSElliott Hughes new_order = list(current_order) 144*e1fe3e4aSElliott Hughes while new_order == current_order: 145*e1fe3e4aSElliott Hughes random.shuffle(new_order) 146*e1fe3e4aSElliott Hughes 147*e1fe3e4aSElliott Hughes font.setGlyphOrder(new_order) 148*e1fe3e4aSElliott Hughes 149*e1fe3e4aSElliott Hughes assert font.getGlyphOrder() == new_order 150*e1fe3e4aSElliott Hughes assert font["glyf"].glyphOrder == new_order 151*e1fe3e4aSElliott Hughes 152*e1fe3e4aSElliott Hughes 153*e1fe3e4aSElliott Hughesdef test_getGlyphOrder_not_true_post_format_1(caplog): 154*e1fe3e4aSElliott Hughes # https://github.com/fonttools/fonttools/issues/2736 155*e1fe3e4aSElliott Hughes caplog.set_level("WARNING") 156*e1fe3e4aSElliott Hughes font = TTFont(os.path.join(DATA_DIR, "bogus_post_format_1.ttf")) 157*e1fe3e4aSElliott Hughes hmtx = font["hmtx"] 158*e1fe3e4aSElliott Hughes assert len(hmtx.metrics) > len(standardGlyphOrder) 159*e1fe3e4aSElliott Hughes log_rec = caplog.records[-1] 160*e1fe3e4aSElliott Hughes assert log_rec.levelname == "WARNING" 161*e1fe3e4aSElliott Hughes assert "Not enough names found in the 'post' table" in log_rec.message 162*e1fe3e4aSElliott Hughes 163*e1fe3e4aSElliott Hughes 164*e1fe3e4aSElliott Hughes@pytest.mark.parametrize("lazy", [None, True, False]) 165*e1fe3e4aSElliott Hughesdef test_ensureDecompiled(lazy): 166*e1fe3e4aSElliott Hughes # test that no matter the lazy value, ensureDecompiled decompiles all tables 167*e1fe3e4aSElliott Hughes font = TTFont() 168*e1fe3e4aSElliott Hughes font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx")) 169*e1fe3e4aSElliott Hughes # test font has no OTL so we add some, as an example of otData-driven tables 170*e1fe3e4aSElliott Hughes addOpenTypeFeaturesFromString( 171*e1fe3e4aSElliott Hughes font, 172*e1fe3e4aSElliott Hughes """ 173*e1fe3e4aSElliott Hughes feature calt { 174*e1fe3e4aSElliott Hughes sub period' period' period' space by ellipsis; 175*e1fe3e4aSElliott Hughes } calt; 176*e1fe3e4aSElliott Hughes 177*e1fe3e4aSElliott Hughes feature dist { 178*e1fe3e4aSElliott Hughes pos period period -30; 179*e1fe3e4aSElliott Hughes } dist; 180*e1fe3e4aSElliott Hughes """, 181*e1fe3e4aSElliott Hughes ) 182*e1fe3e4aSElliott Hughes # also add an additional cmap subtable that will be lazily-loaded 183*e1fe3e4aSElliott Hughes cm = CmapSubtable.newSubtable(14) 184*e1fe3e4aSElliott Hughes cm.platformID = 0 185*e1fe3e4aSElliott Hughes cm.platEncID = 5 186*e1fe3e4aSElliott Hughes cm.language = 0 187*e1fe3e4aSElliott Hughes cm.cmap = {} 188*e1fe3e4aSElliott Hughes cm.uvsDict = {0xFE00: [(0x002E, None)]} 189*e1fe3e4aSElliott Hughes font["cmap"].tables.append(cm) 190*e1fe3e4aSElliott Hughes 191*e1fe3e4aSElliott Hughes # save and reload, potentially lazily 192*e1fe3e4aSElliott Hughes buf = io.BytesIO() 193*e1fe3e4aSElliott Hughes font.save(buf) 194*e1fe3e4aSElliott Hughes buf.seek(0) 195*e1fe3e4aSElliott Hughes font = TTFont(buf, lazy=lazy) 196*e1fe3e4aSElliott Hughes 197*e1fe3e4aSElliott Hughes # check no table is loaded until/unless requested, no matter the laziness 198*e1fe3e4aSElliott Hughes for tag in font.keys(): 199*e1fe3e4aSElliott Hughes assert not font.isLoaded(tag) 200*e1fe3e4aSElliott Hughes 201*e1fe3e4aSElliott Hughes if lazy is not False: 202*e1fe3e4aSElliott Hughes # additional cmap doesn't get decompiled automatically unless lazy=False; 203*e1fe3e4aSElliott Hughes # can't use hasattr or else cmap's maginc __getattr__ kicks in... 204*e1fe3e4aSElliott Hughes cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14) 205*e1fe3e4aSElliott Hughes assert cm.data is not None 206*e1fe3e4aSElliott Hughes assert "uvsDict" not in cm.__dict__ 207*e1fe3e4aSElliott Hughes # glyf glyphs are not expanded unless lazy=False 208*e1fe3e4aSElliott Hughes assert font["glyf"].glyphs["period"].data is not None 209*e1fe3e4aSElliott Hughes assert not hasattr(font["glyf"].glyphs["period"], "coordinates") 210*e1fe3e4aSElliott Hughes 211*e1fe3e4aSElliott Hughes if lazy is True: 212*e1fe3e4aSElliott Hughes # OTL tables hold a 'reader' to lazily load when lazy=True 213*e1fe3e4aSElliott Hughes assert "reader" in font["GSUB"].table.LookupList.__dict__ 214*e1fe3e4aSElliott Hughes assert "reader" in font["GPOS"].table.LookupList.__dict__ 215*e1fe3e4aSElliott Hughes 216*e1fe3e4aSElliott Hughes font.ensureDecompiled() 217*e1fe3e4aSElliott Hughes 218*e1fe3e4aSElliott Hughes # all tables are decompiled now 219*e1fe3e4aSElliott Hughes for tag in font.keys(): 220*e1fe3e4aSElliott Hughes assert font.isLoaded(tag) 221*e1fe3e4aSElliott Hughes # including the additional cmap 222*e1fe3e4aSElliott Hughes cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14) 223*e1fe3e4aSElliott Hughes assert cm.data is None 224*e1fe3e4aSElliott Hughes assert "uvsDict" in cm.__dict__ 225*e1fe3e4aSElliott Hughes # expanded glyf glyphs lost the 'data' attribute 226*e1fe3e4aSElliott Hughes assert not hasattr(font["glyf"].glyphs["period"], "data") 227*e1fe3e4aSElliott Hughes assert hasattr(font["glyf"].glyphs["period"], "coordinates") 228*e1fe3e4aSElliott Hughes # and OTL tables have read their 'reader' 229*e1fe3e4aSElliott Hughes assert "reader" not in font["GSUB"].table.LookupList.__dict__ 230*e1fe3e4aSElliott Hughes assert "Lookup" in font["GSUB"].table.LookupList.__dict__ 231*e1fe3e4aSElliott Hughes assert "reader" not in font["GPOS"].table.LookupList.__dict__ 232*e1fe3e4aSElliott Hughes assert "Lookup" in font["GPOS"].table.LookupList.__dict__ 233*e1fe3e4aSElliott Hughes 234*e1fe3e4aSElliott Hughes 235*e1fe3e4aSElliott Hughes@pytest.fixture 236*e1fe3e4aSElliott Hughesdef testFont_fvar_avar(): 237*e1fe3e4aSElliott Hughes ttxpath = os.path.join(DATA_DIR, "TestTTF_normalizeLocation.ttx") 238*e1fe3e4aSElliott Hughes ttf = TTFont() 239*e1fe3e4aSElliott Hughes ttf.importXML(ttxpath) 240*e1fe3e4aSElliott Hughes return ttf 241*e1fe3e4aSElliott Hughes 242*e1fe3e4aSElliott Hughes 243*e1fe3e4aSElliott Hughes@pytest.mark.parametrize( 244*e1fe3e4aSElliott Hughes "userLocation, expectedNormalizedLocation", 245*e1fe3e4aSElliott Hughes [ 246*e1fe3e4aSElliott Hughes ({}, {"wght": 0.0}), 247*e1fe3e4aSElliott Hughes ({"wght": 100}, {"wght": -1.0}), 248*e1fe3e4aSElliott Hughes ({"wght": 250}, {"wght": -0.75}), 249*e1fe3e4aSElliott Hughes ({"wght": 400}, {"wght": 0.0}), 250*e1fe3e4aSElliott Hughes ({"wght": 550}, {"wght": 0.75}), 251*e1fe3e4aSElliott Hughes ({"wght": 625}, {"wght": 0.875}), 252*e1fe3e4aSElliott Hughes ({"wght": 700}, {"wght": 1.0}), 253*e1fe3e4aSElliott Hughes ], 254*e1fe3e4aSElliott Hughes) 255*e1fe3e4aSElliott Hughesdef test_font_normalizeLocation( 256*e1fe3e4aSElliott Hughes testFont_fvar_avar, userLocation, expectedNormalizedLocation 257*e1fe3e4aSElliott Hughes): 258*e1fe3e4aSElliott Hughes normalizedLocation = testFont_fvar_avar.normalizeLocation(userLocation) 259*e1fe3e4aSElliott Hughes assert expectedNormalizedLocation == normalizedLocation 260*e1fe3e4aSElliott Hughes 261*e1fe3e4aSElliott Hughes 262*e1fe3e4aSElliott Hughesdef test_font_normalizeLocation_no_VF(): 263*e1fe3e4aSElliott Hughes ttf = TTFont() 264*e1fe3e4aSElliott Hughes with pytest.raises(TTLibError, match="Not a variable font"): 265*e1fe3e4aSElliott Hughes ttf.normalizeLocation({}) 266*e1fe3e4aSElliott Hughes 267*e1fe3e4aSElliott Hughes 268*e1fe3e4aSElliott Hughesdef test_getGlyphID(): 269*e1fe3e4aSElliott Hughes font = TTFont() 270*e1fe3e4aSElliott Hughes font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx")) 271*e1fe3e4aSElliott Hughes 272*e1fe3e4aSElliott Hughes assert font.getGlyphID("space") == 3 273*e1fe3e4aSElliott Hughes assert font.getGlyphID("glyph12345") == 12345 # virtual glyph 274*e1fe3e4aSElliott Hughes with pytest.raises(KeyError): 275*e1fe3e4aSElliott Hughes font.getGlyphID("non_existent") 276*e1fe3e4aSElliott Hughes with pytest.raises(KeyError): 277*e1fe3e4aSElliott Hughes font.getGlyphID("glyph_prefix_but_invalid_id") 278*e1fe3e4aSElliott Hughes 279*e1fe3e4aSElliott Hughes 280*e1fe3e4aSElliott Hughesdef test_spooled_tempfile_may_not_have_attribute_seekable(): 281*e1fe3e4aSElliott Hughes # SpooledTemporaryFile only got a seekable attribute on Python 3.11 282*e1fe3e4aSElliott Hughes # https://github.com/fonttools/fonttools/issues/3052 283*e1fe3e4aSElliott Hughes font = TTFont() 284*e1fe3e4aSElliott Hughes font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx")) 285*e1fe3e4aSElliott Hughes tmp = tempfile.SpooledTemporaryFile() 286*e1fe3e4aSElliott Hughes font.save(tmp) 287*e1fe3e4aSElliott Hughes # this should not fail 288*e1fe3e4aSElliott Hughes _ = TTFont(tmp) 289*e1fe3e4aSElliott Hughes 290*e1fe3e4aSElliott Hughes 291*e1fe3e4aSElliott Hughesdef test_unseekable_file_lazy_loading_fails(): 292*e1fe3e4aSElliott Hughes class NonSeekableFile: 293*e1fe3e4aSElliott Hughes def __init__(self): 294*e1fe3e4aSElliott Hughes self.file = io.BytesIO() 295*e1fe3e4aSElliott Hughes 296*e1fe3e4aSElliott Hughes def read(self, size): 297*e1fe3e4aSElliott Hughes return self.file.read(size) 298*e1fe3e4aSElliott Hughes 299*e1fe3e4aSElliott Hughes def seekable(self): 300*e1fe3e4aSElliott Hughes return False 301*e1fe3e4aSElliott Hughes 302*e1fe3e4aSElliott Hughes f = NonSeekableFile() 303*e1fe3e4aSElliott Hughes with pytest.raises(TTLibError, match="Input file must be seekable when lazy=True"): 304*e1fe3e4aSElliott Hughes TTFont(f, lazy=True) 305*e1fe3e4aSElliott Hughes 306*e1fe3e4aSElliott Hughes 307*e1fe3e4aSElliott Hughesdef test_unsupported_seek_operation_lazy_loading_fails(): 308*e1fe3e4aSElliott Hughes class UnsupportedSeekFile: 309*e1fe3e4aSElliott Hughes def __init__(self): 310*e1fe3e4aSElliott Hughes self.file = io.BytesIO() 311*e1fe3e4aSElliott Hughes 312*e1fe3e4aSElliott Hughes def read(self, size): 313*e1fe3e4aSElliott Hughes return self.file.read(size) 314*e1fe3e4aSElliott Hughes 315*e1fe3e4aSElliott Hughes def seek(self, offset): 316*e1fe3e4aSElliott Hughes raise io.UnsupportedOperation("Unsupported seek operation") 317*e1fe3e4aSElliott Hughes 318*e1fe3e4aSElliott Hughes f = UnsupportedSeekFile() 319*e1fe3e4aSElliott Hughes with pytest.raises(TTLibError, match="Input file must be seekable when lazy=True"): 320*e1fe3e4aSElliott Hughes TTFont(f, lazy=True) 321