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)