1from fontTools.misc.roundTools import otRound
2from fontTools import ttLib
3from fontTools.misc.textTools import safeEval
4from . import DefaultTable
5import sys
6import struct
7import array
8import logging
9
10
11log = logging.getLogger(__name__)
12
13
14class table__h_m_t_x(DefaultTable.DefaultTable):
15 headerTag = "hhea"
16 advanceName = "width"
17 sideBearingName = "lsb"
18 numberOfMetricsName = "numberOfHMetrics"
19 longMetricFormat = "Hh"
20
21 def decompile(self, data, ttFont):
22 numGlyphs = ttFont["maxp"].numGlyphs
23 headerTable = ttFont.get(self.headerTag)
24 if headerTable is not None:
25 numberOfMetrics = int(getattr(headerTable, self.numberOfMetricsName))
26 else:
27 numberOfMetrics = numGlyphs
28 if numberOfMetrics > numGlyphs:
29 log.warning(
30 "The %s.%s exceeds the maxp.numGlyphs"
31 % (self.headerTag, self.numberOfMetricsName)
32 )
33 numberOfMetrics = numGlyphs
34 if len(data) < 4 * numberOfMetrics:
35 raise ttLib.TTLibError("not enough '%s' table data" % self.tableTag)
36 # Note: advanceWidth is unsigned, but some font editors might
37 # read/write as signed. We can't be sure whether it was a mistake
38 # or not, so we read as unsigned but also issue a warning...
39 metricsFmt = ">" + self.longMetricFormat * numberOfMetrics
40 metrics = struct.unpack(metricsFmt, data[: 4 * numberOfMetrics])
41 data = data[4 * numberOfMetrics :]
42 numberOfSideBearings = numGlyphs - numberOfMetrics
43 sideBearings = array.array("h", data[: 2 * numberOfSideBearings])
44 data = data[2 * numberOfSideBearings :]
45
46 if sys.byteorder != "big":
47 sideBearings.byteswap()
48 if data:
49 log.warning("too much '%s' table data" % self.tableTag)
50 self.metrics = {}
51 glyphOrder = ttFont.getGlyphOrder()
52 for i in range(numberOfMetrics):
53 glyphName = glyphOrder[i]
54 advanceWidth, lsb = metrics[i * 2 : i * 2 + 2]
55 if advanceWidth > 32767:
56 log.warning(
57 "Glyph %r has a huge advance %s (%d); is it intentional or "
58 "an (invalid) negative value?",
59 glyphName,
60 self.advanceName,
61 advanceWidth,
62 )
63 self.metrics[glyphName] = (advanceWidth, lsb)
64 lastAdvance = metrics[-2]
65 for i in range(numberOfSideBearings):
66 glyphName = glyphOrder[i + numberOfMetrics]
67 self.metrics[glyphName] = (lastAdvance, sideBearings[i])
68
69 def compile(self, ttFont):
70 metrics = []
71 hasNegativeAdvances = False
72 for glyphName in ttFont.getGlyphOrder():
73 advanceWidth, sideBearing = self.metrics[glyphName]
74 if advanceWidth < 0:
75 log.error(
76 "Glyph %r has negative advance %s" % (glyphName, self.advanceName)
77 )
78 hasNegativeAdvances = True
79 metrics.append([advanceWidth, sideBearing])
80
81 headerTable = ttFont.get(self.headerTag)
82 if headerTable is not None:
83 lastAdvance = metrics[-1][0]
84 lastIndex = len(metrics)
85 while metrics[lastIndex - 2][0] == lastAdvance:
86 lastIndex -= 1
87 if lastIndex <= 1:
88 # all advances are equal
89 lastIndex = 1
90 break
91 additionalMetrics = metrics[lastIndex:]
92 additionalMetrics = [otRound(sb) for _, sb in additionalMetrics]
93 metrics = metrics[:lastIndex]
94 numberOfMetrics = len(metrics)
95 setattr(headerTable, self.numberOfMetricsName, numberOfMetrics)
96 else:
97 # no hhea/vhea, can't store numberOfMetrics; assume == numGlyphs
98 numberOfMetrics = ttFont["maxp"].numGlyphs
99 additionalMetrics = []
100
101 allMetrics = []
102 for advance, sb in metrics:
103 allMetrics.extend([otRound(advance), otRound(sb)])
104 metricsFmt = ">" + self.longMetricFormat * numberOfMetrics
105 try:
106 data = struct.pack(metricsFmt, *allMetrics)
107 except struct.error as e:
108 if "out of range" in str(e) and hasNegativeAdvances:
109 raise ttLib.TTLibError(
110 "'%s' table can't contain negative advance %ss"
111 % (self.tableTag, self.advanceName)
112 )
113 else:
114 raise
115 additionalMetrics = array.array("h", additionalMetrics)
116 if sys.byteorder != "big":
117 additionalMetrics.byteswap()
118 data = data + additionalMetrics.tobytes()
119 return data
120
121 def toXML(self, writer, ttFont):
122 names = sorted(self.metrics.keys())
123 for glyphName in names:
124 advance, sb = self.metrics[glyphName]
125 writer.simpletag(
126 "mtx",
127 [
128 ("name", glyphName),
129 (self.advanceName, advance),
130 (self.sideBearingName, sb),
131 ],
132 )
133 writer.newline()
134
135 def fromXML(self, name, attrs, content, ttFont):
136 if not hasattr(self, "metrics"):
137 self.metrics = {}
138 if name == "mtx":
139 self.metrics[attrs["name"]] = (
140 safeEval(attrs[self.advanceName]),
141 safeEval(attrs[self.sideBearingName]),
142 )
143
144 def __delitem__(self, glyphName):
145 del self.metrics[glyphName]
146
147 def __getitem__(self, glyphName):
148 return self.metrics[glyphName]
149
150 def __setitem__(self, glyphName, advance_sb_pair):
151 self.metrics[glyphName] = tuple(advance_sb_pair)