| from __future__ import print_function, division, absolute_import |
| from __future__ import unicode_literals |
| from fontTools.misc.py23 import * |
| from fontTools.misc.testTools import parseXML, getXML |
| from fontTools.misc.textTools import deHexStr |
| from fontTools.ttLib import TTFont, newTable, TTLibError |
| from fontTools.misc.loggingTools import CapturingLogHandler |
| from fontTools.ttLib.tables._h_m_t_x import table__h_m_t_x, log |
| import struct |
| import unittest |
| |
| |
| class HmtxTableTest(unittest.TestCase): |
| |
| def __init__(self, methodName): |
| unittest.TestCase.__init__(self, methodName) |
| # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, |
| # and fires deprecation warnings if a program uses the old name. |
| if not hasattr(self, "assertRaisesRegex"): |
| self.assertRaisesRegex = self.assertRaisesRegexp |
| |
| @classmethod |
| def setUpClass(cls): |
| cls.tableClass = table__h_m_t_x |
| cls.tag = "hmtx" |
| |
| def makeFont(self, numGlyphs, numberOfMetrics): |
| font = TTFont() |
| maxp = font['maxp'] = newTable('maxp') |
| maxp.numGlyphs = numGlyphs |
| # from A to ... |
| font.glyphOrder = [chr(i) for i in range(65, 65+numGlyphs)] |
| headerTag = self.tableClass.headerTag |
| font[headerTag] = newTable(headerTag) |
| numberOfMetricsName = self.tableClass.numberOfMetricsName |
| setattr(font[headerTag], numberOfMetricsName, numberOfMetrics) |
| return font |
| |
| def test_decompile(self): |
| font = self.makeFont(numGlyphs=3, numberOfMetrics=3) |
| data = deHexStr("02A2 FFF5 0278 004F 02C6 0036") |
| |
| mtxTable = newTable(self.tag) |
| mtxTable.decompile(data, font) |
| |
| self.assertEqual(mtxTable['A'], (674, -11)) |
| self.assertEqual(mtxTable['B'], (632, 79)) |
| self.assertEqual(mtxTable['C'], (710, 54)) |
| |
| def test_decompile_additional_SB(self): |
| font = self.makeFont(numGlyphs=4, numberOfMetrics=2) |
| metrics = deHexStr("02A2 FFF5 0278 004F") |
| extraSideBearings = deHexStr("0036 FFFC") |
| data = metrics + extraSideBearings |
| |
| mtxTable = newTable(self.tag) |
| mtxTable.decompile(data, font) |
| |
| self.assertEqual(mtxTable['A'], (674, -11)) |
| self.assertEqual(mtxTable['B'], (632, 79)) |
| # all following have same width as the previous |
| self.assertEqual(mtxTable['C'], (632, 54)) |
| self.assertEqual(mtxTable['D'], (632, -4)) |
| |
| def test_decompile_not_enough_data(self): |
| font = self.makeFont(numGlyphs=1, numberOfMetrics=1) |
| mtxTable = newTable(self.tag) |
| msg = "not enough '%s' table data" % self.tag |
| |
| with self.assertRaisesRegex(TTLibError, msg): |
| mtxTable.decompile(b"\0\0\0", font) |
| |
| def test_decompile_too_much_data(self): |
| font = self.makeFont(numGlyphs=1, numberOfMetrics=1) |
| mtxTable = newTable(self.tag) |
| msg = "too much '%s' table data" % self.tag |
| |
| with CapturingLogHandler(log, "WARNING") as captor: |
| mtxTable.decompile(b"\0\0\0\0\0", font) |
| |
| self.assertTrue( |
| len([r for r in captor.records if msg == r.msg]) == 1) |
| |
| def test_decompile_num_metrics_greater_than_glyphs(self): |
| font = self.makeFont(numGlyphs=1, numberOfMetrics=2) |
| mtxTable = newTable(self.tag) |
| msg = "The %s.%s exceeds the maxp.numGlyphs" % ( |
| self.tableClass.headerTag, self.tableClass.numberOfMetricsName) |
| |
| with CapturingLogHandler(log, "WARNING") as captor: |
| mtxTable.decompile(b"\0\0\0\0", font) |
| |
| self.assertTrue( |
| len([r for r in captor.records if msg == r.msg]) == 1) |
| |
| def test_decompile_possibly_negative_advance(self): |
| font = self.makeFont(numGlyphs=1, numberOfMetrics=1) |
| # we warn if advance is > 0x7FFF as it might be interpreted as signed |
| # by some authoring tools |
| data = deHexStr("8000 0000") |
| mtxTable = newTable(self.tag) |
| |
| with CapturingLogHandler(log, "WARNING") as captor: |
| mtxTable.decompile(data, font) |
| |
| self.assertTrue( |
| len([r for r in captor.records |
| if "has a huge advance" in r.msg]) == 1) |
| |
| def test_decompile_no_header_table(self): |
| font = TTFont() |
| maxp = font['maxp'] = newTable('maxp') |
| maxp.numGlyphs = 3 |
| font.glyphOrder = ["A", "B", "C"] |
| |
| self.assertNotIn(self.tableClass.headerTag, font) |
| |
| data = deHexStr("0190 001E 0190 0028 0190 0032") |
| mtxTable = newTable(self.tag) |
| mtxTable.decompile(data, font) |
| |
| self.assertEqual( |
| mtxTable.metrics, |
| { |
| "A": (400, 30), |
| "B": (400, 40), |
| "C": (400, 50), |
| } |
| ) |
| |
| def test_compile(self): |
| # we set the wrong 'numberOfMetrics' to check it gets adjusted |
| font = self.makeFont(numGlyphs=3, numberOfMetrics=4) |
| mtxTable = font[self.tag] = newTable(self.tag) |
| mtxTable.metrics = { |
| 'A': (674, -11), |
| 'B': (632, 79), |
| 'C': (710, 54), |
| } |
| |
| data = mtxTable.compile(font) |
| |
| self.assertEqual(data, deHexStr("02A2 FFF5 0278 004F 02C6 0036")) |
| |
| headerTable = font[self.tableClass.headerTag] |
| self.assertEqual( |
| getattr(headerTable, self.tableClass.numberOfMetricsName), 3) |
| |
| def test_compile_additional_SB(self): |
| font = self.makeFont(numGlyphs=4, numberOfMetrics=1) |
| mtxTable = font[self.tag] = newTable(self.tag) |
| mtxTable.metrics = { |
| 'A': (632, -11), |
| 'B': (632, 79), |
| 'C': (632, 54), |
| 'D': (632, -4), |
| } |
| |
| data = mtxTable.compile(font) |
| |
| self.assertEqual(data, deHexStr("0278 FFF5 004F 0036 FFFC")) |
| |
| def test_compile_negative_advance(self): |
| font = self.makeFont(numGlyphs=1, numberOfMetrics=1) |
| mtxTable = font[self.tag] = newTable(self.tag) |
| mtxTable.metrics = {'A': [-1, 0]} |
| |
| with CapturingLogHandler(log, "ERROR") as captor: |
| with self.assertRaisesRegex(TTLibError, "negative advance"): |
| mtxTable.compile(font) |
| |
| self.assertTrue( |
| len([r for r in captor.records |
| if "Glyph 'A' has negative advance" in r.msg]) == 1) |
| |
| def test_compile_struct_out_of_range(self): |
| font = self.makeFont(numGlyphs=1, numberOfMetrics=1) |
| mtxTable = font[self.tag] = newTable(self.tag) |
| mtxTable.metrics = {'A': (0xFFFF+1, -0x8001)} |
| |
| with self.assertRaises(struct.error): |
| mtxTable.compile(font) |
| |
| def test_compile_round_float_values(self): |
| font = self.makeFont(numGlyphs=3, numberOfMetrics=2) |
| mtxTable = font[self.tag] = newTable(self.tag) |
| mtxTable.metrics = { |
| 'A': (0.5, 0.5), # round -> (1, 1) |
| 'B': (0.1, 0.9), # round -> (0, 1) |
| 'C': (0.1, 0.1), # round -> (0, 0) |
| } |
| |
| data = mtxTable.compile(font) |
| |
| self.assertEqual(data, deHexStr("0001 0001 0000 0001 0000")) |
| |
| def test_compile_no_header_table(self): |
| font = TTFont() |
| maxp = font['maxp'] = newTable('maxp') |
| maxp.numGlyphs = 3 |
| font.glyphOrder = [chr(i) for i in range(65, 68)] |
| mtxTable = font[self.tag] = newTable(self.tag) |
| mtxTable.metrics = { |
| "A": (400, 30), |
| "B": (400, 40), |
| "C": (400, 50), |
| } |
| |
| self.assertNotIn(self.tableClass.headerTag, font) |
| |
| data = mtxTable.compile(font) |
| |
| self.assertEqual(data, deHexStr("0190 001E 0190 0028 0190 0032")) |
| |
| def test_toXML(self): |
| font = self.makeFont(numGlyphs=2, numberOfMetrics=2) |
| mtxTable = font[self.tag] = newTable(self.tag) |
| mtxTable.metrics = {'B': (632, 79), 'A': (674, -11)} |
| |
| self.assertEqual( |
| getXML(mtxTable.toXML), |
| ('<mtx name="A" %s="674" %s="-11"/>\n' |
| '<mtx name="B" %s="632" %s="79"/>' % ( |
| (self.tableClass.advanceName, |
| self.tableClass.sideBearingName) * 2)).split('\n')) |
| |
| def test_fromXML(self): |
| mtxTable = newTable(self.tag) |
| |
| for name, attrs, content in parseXML( |
| '<mtx name="A" %s="674" %s="-11"/>' |
| '<mtx name="B" %s="632" %s="79"/>' % ( |
| (self.tableClass.advanceName, |
| self.tableClass.sideBearingName) * 2)): |
| mtxTable.fromXML(name, attrs, content, ttFont=None) |
| |
| self.assertEqual( |
| mtxTable.metrics, {'A': (674, -11), 'B': (632, 79)}) |
| |
| def test_delitem(self): |
| mtxTable = newTable(self.tag) |
| mtxTable.metrics = {'A': (0, 0)} |
| |
| del mtxTable['A'] |
| |
| self.assertTrue('A' not in mtxTable.metrics) |
| |
| def test_setitem(self): |
| mtxTable = newTable(self.tag) |
| mtxTable.metrics = {'A': (674, -11), 'B': (632, 79)} |
| mtxTable['B'] = [0, 0] # list is converted to tuple |
| |
| self.assertEqual(mtxTable.metrics, {'A': (674, -11), 'B': (0, 0)}) |
| |
| |
| if __name__ == "__main__": |
| import sys |
| sys.exit(unittest.main()) |