• 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    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