• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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