Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fontTools/ttLib/ttGlyphSet.py: 20%

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

305 statements  

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, deepcopy 

7from types import SimpleNamespace 

8from fontTools.misc.vector import Vector 

9from fontTools.misc.fixedTools import otRound, fixedToFloat as fi2fl 

10from fontTools.misc.loggingTools import deprecateFunction 

11from fontTools.misc.transform import Transform, DecomposedTransform 

12from fontTools.pens.transformPen import TransformPen, TransformPointPen 

13from fontTools.pens.recordingPen import ( 

14 DecomposingRecordingPen, 

15 lerpRecordings, 

16 replayRecording, 

17) 

18 

19 

20class _TTGlyphSet(Mapping): 

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

22 glyph shape from TrueType or CFF. 

23 """ 

24 

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

26 self.recalcBounds = recalcBounds 

27 self.font = font 

28 self.defaultLocationNormalized = ( 

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

30 if "fvar" in self.font 

31 else {} 

32 ) 

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

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

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

36 self.depth = 0 

37 self.locationStack = [] 

38 self.rawLocationStack = [] 

39 self.glyphsMapping = glyphsMapping 

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

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

42 self.hvarTable = None 

43 if location: 

44 from fontTools.varLib.varStore import VarStoreInstancer 

45 

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

47 if self.hvarTable is not None: 

48 self.hvarInstancer = VarStoreInstancer( 

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

50 ) 

51 # TODO VVAR, VORG 

52 

53 @contextmanager 

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

55 self.locationStack.append(self.location) 

56 self.rawLocationStack.append(self.rawLocation) 

57 if reset: 

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

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

60 else: 

61 self.location = self.location.copy() 

62 self.rawLocation = {} 

63 self.location.update(location) 

64 self.rawLocation.update(location) 

65 

66 try: 

67 yield None 

68 finally: 

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

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

71 

72 @contextmanager 

73 def pushDepth(self): 

74 try: 

75 depth = self.depth 

76 self.depth += 1 

77 yield depth 

78 finally: 

79 self.depth -= 1 

80 

81 def __contains__(self, glyphName): 

82 return glyphName in self.glyphsMapping 

83 

84 def __iter__(self): 

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

86 

87 def __len__(self): 

88 return len(self.glyphsMapping) 

89 

90 @deprecateFunction( 

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

92 ) 

93 def has_key(self, glyphName): 

94 return glyphName in self.glyphsMapping 

95 

96 

97class _TTGlyphSetGlyf(_TTGlyphSet): 

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

99 self.glyfTable = font["glyf"] 

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

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

102 

103 def __getitem__(self, glyphName): 

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

105 

106 

107class _TTGlyphSetCFF(_TTGlyphSet): 

108 def __init__(self, font, location): 

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

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

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

112 self.setLocation(location) 

113 

114 def __getitem__(self, glyphName): 

115 return _TTGlyphCFF(self, glyphName) 

116 

117 def setLocation(self, location): 

118 self.blender = None 

119 if location: 

120 # TODO Optimize by using instancer.setLocation() 

121 

122 from fontTools.varLib.varStore import VarStoreInstancer 

123 

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

125 if varStore is not None: 

126 instancer = VarStoreInstancer( 

127 varStore.otVarStore, self.font["fvar"].axes, location 

128 ) 

129 self.blender = instancer.interpolateFromDeltas 

130 else: 

131 self.blender = None 

132 

133 @contextmanager 

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

135 self.setLocation(location) 

136 with _TTGlyphSet.pushLocation(self, location, reset) as value: 

137 try: 

138 yield value 

139 finally: 

140 self.setLocation(self.location) 

141 

142 

143class _TTGlyphSetVARC(_TTGlyphSet): 

144 def __init__(self, font, location, glyphSet): 

145 self.glyphSet = glyphSet 

146 super().__init__(font, location, glyphSet) 

147 self.varcTable = font["VARC"].table 

148 

149 def __getitem__(self, glyphName): 

150 varc = self.varcTable 

151 if glyphName not in varc.Coverage.glyphs: 

152 return self.glyphSet[glyphName] 

153 return _TTGlyphVARC(self, glyphName) 

154 

155 

156class _TTGlyph(ABC): 

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

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

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

160 the 'hmtx' table. 

161 

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

163 attributes. 

164 """ 

165 

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

167 self.glyphSet = glyphSet 

168 self.name = glyphName 

169 self.recalcBounds = recalcBounds 

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

171 if glyphSet.vMetrics is not None: 

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

173 else: 

174 self.height, self.tsb = None, None 

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

176 varidx = ( 

177 glyphSet.font.getGlyphID(glyphName) 

178 if glyphSet.hvarTable.AdvWidthMap is None 

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

180 ) 

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

182 # TODO: VVAR/VORG 

183 

184 @abstractmethod 

185 def draw(self, pen): 

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

187 how that works. 

188 """ 

189 raise NotImplementedError 

190 

191 def drawPoints(self, pen): 

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

193 how that works. 

194 """ 

195 from fontTools.pens.pointPen import SegmentToPointPen 

196 

197 self.draw(SegmentToPointPen(pen)) 

198 

199 

200class _TTGlyphGlyf(_TTGlyph): 

201 def draw(self, pen): 

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

203 how that works. 

204 """ 

205 glyph, offset = self._getGlyphAndOffset() 

206 

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

208 if depth: 

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

210 

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

212 

213 def drawPoints(self, pen): 

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

215 how that works. 

216 """ 

217 glyph, offset = self._getGlyphAndOffset() 

218 

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

220 if depth: 

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

222 

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

224 

225 def _getGlyphAndOffset(self): 

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

227 glyph = self._getGlyphInstance() 

228 else: 

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

230 

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

232 return glyph, offset 

233 

234 def _getGlyphInstance(self): 

235 from fontTools.varLib.iup import iup_delta 

236 from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates 

237 from fontTools.varLib.models import supportScalar 

238 

239 glyphSet = self.glyphSet 

240 glyfTable = glyphSet.glyfTable 

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

242 hMetrics = glyphSet.hMetrics 

243 vMetrics = glyphSet.vMetrics 

244 coordinates, _ = glyfTable._getCoordinatesAndControls( 

245 self.name, hMetrics, vMetrics 

246 ) 

247 origCoords, endPts = None, None 

248 for var in variations: 

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

250 if not scalar: 

251 continue 

252 delta = var.coordinates 

253 if None in delta: 

254 if origCoords is None: 

255 origCoords, control = glyfTable._getCoordinatesAndControls( 

256 self.name, hMetrics, vMetrics 

257 ) 

258 endPts = ( 

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

260 ) 

261 delta = iup_delta(delta, origCoords, endPts) 

262 coordinates += GlyphCoordinates(delta) * scalar 

263 

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

265 width, lsb, height, tsb = _setCoordinates( 

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

267 ) 

268 self.lsb = lsb 

269 self.tsb = tsb 

270 if glyphSet.hvarTable is None: 

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

272 self.width = width 

273 self.height = height 

274 return glyph 

275 

276 

277class _TTGlyphCFF(_TTGlyph): 

278 def draw(self, pen): 

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

280 how that works. 

281 """ 

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

283 

284 

285def _evaluateCondition(condition, fvarAxes, location, instancer): 

286 if condition.Format == 1: 

287 # ConditionAxisRange 

288 axisIndex = condition.AxisIndex 

289 axisTag = fvarAxes[axisIndex].axisTag 

290 axisValue = location.get(axisTag, 0) 

291 minValue = condition.FilterRangeMinValue 

292 maxValue = condition.FilterRangeMaxValue 

293 return minValue <= axisValue <= maxValue 

294 elif condition.Format == 2: 

295 # ConditionValue 

296 value = condition.DefaultValue 

297 value += instancer[condition.VarIdx][0] 

298 return value > 0 

299 elif condition.Format == 3: 

300 # ConditionAnd 

301 for subcondition in condition.ConditionTable: 

302 if not _evaluateCondition(subcondition, fvarAxes, location, instancer): 

303 return False 

304 return True 

305 elif condition.Format == 4: 

306 # ConditionOr 

307 for subcondition in condition.ConditionTable: 

308 if _evaluateCondition(subcondition, fvarAxes, location, instancer): 

309 return True 

310 return False 

311 elif condition.Format == 5: 

312 # ConditionNegate 

313 return not _evaluateCondition( 

314 condition.conditionTable, fvarAxes, location, instancer 

315 ) 

316 else: 

317 return False # Unkonwn condition format 

318 

319 

320class _TTGlyphVARC(_TTGlyph): 

321 def _draw(self, pen, isPointPen): 

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

323 how that works. 

324 """ 

325 from fontTools.ttLib.tables.otTables import ( 

326 VarComponentFlags, 

327 NO_VARIATION_INDEX, 

328 ) 

329 

330 glyphSet = self.glyphSet 

331 varc = glyphSet.varcTable 

332 idx = varc.Coverage.glyphs.index(self.name) 

333 glyph = varc.VarCompositeGlyphs.VarCompositeGlyph[idx] 

334 

335 from fontTools.varLib.multiVarStore import MultiVarStoreInstancer 

336 from fontTools.varLib.varStore import VarStoreInstancer 

337 

338 fvarAxes = glyphSet.font["fvar"].axes 

339 instancer = MultiVarStoreInstancer( 

340 varc.MultiVarStore, fvarAxes, self.glyphSet.location 

341 ) 

342 

343 for comp in glyph.components: 

344 if comp.flags & VarComponentFlags.HAVE_CONDITION: 

345 condition = varc.ConditionList.ConditionTable[comp.conditionIndex] 

346 if not _evaluateCondition( 

347 condition, fvarAxes, self.glyphSet.location, instancer 

348 ): 

349 continue 

350 

351 location = {} 

352 if comp.axisIndicesIndex is not None: 

353 axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex] 

354 axisValues = Vector(comp.axisValues) 

355 if comp.axisValuesVarIndex != NO_VARIATION_INDEX: 

356 axisValues += fi2fl(instancer[comp.axisValuesVarIndex], 14) 

357 assert len(axisIndices) == len(axisValues), ( 

358 len(axisIndices), 

359 len(axisValues), 

360 ) 

361 location = { 

362 fvarAxes[i].axisTag: v for i, v in zip(axisIndices, axisValues) 

363 } 

364 

365 if comp.transformVarIndex != NO_VARIATION_INDEX: 

366 deltas = instancer[comp.transformVarIndex] 

367 comp = deepcopy(comp) 

368 comp.applyTransformDeltas(deltas) 

369 transform = comp.transform 

370 

371 reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES 

372 with self.glyphSet.glyphSet.pushLocation(location, reset): 

373 with self.glyphSet.pushLocation(location, reset): 

374 shouldDecompose = self.name == comp.glyphName 

375 

376 if not shouldDecompose: 

377 try: 

378 pen.addVarComponent( 

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

380 ) 

381 except AttributeError: 

382 shouldDecompose = True 

383 

384 if shouldDecompose: 

385 t = transform.toTransform() 

386 compGlyphSet = ( 

387 self.glyphSet 

388 if comp.glyphName != self.name 

389 else glyphSet.glyphSet 

390 ) 

391 g = compGlyphSet[comp.glyphName] 

392 if isPointPen: 

393 tPen = TransformPointPen(pen, t) 

394 g.drawPoints(tPen) 

395 else: 

396 tPen = TransformPen(pen, t) 

397 g.draw(tPen) 

398 

399 def draw(self, pen): 

400 self._draw(pen, False) 

401 

402 def drawPoints(self, pen): 

403 self._draw(pen, True) 

404 

405 

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

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

408 assert len(coord) >= 4 

409 leftSideX = coord[-4][0] 

410 rightSideX = coord[-3][0] 

411 topSideY = coord[-2][1] 

412 bottomSideY = coord[-1][1] 

413 

414 for _ in range(4): 

415 del coord[-1] 

416 

417 if glyph.isComposite(): 

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

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

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

421 if hasattr(comp, "x"): 

422 comp.x, comp.y = p 

423 elif glyph.numberOfContours == 0: 

424 assert len(coord) == 0 

425 else: 

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

427 glyph.coordinates = coord 

428 

429 if recalcBounds: 

430 glyph.recalcBounds(glyfTable) 

431 

432 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 

433 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 

434 leftSideBearing = otRound(glyph.xMin - leftSideX) 

435 topSideBearing = otRound(topSideY - glyph.yMax) 

436 return ( 

437 horizontalAdvanceWidth, 

438 leftSideBearing, 

439 verticalAdvanceWidth, 

440 topSideBearing, 

441 ) 

442 

443 

444class LerpGlyphSet(Mapping): 

445 """A glyphset that interpolates between two other glyphsets. 

446 

447 Factor is typically between 0 and 1. 0 means the first glyphset, 

448 1 means the second glyphset, and 0.5 means the average of the 

449 two glyphsets. Other values are possible, and can be useful to 

450 extrapolate. Defaults to 0.5. 

451 """ 

452 

453 def __init__(self, glyphset1, glyphset2, factor=0.5): 

454 self.glyphset1 = glyphset1 

455 self.glyphset2 = glyphset2 

456 self.factor = factor 

457 

458 def __getitem__(self, glyphname): 

459 if glyphname in self.glyphset1 and glyphname in self.glyphset2: 

460 return LerpGlyph(glyphname, self) 

461 raise KeyError(glyphname) 

462 

463 def __contains__(self, glyphname): 

464 return glyphname in self.glyphset1 and glyphname in self.glyphset2 

465 

466 def __iter__(self): 

467 set1 = set(self.glyphset1) 

468 set2 = set(self.glyphset2) 

469 return iter(set1.intersection(set2)) 

470 

471 def __len__(self): 

472 set1 = set(self.glyphset1) 

473 set2 = set(self.glyphset2) 

474 return len(set1.intersection(set2)) 

475 

476 

477class LerpGlyph: 

478 def __init__(self, glyphname, glyphset): 

479 self.glyphset = glyphset 

480 self.glyphname = glyphname 

481 

482 def draw(self, pen): 

483 recording1 = DecomposingRecordingPen(self.glyphset.glyphset1) 

484 self.glyphset.glyphset1[self.glyphname].draw(recording1) 

485 recording2 = DecomposingRecordingPen(self.glyphset.glyphset2) 

486 self.glyphset.glyphset2[self.glyphname].draw(recording2) 

487 

488 factor = self.glyphset.factor 

489 

490 replayRecording(lerpRecordings(recording1.value, recording2.value, factor), pen)