Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/tables/_f_v_a_r.py: 19%

150 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1from fontTools.misc import sstruct 

2from fontTools.misc.fixedTools import ( 

3 fixedToFloat as fi2fl, 

4 floatToFixed as fl2fi, 

5 floatToFixedToStr as fl2str, 

6 strToFixedToFloat as str2fl, 

7) 

8from fontTools.misc.textTools import Tag, bytesjoin, safeEval 

9from fontTools.ttLib import TTLibError 

10from . import DefaultTable 

11import struct 

12 

13 

14# Apple's documentation of 'fvar': 

15# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6fvar.html 

16 

17FVAR_HEADER_FORMAT = """ 

18 > # big endian 

19 version: L 

20 offsetToData: H 

21 countSizePairs: H 

22 axisCount: H 

23 axisSize: H 

24 instanceCount: H 

25 instanceSize: H 

26""" 

27 

28FVAR_AXIS_FORMAT = """ 

29 > # big endian 

30 axisTag: 4s 

31 minValue: 16.16F 

32 defaultValue: 16.16F 

33 maxValue: 16.16F 

34 flags: H 

35 axisNameID: H 

36""" 

37 

38FVAR_INSTANCE_FORMAT = """ 

39 > # big endian 

40 subfamilyNameID: H 

41 flags: H 

42""" 

43 

44 

45class table__f_v_a_r(DefaultTable.DefaultTable): 

46 dependencies = ["name"] 

47 

48 def __init__(self, tag=None): 

49 DefaultTable.DefaultTable.__init__(self, tag) 

50 self.axes = [] 

51 self.instances = [] 

52 

53 def compile(self, ttFont): 

54 instanceSize = sstruct.calcsize(FVAR_INSTANCE_FORMAT) + (len(self.axes) * 4) 

55 includePostScriptNames = any( 

56 instance.postscriptNameID != 0xFFFF for instance in self.instances 

57 ) 

58 if includePostScriptNames: 

59 instanceSize += 2 

60 header = { 

61 "version": 0x00010000, 

62 "offsetToData": sstruct.calcsize(FVAR_HEADER_FORMAT), 

63 "countSizePairs": 2, 

64 "axisCount": len(self.axes), 

65 "axisSize": sstruct.calcsize(FVAR_AXIS_FORMAT), 

66 "instanceCount": len(self.instances), 

67 "instanceSize": instanceSize, 

68 } 

69 result = [sstruct.pack(FVAR_HEADER_FORMAT, header)] 

70 result.extend([axis.compile() for axis in self.axes]) 

71 axisTags = [axis.axisTag for axis in self.axes] 

72 for instance in self.instances: 

73 result.append(instance.compile(axisTags, includePostScriptNames)) 

74 return bytesjoin(result) 

75 

76 def decompile(self, data, ttFont): 

77 header = {} 

78 headerSize = sstruct.calcsize(FVAR_HEADER_FORMAT) 

79 header = sstruct.unpack(FVAR_HEADER_FORMAT, data[0:headerSize]) 

80 if header["version"] != 0x00010000: 

81 raise TTLibError("unsupported 'fvar' version %04x" % header["version"]) 

82 pos = header["offsetToData"] 

83 axisSize = header["axisSize"] 

84 for _ in range(header["axisCount"]): 

85 axis = Axis() 

86 axis.decompile(data[pos : pos + axisSize]) 

87 self.axes.append(axis) 

88 pos += axisSize 

89 instanceSize = header["instanceSize"] 

90 axisTags = [axis.axisTag for axis in self.axes] 

91 for _ in range(header["instanceCount"]): 

92 instance = NamedInstance() 

93 instance.decompile(data[pos : pos + instanceSize], axisTags) 

94 self.instances.append(instance) 

95 pos += instanceSize 

96 

97 def toXML(self, writer, ttFont): 

98 for axis in self.axes: 

99 axis.toXML(writer, ttFont) 

100 for instance in self.instances: 

101 instance.toXML(writer, ttFont) 

102 

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

104 if name == "Axis": 

105 axis = Axis() 

106 axis.fromXML(name, attrs, content, ttFont) 

107 self.axes.append(axis) 

108 elif name == "NamedInstance": 

109 instance = NamedInstance() 

110 instance.fromXML(name, attrs, content, ttFont) 

111 self.instances.append(instance) 

112 

113 

114class Axis(object): 

115 def __init__(self): 

116 self.axisTag = None 

117 self.axisNameID = 0 

118 self.flags = 0 

119 self.minValue = -1.0 

120 self.defaultValue = 0.0 

121 self.maxValue = 1.0 

122 

123 def compile(self): 

124 return sstruct.pack(FVAR_AXIS_FORMAT, self) 

125 

126 def decompile(self, data): 

127 sstruct.unpack2(FVAR_AXIS_FORMAT, data, self) 

128 

129 def toXML(self, writer, ttFont): 

130 name = ( 

131 ttFont["name"].getDebugName(self.axisNameID) if "name" in ttFont else None 

132 ) 

133 if name is not None: 

134 writer.newline() 

135 writer.comment(name) 

136 writer.newline() 

137 writer.begintag("Axis") 

138 writer.newline() 

139 for tag, value in [ 

140 ("AxisTag", self.axisTag), 

141 ("Flags", "0x%X" % self.flags), 

142 ("MinValue", fl2str(self.minValue, 16)), 

143 ("DefaultValue", fl2str(self.defaultValue, 16)), 

144 ("MaxValue", fl2str(self.maxValue, 16)), 

145 ("AxisNameID", str(self.axisNameID)), 

146 ]: 

147 writer.begintag(tag) 

148 writer.write(value) 

149 writer.endtag(tag) 

150 writer.newline() 

151 writer.endtag("Axis") 

152 writer.newline() 

153 

154 def fromXML(self, name, _attrs, content, ttFont): 

155 assert name == "Axis" 

156 for tag, _, value in filter(lambda t: type(t) is tuple, content): 

157 value = "".join(value) 

158 if tag == "AxisTag": 

159 self.axisTag = Tag(value) 

160 elif tag in {"Flags", "MinValue", "DefaultValue", "MaxValue", "AxisNameID"}: 

161 setattr( 

162 self, 

163 tag[0].lower() + tag[1:], 

164 str2fl(value, 16) if tag.endswith("Value") else safeEval(value), 

165 ) 

166 

167 

168class NamedInstance(object): 

169 def __init__(self): 

170 self.subfamilyNameID = 0 

171 self.postscriptNameID = 0xFFFF 

172 self.flags = 0 

173 self.coordinates = {} 

174 

175 def compile(self, axisTags, includePostScriptName): 

176 result = [sstruct.pack(FVAR_INSTANCE_FORMAT, self)] 

177 for axis in axisTags: 

178 fixedCoord = fl2fi(self.coordinates[axis], 16) 

179 result.append(struct.pack(">l", fixedCoord)) 

180 if includePostScriptName: 

181 result.append(struct.pack(">H", self.postscriptNameID)) 

182 return bytesjoin(result) 

183 

184 def decompile(self, data, axisTags): 

185 sstruct.unpack2(FVAR_INSTANCE_FORMAT, data, self) 

186 pos = sstruct.calcsize(FVAR_INSTANCE_FORMAT) 

187 for axis in axisTags: 

188 value = struct.unpack(">l", data[pos : pos + 4])[0] 

189 self.coordinates[axis] = fi2fl(value, 16) 

190 pos += 4 

191 if pos + 2 <= len(data): 

192 self.postscriptNameID = struct.unpack(">H", data[pos : pos + 2])[0] 

193 else: 

194 self.postscriptNameID = 0xFFFF 

195 

196 def toXML(self, writer, ttFont): 

197 name = ( 

198 ttFont["name"].getDebugName(self.subfamilyNameID) 

199 if "name" in ttFont 

200 else None 

201 ) 

202 if name is not None: 

203 writer.newline() 

204 writer.comment(name) 

205 writer.newline() 

206 psname = ( 

207 ttFont["name"].getDebugName(self.postscriptNameID) 

208 if "name" in ttFont 

209 else None 

210 ) 

211 if psname is not None: 

212 writer.comment("PostScript: " + psname) 

213 writer.newline() 

214 if self.postscriptNameID == 0xFFFF: 

215 writer.begintag( 

216 "NamedInstance", 

217 flags=("0x%X" % self.flags), 

218 subfamilyNameID=self.subfamilyNameID, 

219 ) 

220 else: 

221 writer.begintag( 

222 "NamedInstance", 

223 flags=("0x%X" % self.flags), 

224 subfamilyNameID=self.subfamilyNameID, 

225 postscriptNameID=self.postscriptNameID, 

226 ) 

227 writer.newline() 

228 for axis in ttFont["fvar"].axes: 

229 writer.simpletag( 

230 "coord", 

231 axis=axis.axisTag, 

232 value=fl2str(self.coordinates[axis.axisTag], 16), 

233 ) 

234 writer.newline() 

235 writer.endtag("NamedInstance") 

236 writer.newline() 

237 

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

239 assert name == "NamedInstance" 

240 self.subfamilyNameID = safeEval(attrs["subfamilyNameID"]) 

241 self.flags = safeEval(attrs.get("flags", "0")) 

242 if "postscriptNameID" in attrs: 

243 self.postscriptNameID = safeEval(attrs["postscriptNameID"]) 

244 else: 

245 self.postscriptNameID = 0xFFFF 

246 

247 for tag, elementAttrs, _ in filter(lambda t: type(t) is tuple, content): 

248 if tag == "coord": 

249 value = str2fl(elementAttrs["value"], 16) 

250 self.coordinates[elementAttrs["axis"]] = value