Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fontTools/ttLib/tables/_h_m_t_x.py: 21%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

106 statements  

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)