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

185 statements  

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

1from collections import UserDict, deque 

2from functools import partial 

3from fontTools.misc import sstruct 

4from fontTools.misc.textTools import safeEval 

5from . import DefaultTable 

6import array 

7import itertools 

8import logging 

9import struct 

10import sys 

11import fontTools.ttLib.tables.TupleVariation as tv 

12 

13 

14log = logging.getLogger(__name__) 

15TupleVariation = tv.TupleVariation 

16 

17 

18# https://www.microsoft.com/typography/otspec/gvar.htm 

19# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm 

20# 

21# Apple's documentation of 'gvar': 

22# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html 

23# 

24# FreeType2 source code for parsing 'gvar': 

25# http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c 

26 

27GVAR_HEADER_FORMAT = """ 

28 > # big endian 

29 version: H 

30 reserved: H 

31 axisCount: H 

32 sharedTupleCount: H 

33 offsetToSharedTuples: I 

34 glyphCount: H 

35 flags: H 

36 offsetToGlyphVariationData: I 

37""" 

38 

39GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT) 

40 

41 

42class _LazyDict(UserDict): 

43 def __init__(self, data): 

44 super().__init__() 

45 self.data = data 

46 

47 def __getitem__(self, k): 

48 v = self.data[k] 

49 if callable(v): 

50 v = v() 

51 self.data[k] = v 

52 return v 

53 

54 

55class table__g_v_a_r(DefaultTable.DefaultTable): 

56 dependencies = ["fvar", "glyf"] 

57 

58 def __init__(self, tag=None): 

59 DefaultTable.DefaultTable.__init__(self, tag) 

60 self.version, self.reserved = 1, 0 

61 self.variations = {} 

62 

63 def compile(self, ttFont): 

64 axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] 

65 sharedTuples = tv.compileSharedTuples( 

66 axisTags, itertools.chain(*self.variations.values()) 

67 ) 

68 sharedTupleIndices = {coord: i for i, coord in enumerate(sharedTuples)} 

69 sharedTupleSize = sum([len(c) for c in sharedTuples]) 

70 compiledGlyphs = self.compileGlyphs_(ttFont, axisTags, sharedTupleIndices) 

71 offset = 0 

72 offsets = [] 

73 for glyph in compiledGlyphs: 

74 offsets.append(offset) 

75 offset += len(glyph) 

76 offsets.append(offset) 

77 compiledOffsets, tableFormat = self.compileOffsets_(offsets) 

78 

79 header = {} 

80 header["version"] = self.version 

81 header["reserved"] = self.reserved 

82 header["axisCount"] = len(axisTags) 

83 header["sharedTupleCount"] = len(sharedTuples) 

84 header["offsetToSharedTuples"] = GVAR_HEADER_SIZE + len(compiledOffsets) 

85 header["glyphCount"] = len(compiledGlyphs) 

86 header["flags"] = tableFormat 

87 header["offsetToGlyphVariationData"] = ( 

88 header["offsetToSharedTuples"] + sharedTupleSize 

89 ) 

90 compiledHeader = sstruct.pack(GVAR_HEADER_FORMAT, header) 

91 

92 result = [compiledHeader, compiledOffsets] 

93 result.extend(sharedTuples) 

94 result.extend(compiledGlyphs) 

95 return b"".join(result) 

96 

97 def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices): 

98 result = [] 

99 glyf = ttFont["glyf"] 

100 for glyphName in ttFont.getGlyphOrder(): 

101 variations = self.variations.get(glyphName, []) 

102 if not variations: 

103 result.append(b"") 

104 continue 

105 pointCountUnused = 0 # pointCount is actually unused by compileGlyph 

106 result.append( 

107 compileGlyph_( 

108 variations, pointCountUnused, axisTags, sharedCoordIndices 

109 ) 

110 ) 

111 return result 

112 

113 def decompile(self, data, ttFont): 

114 axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] 

115 glyphs = ttFont.getGlyphOrder() 

116 sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self) 

117 assert len(glyphs) == self.glyphCount 

118 assert len(axisTags) == self.axisCount 

119 offsets = self.decompileOffsets_( 

120 data[GVAR_HEADER_SIZE:], 

121 tableFormat=(self.flags & 1), 

122 glyphCount=self.glyphCount, 

123 ) 

124 sharedCoords = tv.decompileSharedTuples( 

125 axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples 

126 ) 

127 variations = {} 

128 offsetToData = self.offsetToGlyphVariationData 

129 glyf = ttFont["glyf"] 

130 

131 def decompileVarGlyph(glyphName, gid): 

132 gvarData = data[ 

133 offsetToData + offsets[gid] : offsetToData + offsets[gid + 1] 

134 ] 

135 if not gvarData: 

136 return [] 

137 glyph = glyf[glyphName] 

138 numPointsInGlyph = self.getNumPoints_(glyph) 

139 return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData) 

140 

141 for gid in range(self.glyphCount): 

142 glyphName = glyphs[gid] 

143 variations[glyphName] = partial(decompileVarGlyph, glyphName, gid) 

144 self.variations = _LazyDict(variations) 

145 

146 if ttFont.lazy is False: # Be lazy for None and True 

147 self.ensureDecompiled() 

148 

149 def ensureDecompiled(self, recurse=False): 

150 # The recurse argument is unused, but part of the signature of 

151 # ensureDecompiled across the library. 

152 # Use a zero-length deque to consume the lazy dict 

153 deque(self.variations.values(), maxlen=0) 

154 

155 @staticmethod 

156 def decompileOffsets_(data, tableFormat, glyphCount): 

157 if tableFormat == 0: 

158 # Short format: array of UInt16 

159 offsets = array.array("H") 

160 offsetsSize = (glyphCount + 1) * 2 

161 else: 

162 # Long format: array of UInt32 

163 offsets = array.array("I") 

164 offsetsSize = (glyphCount + 1) * 4 

165 offsets.frombytes(data[0:offsetsSize]) 

166 if sys.byteorder != "big": 

167 offsets.byteswap() 

168 

169 # In the short format, offsets need to be multiplied by 2. 

170 # This is not documented in Apple's TrueType specification, 

171 # but can be inferred from the FreeType implementation, and 

172 # we could verify it with two sample GX fonts. 

173 if tableFormat == 0: 

174 offsets = [off * 2 for off in offsets] 

175 

176 return offsets 

177 

178 @staticmethod 

179 def compileOffsets_(offsets): 

180 """Packs a list of offsets into a 'gvar' offset table. 

181 

182 Returns a pair (bytestring, tableFormat). Bytestring is the 

183 packed offset table. Format indicates whether the table 

184 uses short (tableFormat=0) or long (tableFormat=1) integers. 

185 The returned tableFormat should get packed into the flags field 

186 of the 'gvar' header. 

187 """ 

188 assert len(offsets) >= 2 

189 for i in range(1, len(offsets)): 

190 assert offsets[i - 1] <= offsets[i] 

191 if max(offsets) <= 0xFFFF * 2: 

192 packed = array.array("H", [n >> 1 for n in offsets]) 

193 tableFormat = 0 

194 else: 

195 packed = array.array("I", offsets) 

196 tableFormat = 1 

197 if sys.byteorder != "big": 

198 packed.byteswap() 

199 return (packed.tobytes(), tableFormat) 

200 

201 def toXML(self, writer, ttFont): 

202 writer.simpletag("version", value=self.version) 

203 writer.newline() 

204 writer.simpletag("reserved", value=self.reserved) 

205 writer.newline() 

206 axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] 

207 for glyphName in ttFont.getGlyphNames(): 

208 variations = self.variations.get(glyphName) 

209 if not variations: 

210 continue 

211 writer.begintag("glyphVariations", glyph=glyphName) 

212 writer.newline() 

213 for gvar in variations: 

214 gvar.toXML(writer, axisTags) 

215 writer.endtag("glyphVariations") 

216 writer.newline() 

217 

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

219 if name == "version": 

220 self.version = safeEval(attrs["value"]) 

221 elif name == "reserved": 

222 self.reserved = safeEval(attrs["value"]) 

223 elif name == "glyphVariations": 

224 if not hasattr(self, "variations"): 

225 self.variations = {} 

226 glyphName = attrs["glyph"] 

227 glyph = ttFont["glyf"][glyphName] 

228 numPointsInGlyph = self.getNumPoints_(glyph) 

229 glyphVariations = [] 

230 for element in content: 

231 if isinstance(element, tuple): 

232 name, attrs, content = element 

233 if name == "tuple": 

234 gvar = TupleVariation({}, [None] * numPointsInGlyph) 

235 glyphVariations.append(gvar) 

236 for tupleElement in content: 

237 if isinstance(tupleElement, tuple): 

238 tupleName, tupleAttrs, tupleContent = tupleElement 

239 gvar.fromXML(tupleName, tupleAttrs, tupleContent) 

240 self.variations[glyphName] = glyphVariations 

241 

242 @staticmethod 

243 def getNumPoints_(glyph): 

244 NUM_PHANTOM_POINTS = 4 

245 

246 if glyph.isComposite(): 

247 return len(glyph.components) + NUM_PHANTOM_POINTS 

248 elif glyph.isVarComposite(): 

249 count = 0 

250 for component in glyph.components: 

251 count += component.getPointCount() 

252 return count + NUM_PHANTOM_POINTS 

253 else: 

254 # Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute. 

255 return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS 

256 

257 

258def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices): 

259 tupleVariationCount, tuples, data = tv.compileTupleVariationStore( 

260 variations, pointCount, axisTags, sharedCoordIndices 

261 ) 

262 if tupleVariationCount == 0: 

263 return b"" 

264 result = [struct.pack(">HH", tupleVariationCount, 4 + len(tuples)), tuples, data] 

265 if (len(tuples) + len(data)) % 2 != 0: 

266 result.append(b"\0") # padding 

267 return b"".join(result) 

268 

269 

270def decompileGlyph_(pointCount, sharedTuples, axisTags, data): 

271 if len(data) < 4: 

272 return [] 

273 tupleVariationCount, offsetToData = struct.unpack(">HH", data[:4]) 

274 dataPos = offsetToData 

275 return tv.decompileTupleVariationStore( 

276 "gvar", 

277 axisTags, 

278 tupleVariationCount, 

279 pointCount, 

280 sharedTuples, 

281 data, 

282 4, 

283 offsetToData, 

284 )