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