Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

301 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 _TTGlyphSetGlyf(_TTGlyphSet): 

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

109 self.glyfTable = font["glyf"] 

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

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

112 

113 def __getitem__(self, glyphName): 

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

115 

116 

117class _TTGlyphSetCFF(_TTGlyphSet): 

118 def __init__(self, font, location): 

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

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

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

122 self.blender = None 

123 if location: 

124 from fontTools.varLib.varStore import VarStoreInstancer 

125 

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

127 if varStore is not None: 

128 instancer = VarStoreInstancer( 

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

130 ) 

131 self.blender = instancer.interpolateFromDeltas 

132 

133 def __getitem__(self, glyphName): 

134 return _TTGlyphCFF(self, glyphName) 

135 

136 

137class _TTGlyphSetVARC(_TTGlyphSet): 

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

139 self.glyphSet = glyphSet 

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

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

142 

143 def __getitem__(self, glyphName): 

144 varc = self.varcTable 

145 if glyphName not in varc.Coverage.glyphs: 

146 return self.glyphSet[glyphName] 

147 return _TTGlyphVARC(self, glyphName) 

148 

149 

150class _TTGlyph(ABC): 

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

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

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

154 the 'hmtx' table. 

155 

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

157 attributes. 

158 """ 

159 

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

161 self.glyphSet = glyphSet 

162 self.name = glyphName 

163 self.recalcBounds = recalcBounds 

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

165 if glyphSet.vMetrics is not None: 

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

167 else: 

168 self.height, self.tsb = None, None 

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

170 varidx = ( 

171 glyphSet.font.getGlyphID(glyphName) 

172 if glyphSet.hvarTable.AdvWidthMap is None 

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

174 ) 

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

176 # TODO: VVAR/VORG 

177 

178 @abstractmethod 

179 def draw(self, pen): 

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

181 how that works. 

182 """ 

183 raise NotImplementedError 

184 

185 def drawPoints(self, pen): 

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

187 how that works. 

188 """ 

189 from fontTools.pens.pointPen import SegmentToPointPen 

190 

191 self.draw(SegmentToPointPen(pen)) 

192 

193 

194class _TTGlyphGlyf(_TTGlyph): 

195 def draw(self, pen): 

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

197 how that works. 

198 """ 

199 glyph, offset = self._getGlyphAndOffset() 

200 

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

202 if depth: 

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

204 

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

206 

207 def drawPoints(self, pen): 

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

209 how that works. 

210 """ 

211 glyph, offset = self._getGlyphAndOffset() 

212 

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

214 if depth: 

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

216 

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

218 

219 def _getGlyphAndOffset(self): 

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

221 glyph = self._getGlyphInstance() 

222 else: 

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

224 

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

226 return glyph, offset 

227 

228 def _getGlyphInstance(self): 

229 from fontTools.varLib.iup import iup_delta 

230 from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates 

231 from fontTools.varLib.models import supportScalar 

232 

233 glyphSet = self.glyphSet 

234 glyfTable = glyphSet.glyfTable 

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

236 hMetrics = glyphSet.hMetrics 

237 vMetrics = glyphSet.vMetrics 

238 coordinates, _ = glyfTable._getCoordinatesAndControls( 

239 self.name, hMetrics, vMetrics 

240 ) 

241 origCoords, endPts = None, None 

242 for var in variations: 

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

244 if not scalar: 

245 continue 

246 delta = var.coordinates 

247 if None in delta: 

248 if origCoords is None: 

249 origCoords, control = glyfTable._getCoordinatesAndControls( 

250 self.name, hMetrics, vMetrics 

251 ) 

252 endPts = ( 

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

254 ) 

255 delta = iup_delta(delta, origCoords, endPts) 

256 coordinates += GlyphCoordinates(delta) * scalar 

257 

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

259 width, lsb, height, tsb = _setCoordinates( 

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

261 ) 

262 self.lsb = lsb 

263 self.tsb = tsb 

264 if glyphSet.hvarTable is None: 

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

266 self.width = width 

267 self.height = height 

268 return glyph 

269 

270 

271class _TTGlyphCFF(_TTGlyph): 

272 def draw(self, pen): 

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

274 how that works. 

275 """ 

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

277 

278 

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

280 if condition.Format == 1: 

281 # ConditionAxisRange 

282 axisIndex = condition.AxisIndex 

283 axisTag = fvarAxes[axisIndex].axisTag 

284 axisValue = location.get(axisTag, 0) 

285 minValue = condition.FilterRangeMinValue 

286 maxValue = condition.FilterRangeMaxValue 

287 return minValue <= axisValue <= maxValue 

288 elif condition.Format == 2: 

289 # ConditionValue 

290 value = condition.DefaultValue 

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

292 return value > 0 

293 elif condition.Format == 3: 

294 # ConditionAnd 

295 for subcondition in condition.ConditionTable: 

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

297 return False 

298 return True 

299 elif condition.Format == 4: 

300 # ConditionOr 

301 for subcondition in condition.ConditionTable: 

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

303 return True 

304 return False 

305 elif condition.Format == 5: 

306 # ConditionNegate 

307 return not _evaluateCondition( 

308 condition.conditionTable, fvarAxes, location, instancer 

309 ) 

310 else: 

311 return False # Unkonwn condition format 

312 

313 

314class _TTGlyphVARC(_TTGlyph): 

315 def _draw(self, pen, isPointPen): 

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

317 how that works. 

318 """ 

319 from fontTools.ttLib.tables.otTables import ( 

320 VarComponentFlags, 

321 NO_VARIATION_INDEX, 

322 ) 

323 

324 glyphSet = self.glyphSet 

325 varc = glyphSet.varcTable 

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

327 glyph = varc.VarCompositeGlyphs.VarCompositeGlyph[idx] 

328 

329 from fontTools.varLib.multiVarStore import MultiVarStoreInstancer 

330 from fontTools.varLib.varStore import VarStoreInstancer 

331 

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

333 instancer = MultiVarStoreInstancer( 

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

335 ) 

336 

337 for comp in glyph.components: 

338 

339 if comp.flags & VarComponentFlags.HAVE_CONDITION: 

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

341 if not _evaluateCondition( 

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

343 ): 

344 continue 

345 

346 location = {} 

347 if comp.axisIndicesIndex is not None: 

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

349 axisValues = Vector(comp.axisValues) 

350 if comp.axisValuesVarIndex != NO_VARIATION_INDEX: 

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

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

353 len(axisIndices), 

354 len(axisValues), 

355 ) 

356 location = { 

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

358 } 

359 

360 if comp.transformVarIndex != NO_VARIATION_INDEX: 

361 deltas = instancer[comp.transformVarIndex] 

362 comp = deepcopy(comp) 

363 comp.applyTransformDeltas(deltas) 

364 transform = comp.transform 

365 

366 reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES 

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

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

369 shouldDecompose = self.name == comp.glyphName 

370 

371 if not shouldDecompose: 

372 try: 

373 pen.addVarComponent( 

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

375 ) 

376 except AttributeError: 

377 shouldDecompose = True 

378 

379 if shouldDecompose: 

380 t = transform.toTransform() 

381 compGlyphSet = ( 

382 self.glyphSet 

383 if comp.glyphName != self.name 

384 else glyphSet.glyphSet 

385 ) 

386 g = compGlyphSet[comp.glyphName] 

387 if isPointPen: 

388 tPen = TransformPointPen(pen, t) 

389 g.drawPoints(tPen) 

390 else: 

391 tPen = TransformPen(pen, t) 

392 g.draw(tPen) 

393 

394 def draw(self, pen): 

395 self._draw(pen, False) 

396 

397 def drawPoints(self, pen): 

398 self._draw(pen, True) 

399 

400 

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

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

403 assert len(coord) >= 4 

404 leftSideX = coord[-4][0] 

405 rightSideX = coord[-3][0] 

406 topSideY = coord[-2][1] 

407 bottomSideY = coord[-1][1] 

408 

409 for _ in range(4): 

410 del coord[-1] 

411 

412 if glyph.isComposite(): 

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

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

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

416 if hasattr(comp, "x"): 

417 comp.x, comp.y = p 

418 elif glyph.numberOfContours == 0: 

419 assert len(coord) == 0 

420 else: 

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

422 glyph.coordinates = coord 

423 

424 if recalcBounds: 

425 glyph.recalcBounds(glyfTable) 

426 

427 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 

428 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 

429 leftSideBearing = otRound(glyph.xMin - leftSideX) 

430 topSideBearing = otRound(topSideY - glyph.yMax) 

431 return ( 

432 horizontalAdvanceWidth, 

433 leftSideBearing, 

434 verticalAdvanceWidth, 

435 topSideBearing, 

436 ) 

437 

438 

439class LerpGlyphSet(Mapping): 

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

441 

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

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

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

445 extrapolate. Defaults to 0.5. 

446 """ 

447 

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

449 self.glyphset1 = glyphset1 

450 self.glyphset2 = glyphset2 

451 self.factor = factor 

452 

453 def __getitem__(self, glyphname): 

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

455 return LerpGlyph(glyphname, self) 

456 raise KeyError(glyphname) 

457 

458 def __contains__(self, glyphname): 

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

460 

461 def __iter__(self): 

462 set1 = set(self.glyphset1) 

463 set2 = set(self.glyphset2) 

464 return iter(set1.intersection(set2)) 

465 

466 def __len__(self): 

467 set1 = set(self.glyphset1) 

468 set2 = set(self.glyphset2) 

469 return len(set1.intersection(set2)) 

470 

471 

472class LerpGlyph: 

473 def __init__(self, glyphname, glyphset): 

474 self.glyphset = glyphset 

475 self.glyphname = glyphname 

476 

477 def draw(self, pen): 

478 recording1 = DecomposingRecordingPen(self.glyphset.glyphset1) 

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

480 recording2 = DecomposingRecordingPen(self.glyphset.glyphset2) 

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

482 

483 factor = self.glyphset.factor 

484 

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