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