1from fontTools.misc.loggingTools import CapturingLogHandler 2from fontTools.misc.textTools import tobytes 3from fontTools.ttLib import TTFont, TTLibError 4from fontTools.ttLib.tables.T_S_I__0 import table_T_S_I__0 5from fontTools.ttLib.tables.T_S_I__1 import table_T_S_I__1 6import pytest 7 8 9TSI1_DATA = b"""abcdefghijklmnopqrstuvxywz0123456789""" 10TSI1_UTF8_DATA = b"""abcd\xc3\xa9ghijklmnopqrstuvxywz0123456789""" 11 12 13@pytest.fixture 14def indextable(): 15 table = table_T_S_I__0() 16 table.set( 17 [ 18 (0, 1, 0), # gid 0, length=1, offset=0, text='a' 19 (1, 5, 1), # gid 1, length=5, offset=1, text='bcdef' 20 (2, 0, 1), # gid 2, length=0, offset=1, text='' 21 (3, 0, 1), # gid 3, length=0, offset=1, text='' 22 (4, 8, 6), 23 ], # gid 4, length=8, offset=6, text='ghijklmn' 24 [ 25 (0xFFFA, 2, 14), # 'ppgm', length=2, offset=14, text='op' 26 (0xFFFB, 4, 16), # 'cvt', length=4, offset=16, text='qrst' 27 (0xFFFC, 6, 20), # 'reserved', length=6, offset=20, text='uvxywz' 28 (0xFFFD, 10, 26), 29 ], # 'fpgm', length=10, offset=26, text='0123456789' 30 ) 31 return table 32 33 34@pytest.fixture 35def font(indextable): 36 font = TTFont() 37 # ['a', 'b', 'c', ...] 38 ch = 0x61 39 n = len(indextable.indices) 40 font.glyphOrder = [chr(i) for i in range(ch, ch + n)] 41 font["TSI0"] = indextable 42 return font 43 44 45@pytest.fixture 46def empty_font(): 47 font = TTFont() 48 font.glyphOrder = [] 49 indextable = table_T_S_I__0() 50 indextable.set([], [(0xFFFA, 0, 0), (0xFFFB, 0, 0), (0xFFFC, 0, 0), (0xFFFD, 0, 0)]) 51 font["TSI0"] = indextable 52 return font 53 54 55def test_decompile(font): 56 table = table_T_S_I__1() 57 table.decompile(TSI1_DATA, font) 58 59 assert table.glyphPrograms == { 60 "a": "a", 61 "b": "bcdef", 62 # 'c': '', # zero-length entries are skipped 63 # 'd': '', 64 "e": "ghijklmn", 65 } 66 assert table.extraPrograms == { 67 "ppgm": "op", 68 "cvt": "qrst", 69 "reserved": "uvxywz", 70 "fpgm": "0123456789", 71 } 72 73 74def test_decompile_utf8(font): 75 table = table_T_S_I__1() 76 table.decompile(TSI1_UTF8_DATA, font) 77 78 assert table.glyphPrograms == { 79 "a": "a", 80 "b": "bcd\u00e9", 81 # 'c': '', # zero-length entries are skipped 82 # 'd': '', 83 "e": "ghijklmn", 84 } 85 assert table.extraPrograms == { 86 "ppgm": "op", 87 "cvt": "qrst", 88 "reserved": "uvxywz", 89 "fpgm": "0123456789", 90 } 91 92 93def test_decompile_empty(empty_font): 94 table = table_T_S_I__1() 95 table.decompile(b"", empty_font) 96 97 assert table.glyphPrograms == {} 98 assert table.extraPrograms == {} 99 100 101def test_decompile_invalid_length(empty_font): 102 empty_font.glyphOrder = ["a"] 103 empty_font["TSI0"].indices = [(0, 0x8000 + 1, 0)] 104 105 table = table_T_S_I__1() 106 with pytest.raises(TTLibError) as excinfo: 107 table.decompile(b"", empty_font) 108 assert excinfo.match("textLength .* must not be > 32768") 109 110 111def test_decompile_offset_past_end(empty_font): 112 empty_font.glyphOrder = ["foo", "bar"] 113 content = "baz" 114 data = tobytes(content) 115 empty_font["TSI0"].indices = [(0, len(data), 0), (1, 1, len(data) + 1)] 116 117 table = table_T_S_I__1() 118 with CapturingLogHandler(table.log, "WARNING") as captor: 119 table.decompile(data, empty_font) 120 121 # the 'bar' program is skipped because its offset > len(data) 122 assert table.glyphPrograms == {"foo": "baz"} 123 assert any("textOffset > totalLength" in r.msg for r in captor.records) 124 125 126def test_decompile_magic_length_last_extra(empty_font): 127 indextable = empty_font["TSI0"] 128 indextable.extra_indices[-1] = (0xFFFD, 0x8000, 0) 129 content = "0" * (0x8000 + 1) 130 data = tobytes(content) 131 132 table = table_T_S_I__1() 133 table.decompile(data, empty_font) 134 135 assert table.extraPrograms["fpgm"] == content 136 137 138def test_decompile_magic_length_last_glyph(empty_font): 139 empty_font.glyphOrder = ["foo", "bar"] 140 indextable = empty_font["TSI0"] 141 indextable.indices = [ 142 (0, 3, 0), 143 (1, 0x8000, 3), 144 ] # the actual length of 'bar' program is 145 indextable.extra_indices = [ # the difference between the first extra's 146 (0xFFFA, 0, 0x8004), # offset and 'bar' offset: 0x8004 - 3 147 (0xFFFB, 0, 0x8004), 148 (0xFFFC, 0, 0x8004), 149 (0xFFFD, 0, 0x8004), 150 ] 151 foo_content = "0" * 3 152 bar_content = "1" * (0x8000 + 1) 153 data = tobytes(foo_content + bar_content) 154 155 table = table_T_S_I__1() 156 table.decompile(data, empty_font) 157 158 assert table.glyphPrograms["foo"] == foo_content 159 assert table.glyphPrograms["bar"] == bar_content 160 161 162def test_decompile_magic_length_non_last(empty_font): 163 indextable = empty_font["TSI0"] 164 indextable.extra_indices = [ 165 (0xFFFA, 3, 0), 166 (0xFFFB, 0x8000, 3), # the actual length of 'cvt' program is: 167 (0xFFFC, 0, 0x8004), # nextTextOffset - textOffset: 0x8004 - 3 168 (0xFFFD, 0, 0x8004), 169 ] 170 ppgm_content = "0" * 3 171 cvt_content = "1" * (0x8000 + 1) 172 data = tobytes(ppgm_content + cvt_content) 173 174 table = table_T_S_I__1() 175 table.decompile(data, empty_font) 176 177 assert table.extraPrograms["ppgm"] == ppgm_content 178 assert table.extraPrograms["cvt"] == cvt_content 179 180 table = table_T_S_I__1() 181 with CapturingLogHandler(table.log, "WARNING") as captor: 182 table.decompile(data[:-1], empty_font) # last entry is truncated 183 captor.assertRegex("nextTextOffset > totalLength") 184 assert table.extraPrograms["cvt"] == cvt_content[:-1] 185 186 187if __name__ == "__main__": 188 import sys 189 190 sys.exit(pytest.main(sys.argv)) 191