1from fontTools.misc.testTools import parseXML, getXML 2from fontTools.misc.textTools import deHexStr 3from fontTools.ttLib import TTFont, newTable, TTLibError 4from fontTools.misc.loggingTools import CapturingLogHandler 5from fontTools.ttLib.tables._h_m_t_x import table__h_m_t_x, log 6import struct 7import unittest 8 9 10class HmtxTableTest(unittest.TestCase): 11 def __init__(self, methodName): 12 unittest.TestCase.__init__(self, methodName) 13 # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 14 # and fires deprecation warnings if a program uses the old name. 15 if not hasattr(self, "assertRaisesRegex"): 16 self.assertRaisesRegex = self.assertRaisesRegexp 17 18 @classmethod 19 def setUpClass(cls): 20 cls.tableClass = table__h_m_t_x 21 cls.tag = "hmtx" 22 23 def makeFont(self, numGlyphs, numberOfMetrics): 24 font = TTFont() 25 maxp = font["maxp"] = newTable("maxp") 26 maxp.numGlyphs = numGlyphs 27 # from A to ... 28 font.glyphOrder = [chr(i) for i in range(65, 65 + numGlyphs)] 29 headerTag = self.tableClass.headerTag 30 font[headerTag] = newTable(headerTag) 31 numberOfMetricsName = self.tableClass.numberOfMetricsName 32 setattr(font[headerTag], numberOfMetricsName, numberOfMetrics) 33 return font 34 35 def test_decompile(self): 36 font = self.makeFont(numGlyphs=3, numberOfMetrics=3) 37 data = deHexStr("02A2 FFF5 0278 004F 02C6 0036") 38 39 mtxTable = newTable(self.tag) 40 mtxTable.decompile(data, font) 41 42 self.assertEqual(mtxTable["A"], (674, -11)) 43 self.assertEqual(mtxTable["B"], (632, 79)) 44 self.assertEqual(mtxTable["C"], (710, 54)) 45 46 def test_decompile_additional_SB(self): 47 font = self.makeFont(numGlyphs=4, numberOfMetrics=2) 48 metrics = deHexStr("02A2 FFF5 0278 004F") 49 extraSideBearings = deHexStr("0036 FFFC") 50 data = metrics + extraSideBearings 51 52 mtxTable = newTable(self.tag) 53 mtxTable.decompile(data, font) 54 55 self.assertEqual(mtxTable["A"], (674, -11)) 56 self.assertEqual(mtxTable["B"], (632, 79)) 57 # all following have same width as the previous 58 self.assertEqual(mtxTable["C"], (632, 54)) 59 self.assertEqual(mtxTable["D"], (632, -4)) 60 61 def test_decompile_not_enough_data(self): 62 font = self.makeFont(numGlyphs=1, numberOfMetrics=1) 63 mtxTable = newTable(self.tag) 64 msg = "not enough '%s' table data" % self.tag 65 66 with self.assertRaisesRegex(TTLibError, msg): 67 mtxTable.decompile(b"\0\0\0", font) 68 69 def test_decompile_too_much_data(self): 70 font = self.makeFont(numGlyphs=1, numberOfMetrics=1) 71 mtxTable = newTable(self.tag) 72 msg = "too much '%s' table data" % self.tag 73 74 with CapturingLogHandler(log, "WARNING") as captor: 75 mtxTable.decompile(b"\0\0\0\0\0", font) 76 77 self.assertTrue(len([r for r in captor.records if msg == r.msg]) == 1) 78 79 def test_decompile_num_metrics_greater_than_glyphs(self): 80 font = self.makeFont(numGlyphs=1, numberOfMetrics=2) 81 mtxTable = newTable(self.tag) 82 msg = "The %s.%s exceeds the maxp.numGlyphs" % ( 83 self.tableClass.headerTag, 84 self.tableClass.numberOfMetricsName, 85 ) 86 87 with CapturingLogHandler(log, "WARNING") as captor: 88 mtxTable.decompile(b"\0\0\0\0", font) 89 90 self.assertTrue(len([r for r in captor.records if msg == r.msg]) == 1) 91 92 def test_decompile_possibly_negative_advance(self): 93 font = self.makeFont(numGlyphs=1, numberOfMetrics=1) 94 # we warn if advance is > 0x7FFF as it might be interpreted as signed 95 # by some authoring tools 96 data = deHexStr("8000 0000") 97 mtxTable = newTable(self.tag) 98 99 with CapturingLogHandler(log, "WARNING") as captor: 100 mtxTable.decompile(data, font) 101 102 self.assertTrue( 103 len([r for r in captor.records if "has a huge advance" in r.msg]) == 1 104 ) 105 106 def test_decompile_no_header_table(self): 107 font = TTFont() 108 maxp = font["maxp"] = newTable("maxp") 109 maxp.numGlyphs = 3 110 font.glyphOrder = ["A", "B", "C"] 111 112 self.assertNotIn(self.tableClass.headerTag, font) 113 114 data = deHexStr("0190 001E 0190 0028 0190 0032") 115 mtxTable = newTable(self.tag) 116 mtxTable.decompile(data, font) 117 118 self.assertEqual( 119 mtxTable.metrics, 120 { 121 "A": (400, 30), 122 "B": (400, 40), 123 "C": (400, 50), 124 }, 125 ) 126 127 def test_compile(self): 128 # we set the wrong 'numberOfMetrics' to check it gets adjusted 129 font = self.makeFont(numGlyphs=3, numberOfMetrics=4) 130 mtxTable = font[self.tag] = newTable(self.tag) 131 mtxTable.metrics = { 132 "A": (674, -11), 133 "B": (632, 79), 134 "C": (710, 54), 135 } 136 137 data = mtxTable.compile(font) 138 139 self.assertEqual(data, deHexStr("02A2 FFF5 0278 004F 02C6 0036")) 140 141 headerTable = font[self.tableClass.headerTag] 142 self.assertEqual(getattr(headerTable, self.tableClass.numberOfMetricsName), 3) 143 144 def test_compile_additional_SB(self): 145 font = self.makeFont(numGlyphs=4, numberOfMetrics=1) 146 mtxTable = font[self.tag] = newTable(self.tag) 147 mtxTable.metrics = { 148 "A": (632, -11), 149 "B": (632, 79), 150 "C": (632, 54), 151 "D": (632, -4), 152 } 153 154 data = mtxTable.compile(font) 155 156 self.assertEqual(data, deHexStr("0278 FFF5 004F 0036 FFFC")) 157 158 def test_compile_negative_advance(self): 159 font = self.makeFont(numGlyphs=1, numberOfMetrics=1) 160 mtxTable = font[self.tag] = newTable(self.tag) 161 mtxTable.metrics = {"A": [-1, 0]} 162 163 with CapturingLogHandler(log, "ERROR") as captor: 164 with self.assertRaisesRegex(TTLibError, "negative advance"): 165 mtxTable.compile(font) 166 167 self.assertTrue( 168 len( 169 [r for r in captor.records if "Glyph 'A' has negative advance" in r.msg] 170 ) 171 == 1 172 ) 173 174 def test_compile_struct_out_of_range(self): 175 font = self.makeFont(numGlyphs=1, numberOfMetrics=1) 176 mtxTable = font[self.tag] = newTable(self.tag) 177 mtxTable.metrics = {"A": (0xFFFF + 1, -0x8001)} 178 179 with self.assertRaises(struct.error): 180 mtxTable.compile(font) 181 182 def test_compile_round_float_values(self): 183 font = self.makeFont(numGlyphs=3, numberOfMetrics=2) 184 mtxTable = font[self.tag] = newTable(self.tag) 185 mtxTable.metrics = { 186 "A": (0.5, 0.5), # round -> (1, 1) 187 "B": (0.1, 0.9), # round -> (0, 1) 188 "C": (0.1, 0.1), # round -> (0, 0) 189 } 190 191 data = mtxTable.compile(font) 192 193 self.assertEqual(data, deHexStr("0001 0001 0000 0001 0000")) 194 195 def test_compile_no_header_table(self): 196 font = TTFont() 197 maxp = font["maxp"] = newTable("maxp") 198 maxp.numGlyphs = 3 199 font.glyphOrder = [chr(i) for i in range(65, 68)] 200 mtxTable = font[self.tag] = newTable(self.tag) 201 mtxTable.metrics = { 202 "A": (400, 30), 203 "B": (400, 40), 204 "C": (400, 50), 205 } 206 207 self.assertNotIn(self.tableClass.headerTag, font) 208 209 data = mtxTable.compile(font) 210 211 self.assertEqual(data, deHexStr("0190 001E 0190 0028 0190 0032")) 212 213 def test_toXML(self): 214 font = self.makeFont(numGlyphs=2, numberOfMetrics=2) 215 mtxTable = font[self.tag] = newTable(self.tag) 216 mtxTable.metrics = {"B": (632, 79), "A": (674, -11)} 217 218 self.assertEqual( 219 getXML(mtxTable.toXML), 220 ( 221 '<mtx name="A" %s="674" %s="-11"/>\n' 222 '<mtx name="B" %s="632" %s="79"/>' 223 % ((self.tableClass.advanceName, self.tableClass.sideBearingName) * 2) 224 ).split("\n"), 225 ) 226 227 def test_fromXML(self): 228 mtxTable = newTable(self.tag) 229 230 for name, attrs, content in parseXML( 231 '<mtx name="A" %s="674" %s="-11"/>' 232 '<mtx name="B" %s="632" %s="79"/>' 233 % ((self.tableClass.advanceName, self.tableClass.sideBearingName) * 2) 234 ): 235 mtxTable.fromXML(name, attrs, content, ttFont=None) 236 237 self.assertEqual(mtxTable.metrics, {"A": (674, -11), "B": (632, 79)}) 238 239 def test_delitem(self): 240 mtxTable = newTable(self.tag) 241 mtxTable.metrics = {"A": (0, 0)} 242 243 del mtxTable["A"] 244 245 self.assertTrue("A" not in mtxTable.metrics) 246 247 def test_setitem(self): 248 mtxTable = newTable(self.tag) 249 mtxTable.metrics = {"A": (674, -11), "B": (632, 79)} 250 mtxTable["B"] = [0, 0] # list is converted to tuple 251 252 self.assertEqual(mtxTable.metrics, {"A": (674, -11), "B": (0, 0)}) 253 254 255if __name__ == "__main__": 256 import sys 257 258 sys.exit(unittest.main()) 259