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)