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
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""GlyphSets returned by a TTFont."""
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
14class _TTGlyphSet(Mapping):
16 """Generic dict-like GlyphSet class that pulls metrics from hmtx and
17 glyph shape from TrueType or CFF.
18 """
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
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
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)
61 try:
62 yield None
63 finally:
64 self.location = self.locationStack.pop()
65 self.rawLocation = self.rawLocationStack.pop()
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
76 def __contains__(self, glyphName):
77 return glyphName in self.glyphsMapping
79 def __iter__(self):
80 return iter(self.glyphsMapping.keys())
82 def __len__(self):
83 return len(self.glyphsMapping)
85 @deprecateFunction(
86 "use 'glyphName in glyphSet' instead", category=DeprecationWarning
87 )
88 def has_key(self, glyphName):
89 return glyphName in self.glyphsMapping
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")
98 def __getitem__(self, glyphName):
99 return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds)
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
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
118 def __getitem__(self, glyphName):
119 return _TTGlyphCFF(self, glyphName)
122class _TTGlyph(ABC):
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.
129 If the font contains a 'vmtx' table, there will also be 'height' and 'tsb'
130 attributes.
131 """
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
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
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
164 self.draw(SegmentToPointPen(pen))
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()
174 with self.glyphSet.pushDepth() as depth:
175 if depth:
176 offset = 0 # Offset should only apply at top-level
178 if glyph.isVarComposite():
179 self._drawVarComposite(glyph, pen, False)
180 return
182 glyph.draw(pen, self.glyphSet.glyfTable, offset)
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()
190 with self.glyphSet.pushDepth() as depth:
191 if depth:
192 offset = 0 # Offset should only apply at top-level
194 if glyph.isVarComposite():
195 self._drawVarComposite(glyph, pen, True)
196 return
198 glyph.drawPoints(pen, self.glyphSet.glyfTable, offset)
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 )
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)
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]
229 offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
230 return glyph, offset
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
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
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
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)
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]
291 for _ in range(4):
292 del coord[-1]
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
311 if recalcBounds:
312 glyph.recalcBounds(glyfTable)
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 )