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