Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/ttGlyphSet.py: 20%

200 statements  

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

1"""GlyphSets returned by a TTFont.""" 

2 

3from abc import ABC, abstractmethod 

4from collections.abc import Mapping 

5from contextlib import contextmanager 

6from copy import copy 

7from types import SimpleNamespace 

8from fontTools.misc.fixedTools import otRound 

9from fontTools.misc.loggingTools import deprecateFunction 

10from fontTools.misc.transform import Transform 

11from fontTools.pens.transformPen import TransformPen, TransformPointPen 

12 

13 

14class _TTGlyphSet(Mapping): 

15 

16 """Generic dict-like GlyphSet class that pulls metrics from hmtx and 

17 glyph shape from TrueType or CFF. 

18 """ 

19 

20 def __init__(self, font, location, glyphsMapping, *, recalcBounds=True): 

21 self.recalcBounds = recalcBounds 

22 self.font = font 

23 self.defaultLocationNormalized = ( 

24 {axis.axisTag: 0 for axis in self.font["fvar"].axes} 

25 if "fvar" in self.font 

26 else {} 

27 ) 

28 self.location = location if location is not None else {} 

29 self.rawLocation = {} # VarComponent-only location 

30 self.originalLocation = location if location is not None else {} 

31 self.depth = 0 

32 self.locationStack = [] 

33 self.rawLocationStack = [] 

34 self.glyphsMapping = glyphsMapping 

35 self.hMetrics = font["hmtx"].metrics 

36 self.vMetrics = getattr(font.get("vmtx"), "metrics", None) 

37 self.hvarTable = None 

38 if location: 

39 from fontTools.varLib.varStore import VarStoreInstancer 

40 

41 self.hvarTable = getattr(font.get("HVAR"), "table", None) 

42 if self.hvarTable is not None: 

43 self.hvarInstancer = VarStoreInstancer( 

44 self.hvarTable.VarStore, font["fvar"].axes, location 

45 ) 

46 # TODO VVAR, VORG 

47 

48 @contextmanager 

49 def pushLocation(self, location, reset: bool): 

50 self.locationStack.append(self.location) 

51 self.rawLocationStack.append(self.rawLocation) 

52 if reset: 

53 self.location = self.originalLocation.copy() 

54 self.rawLocation = self.defaultLocationNormalized.copy() 

55 else: 

56 self.location = self.location.copy() 

57 self.rawLocation = {} 

58 self.location.update(location) 

59 self.rawLocation.update(location) 

60 

61 try: 

62 yield None 

63 finally: 

64 self.location = self.locationStack.pop() 

65 self.rawLocation = self.rawLocationStack.pop() 

66 

67 @contextmanager 

68 def pushDepth(self): 

69 try: 

70 depth = self.depth 

71 self.depth += 1 

72 yield depth 

73 finally: 

74 self.depth -= 1 

75 

76 def __contains__(self, glyphName): 

77 return glyphName in self.glyphsMapping 

78 

79 def __iter__(self): 

80 return iter(self.glyphsMapping.keys()) 

81 

82 def __len__(self): 

83 return len(self.glyphsMapping) 

84 

85 @deprecateFunction( 

86 "use 'glyphName in glyphSet' instead", category=DeprecationWarning 

87 ) 

88 def has_key(self, glyphName): 

89 return glyphName in self.glyphsMapping 

90 

91 

92class _TTGlyphSetGlyf(_TTGlyphSet): 

93 def __init__(self, font, location, recalcBounds=True): 

94 self.glyfTable = font["glyf"] 

95 super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds) 

96 self.gvarTable = font.get("gvar") 

97 

98 def __getitem__(self, glyphName): 

99 return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) 

100 

101 

102class _TTGlyphSetCFF(_TTGlyphSet): 

103 def __init__(self, font, location): 

104 tableTag = "CFF2" if "CFF2" in font else "CFF " 

105 self.charStrings = list(font[tableTag].cff.values())[0].CharStrings 

106 super().__init__(font, location, self.charStrings) 

107 self.blender = None 

108 if location: 

109 from fontTools.varLib.varStore import VarStoreInstancer 

110 

111 varStore = getattr(self.charStrings, "varStore", None) 

112 if varStore is not None: 

113 instancer = VarStoreInstancer( 

114 varStore.otVarStore, font["fvar"].axes, location 

115 ) 

116 self.blender = instancer.interpolateFromDeltas 

117 

118 def __getitem__(self, glyphName): 

119 return _TTGlyphCFF(self, glyphName) 

120 

121 

122class _TTGlyph(ABC): 

123 

124 """Glyph object that supports the Pen protocol, meaning that it has 

125 .draw() and .drawPoints() methods that take a pen object as their only 

126 argument. Additionally there are 'width' and 'lsb' attributes, read from 

127 the 'hmtx' table. 

128 

129 If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' 

130 attributes. 

131 """ 

132 

133 def __init__(self, glyphSet, glyphName, *, recalcBounds=True): 

134 self.glyphSet = glyphSet 

135 self.name = glyphName 

136 self.recalcBounds = recalcBounds 

137 self.width, self.lsb = glyphSet.hMetrics[glyphName] 

138 if glyphSet.vMetrics is not None: 

139 self.height, self.tsb = glyphSet.vMetrics[glyphName] 

140 else: 

141 self.height, self.tsb = None, None 

142 if glyphSet.location and glyphSet.hvarTable is not None: 

143 varidx = ( 

144 glyphSet.font.getGlyphID(glyphName) 

145 if glyphSet.hvarTable.AdvWidthMap is None 

146 else glyphSet.hvarTable.AdvWidthMap.mapping[glyphName] 

147 ) 

148 self.width += glyphSet.hvarInstancer[varidx] 

149 # TODO: VVAR/VORG 

150 

151 @abstractmethod 

152 def draw(self, pen): 

153 """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details 

154 how that works. 

155 """ 

156 raise NotImplementedError 

157 

158 def drawPoints(self, pen): 

159 """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details 

160 how that works. 

161 """ 

162 from fontTools.pens.pointPen import SegmentToPointPen 

163 

164 self.draw(SegmentToPointPen(pen)) 

165 

166 

167class _TTGlyphGlyf(_TTGlyph): 

168 def draw(self, pen): 

169 """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details 

170 how that works. 

171 """ 

172 glyph, offset = self._getGlyphAndOffset() 

173 

174 with self.glyphSet.pushDepth() as depth: 

175 if depth: 

176 offset = 0 # Offset should only apply at top-level 

177 

178 if glyph.isVarComposite(): 

179 self._drawVarComposite(glyph, pen, False) 

180 return 

181 

182 glyph.draw(pen, self.glyphSet.glyfTable, offset) 

183 

184 def drawPoints(self, pen): 

185 """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details 

186 how that works. 

187 """ 

188 glyph, offset = self._getGlyphAndOffset() 

189 

190 with self.glyphSet.pushDepth() as depth: 

191 if depth: 

192 offset = 0 # Offset should only apply at top-level 

193 

194 if glyph.isVarComposite(): 

195 self._drawVarComposite(glyph, pen, True) 

196 return 

197 

198 glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) 

199 

200 def _drawVarComposite(self, glyph, pen, isPointPen): 

201 from fontTools.ttLib.tables._g_l_y_f import ( 

202 VarComponentFlags, 

203 VAR_COMPONENT_TRANSFORM_MAPPING, 

204 ) 

205 

206 for comp in glyph.components: 

207 with self.glyphSet.pushLocation( 

208 comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES 

209 ): 

210 try: 

211 pen.addVarComponent( 

212 comp.glyphName, comp.transform, self.glyphSet.rawLocation 

213 ) 

214 except AttributeError: 

215 t = comp.transform.toTransform() 

216 if isPointPen: 

217 tPen = TransformPointPen(pen, t) 

218 self.glyphSet[comp.glyphName].drawPoints(tPen) 

219 else: 

220 tPen = TransformPen(pen, t) 

221 self.glyphSet[comp.glyphName].draw(tPen) 

222 

223 def _getGlyphAndOffset(self): 

224 if self.glyphSet.location and self.glyphSet.gvarTable is not None: 

225 glyph = self._getGlyphInstance() 

226 else: 

227 glyph = self.glyphSet.glyfTable[self.name] 

228 

229 offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 

230 return glyph, offset 

231 

232 def _getGlyphInstance(self): 

233 from fontTools.varLib.iup import iup_delta 

234 from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates 

235 from fontTools.varLib.models import supportScalar 

236 

237 glyphSet = self.glyphSet 

238 glyfTable = glyphSet.glyfTable 

239 variations = glyphSet.gvarTable.variations[self.name] 

240 hMetrics = glyphSet.hMetrics 

241 vMetrics = glyphSet.vMetrics 

242 coordinates, _ = glyfTable._getCoordinatesAndControls( 

243 self.name, hMetrics, vMetrics 

244 ) 

245 origCoords, endPts = None, None 

246 for var in variations: 

247 scalar = supportScalar(glyphSet.location, var.axes) 

248 if not scalar: 

249 continue 

250 delta = var.coordinates 

251 if None in delta: 

252 if origCoords is None: 

253 origCoords, control = glyfTable._getCoordinatesAndControls( 

254 self.name, hMetrics, vMetrics 

255 ) 

256 endPts = ( 

257 control[1] if control[0] >= 1 else list(range(len(control[1]))) 

258 ) 

259 delta = iup_delta(delta, origCoords, endPts) 

260 coordinates += GlyphCoordinates(delta) * scalar 

261 

262 glyph = copy(glyfTable[self.name]) # Shallow copy 

263 width, lsb, height, tsb = _setCoordinates( 

264 glyph, coordinates, glyfTable, recalcBounds=self.recalcBounds 

265 ) 

266 self.lsb = lsb 

267 self.tsb = tsb 

268 if glyphSet.hvarTable is None: 

269 # no HVAR: let's set metrics from the phantom points 

270 self.width = width 

271 self.height = height 

272 return glyph 

273 

274 

275class _TTGlyphCFF(_TTGlyph): 

276 def draw(self, pen): 

277 """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details 

278 how that works. 

279 """ 

280 self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender) 

281 

282 

283def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True): 

284 # Handle phantom points for (left, right, top, bottom) positions. 

285 assert len(coord) >= 4 

286 leftSideX = coord[-4][0] 

287 rightSideX = coord[-3][0] 

288 topSideY = coord[-2][1] 

289 bottomSideY = coord[-1][1] 

290 

291 for _ in range(4): 

292 del coord[-1] 

293 

294 if glyph.isComposite(): 

295 assert len(coord) == len(glyph.components) 

296 glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy 

297 for p, comp in zip(coord, glyph.components): 

298 if hasattr(comp, "x"): 

299 comp.x, comp.y = p 

300 elif glyph.isVarComposite(): 

301 glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy 

302 for comp in glyph.components: 

303 coord = comp.setCoordinates(coord) 

304 assert not coord 

305 elif glyph.numberOfContours == 0: 

306 assert len(coord) == 0 

307 else: 

308 assert len(coord) == len(glyph.coordinates) 

309 glyph.coordinates = coord 

310 

311 if recalcBounds: 

312 glyph.recalcBounds(glyfTable) 

313 

314 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 

315 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 

316 leftSideBearing = otRound(glyph.xMin - leftSideX) 

317 topSideBearing = otRound(topSideY - glyph.yMax) 

318 return ( 

319 horizontalAdvanceWidth, 

320 leftSideBearing, 

321 verticalAdvanceWidth, 

322 topSideBearing, 

323 )