2016-10-14 15:02:09 +01:00
|
|
|
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)
|
|
|
|
|
2016-10-14 16:47:44 +01:00
|
|
|
self.assertEqual(mtxTable["A"], (674, -11))
|
|
|
|
self.assertEqual(mtxTable["B"], (632, 79))
|
|
|
|
self.assertEqual(mtxTable["C"], (710, 54))
|
2016-10-14 15:02:09 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2016-10-14 16:47:44 +01:00
|
|
|
self.assertEqual(mtxTable["A"], (674, -11))
|
|
|
|
self.assertEqual(mtxTable["B"], (632, 79))
|
2016-10-14 15:02:09 +01:00
|
|
|
# all following have same width as the previous
|
2016-10-14 16:47:44 +01:00
|
|
|
self.assertEqual(mtxTable["C"], (632, 54))
|
|
|
|
self.assertEqual(mtxTable["D"], (632, -4))
|
2016-10-14 15:02:09 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2019-01-14 13:45:24 +00:00
|
|
|
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),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2016-10-14 15:02:09 +01:00
|
|
|
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 = {
|
2016-10-14 16:47:44 +01:00
|
|
|
"A": (674, -11),
|
|
|
|
"B": (632, 79),
|
|
|
|
"C": (710, 54),
|
2016-10-14 15:02:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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 = {
|
2016-10-14 16:47:44 +01:00
|
|
|
"A": (632, -11),
|
|
|
|
"B": (632, 79),
|
|
|
|
"C": (632, 54),
|
|
|
|
"D": (632, -4),
|
2016-10-14 15:02:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2016-10-14 16:47:44 +01:00
|
|
|
mtxTable.metrics = {"A": (0xFFFF + 1, -0x8001)}
|
2016-10-14 15:02:09 +01:00
|
|
|
|
|
|
|
with self.assertRaises(struct.error):
|
|
|
|
mtxTable.compile(font)
|
|
|
|
|
2016-10-21 12:42:01 +01:00
|
|
|
def test_compile_round_float_values(self):
|
|
|
|
font = self.makeFont(numGlyphs=3, numberOfMetrics=2)
|
|
|
|
mtxTable = font[self.tag] = newTable(self.tag)
|
|
|
|
mtxTable.metrics = {
|
2018-06-14 17:49:37 +01:00
|
|
|
"A": (0.5, 0.5), # round -> (1, 1)
|
2016-10-21 12:42:01 +01:00
|
|
|
"B": (0.1, 0.9), # round -> (0, 1)
|
|
|
|
"C": (0.1, 0.1), # round -> (0, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
data = mtxTable.compile(font)
|
|
|
|
|
2018-06-14 17:49:37 +01:00
|
|
|
self.assertEqual(data, deHexStr("0001 0001 0000 0001 0000"))
|
2016-10-21 12:42:01 +01:00
|
|
|
|
2019-01-14 13:45:24 +00:00
|
|
|
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"))
|
|
|
|
|
2016-10-14 15:02:09 +01:00
|
|
|
def test_toXML(self):
|
|
|
|
font = self.makeFont(numGlyphs=2, numberOfMetrics=2)
|
|
|
|
mtxTable = font[self.tag] = newTable(self.tag)
|
2016-10-14 16:47:44 +01:00
|
|
|
mtxTable.metrics = {"B": (632, 79), "A": (674, -11)}
|
2016-10-14 15:02:09 +01:00
|
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
getXML(mtxTable.toXML),
|
2016-12-21 13:24:51 +00:00
|
|
|
(
|
|
|
|
'<mtx name="A" %s="674" %s="-11"/>\n'
|
|
|
|
'<mtx name="B" %s="632" %s="79"/>'
|
|
|
|
% ((self.tableClass.advanceName, self.tableClass.sideBearingName) * 2)
|
|
|
|
).split("\n"),
|
2022-12-13 11:26:36 +00:00
|
|
|
)
|
2016-10-14 15:02:09 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2016-10-14 16:47:44 +01:00
|
|
|
self.assertEqual(mtxTable.metrics, {"A": (674, -11), "B": (632, 79)})
|
2016-10-14 15:02:09 +01:00
|
|
|
|
|
|
|
def test_delitem(self):
|
|
|
|
mtxTable = newTable(self.tag)
|
2016-10-14 16:47:44 +01:00
|
|
|
mtxTable.metrics = {"A": (0, 0)}
|
2016-10-14 15:02:09 +01:00
|
|
|
|
|
|
|
del mtxTable["A"]
|
|
|
|
|
|
|
|
self.assertTrue("A" not in mtxTable.metrics)
|
|
|
|
|
|
|
|
def test_setitem(self):
|
|
|
|
mtxTable = newTable(self.tag)
|
2016-10-14 16:47:44 +01:00
|
|
|
mtxTable.metrics = {"A": (674, -11), "B": (632, 79)}
|
|
|
|
mtxTable["B"] = [0, 0] # list is converted to tuple
|
2016-10-14 15:02:09 +01:00
|
|
|
|
2016-10-14 16:47:44 +01:00
|
|
|
self.assertEqual(mtxTable.metrics, {"A": (674, -11), "B": (0, 0)})
|
2016-10-14 15:02:09 +01:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2017-01-11 13:05:35 +00:00
|
|
|
import sys
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2017-01-11 13:05:35 +00:00
|
|
|
sys.exit(unittest.main())
|