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