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

105 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 if len(data) < 4 * numberOfMetrics: 

44 raise ttLib.TTLibError("not enough '%s' table data" % self.tableTag) 

45 # Note: advanceWidth is unsigned, but some font editors might 

46 # read/write as signed. We can't be sure whether it was a mistake 

47 # or not, so we read as unsigned but also issue a warning... 

48 metricsFmt = ">" + self.longMetricFormat * numberOfMetrics 

49 metrics = struct.unpack(metricsFmt, data[: 4 * numberOfMetrics]) 

50 data = data[4 * numberOfMetrics :] 

51 numberOfSideBearings = numGlyphs - numberOfMetrics 

52 sideBearings = array.array("h", data[: 2 * numberOfSideBearings]) 

53 data = data[2 * numberOfSideBearings :] 

54 

55 if sys.byteorder != "big": 

56 sideBearings.byteswap() 

57 if data: 

58 log.warning("too much '%s' table data" % self.tableTag) 

59 self.metrics = {} 

60 glyphOrder = ttFont.getGlyphOrder() 

61 for i in range(numberOfMetrics): 

62 glyphName = glyphOrder[i] 

63 advanceWidth, lsb = metrics[i * 2 : i * 2 + 2] 

64 if advanceWidth > 32767: 

65 log.warning( 

66 "Glyph %r has a huge advance %s (%d); is it intentional or " 

67 "an (invalid) negative value?", 

68 glyphName, 

69 self.advanceName, 

70 advanceWidth, 

71 ) 

72 self.metrics[glyphName] = (advanceWidth, lsb) 

73 lastAdvance = metrics[-2] 

74 for i in range(numberOfSideBearings): 

75 glyphName = glyphOrder[i + numberOfMetrics] 

76 self.metrics[glyphName] = (lastAdvance, sideBearings[i]) 

77 

78 def compile(self, ttFont): 

79 metrics = [] 

80 hasNegativeAdvances = False 

81 for glyphName in ttFont.getGlyphOrder(): 

82 advanceWidth, sideBearing = self.metrics[glyphName] 

83 if advanceWidth < 0: 

84 log.error( 

85 "Glyph %r has negative advance %s" % (glyphName, self.advanceName) 

86 ) 

87 hasNegativeAdvances = True 

88 metrics.append([advanceWidth, sideBearing]) 

89 

90 headerTable = ttFont.get(self.headerTag) 

91 if headerTable is not None: 

92 lastAdvance = metrics[-1][0] 

93 lastIndex = len(metrics) 

94 while metrics[lastIndex - 2][0] == lastAdvance: 

95 lastIndex -= 1 

96 if lastIndex <= 1: 

97 # all advances are equal 

98 lastIndex = 1 

99 break 

100 additionalMetrics = metrics[lastIndex:] 

101 additionalMetrics = [otRound(sb) for _, sb in additionalMetrics] 

102 metrics = metrics[:lastIndex] 

103 numberOfMetrics = len(metrics) 

104 setattr(headerTable, self.numberOfMetricsName, numberOfMetrics) 

105 else: 

106 # no hhea/vhea, can't store numberOfMetrics; assume == numGlyphs 

107 numberOfMetrics = ttFont["maxp"].numGlyphs 

108 additionalMetrics = [] 

109 

110 allMetrics = [] 

111 for advance, sb in metrics: 

112 allMetrics.extend([otRound(advance), otRound(sb)]) 

113 metricsFmt = ">" + self.longMetricFormat * numberOfMetrics 

114 try: 

115 data = struct.pack(metricsFmt, *allMetrics) 

116 except struct.error as e: 

117 if "out of range" in str(e) and hasNegativeAdvances: 

118 raise ttLib.TTLibError( 

119 "'%s' table can't contain negative advance %ss" 

120 % (self.tableTag, self.advanceName) 

121 ) 

122 else: 

123 raise 

124 additionalMetrics = array.array("h", additionalMetrics) 

125 if sys.byteorder != "big": 

126 additionalMetrics.byteswap() 

127 data = data + additionalMetrics.tobytes() 

128 return data 

129 

130 def toXML(self, writer, ttFont): 

131 names = sorted(self.metrics.keys()) 

132 for glyphName in names: 

133 advance, sb = self.metrics[glyphName] 

134 writer.simpletag( 

135 "mtx", 

136 [ 

137 ("name", glyphName), 

138 (self.advanceName, advance), 

139 (self.sideBearingName, sb), 

140 ], 

141 ) 

142 writer.newline() 

143 

144 def fromXML(self, name, attrs, content, ttFont): 

145 if not hasattr(self, "metrics"): 

146 self.metrics = {} 

147 if name == "mtx": 

148 self.metrics[attrs["name"]] = ( 

149 safeEval(attrs[self.advanceName]), 

150 safeEval(attrs[self.sideBearingName]), 

151 ) 

152 

153 def __delitem__(self, glyphName): 

154 del self.metrics[glyphName] 

155 

156 def __getitem__(self, glyphName): 

157 return self.metrics[glyphName] 

158 

159 def __setitem__(self, glyphName, advance_sb_pair): 

160 self.metrics[glyphName] = tuple(advance_sb_pair)