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 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 12from fontTools.pens.recordingPen import ( 13 DecomposingRecordingPen, 14 lerpRecordings, 15 replayRecording, 16) 17 18 19class _TTGlyphSet(Mapping): 20 """Generic dict-like GlyphSet class that pulls metrics from hmtx and 21 glyph shape from TrueType or CFF. 22 """ 23 24 def __init__(self, font, location, glyphsMapping, *, recalcBounds=True): 25 self.recalcBounds = recalcBounds 26 self.font = font 27 self.defaultLocationNormalized = ( 28 {axis.axisTag: 0 for axis in self.font["fvar"].axes} 29 if "fvar" in self.font 30 else {} 31 ) 32 self.location = location if location is not None else {} 33 self.rawLocation = {} # VarComponent-only location 34 self.originalLocation = location if location is not None else {} 35 self.depth = 0 36 self.locationStack = [] 37 self.rawLocationStack = [] 38 self.glyphsMapping = glyphsMapping 39 self.hMetrics = font["hmtx"].metrics 40 self.vMetrics = getattr(font.get("vmtx"), "metrics", None) 41 self.hvarTable = None 42 if location: 43 from fontTools.varLib.varStore import VarStoreInstancer 44 45 self.hvarTable = getattr(font.get("HVAR"), "table", None) 46 if self.hvarTable is not None: 47 self.hvarInstancer = VarStoreInstancer( 48 self.hvarTable.VarStore, font["fvar"].axes, location 49 ) 50 # TODO VVAR, VORG 51 52 @contextmanager 53 def pushLocation(self, location, reset: bool): 54 self.locationStack.append(self.location) 55 self.rawLocationStack.append(self.rawLocation) 56 if reset: 57 self.location = self.originalLocation.copy() 58 self.rawLocation = self.defaultLocationNormalized.copy() 59 else: 60 self.location = self.location.copy() 61 self.rawLocation = {} 62 self.location.update(location) 63 self.rawLocation.update(location) 64 65 try: 66 yield None 67 finally: 68 self.location = self.locationStack.pop() 69 self.rawLocation = self.rawLocationStack.pop() 70 71 @contextmanager 72 def pushDepth(self): 73 try: 74 depth = self.depth 75 self.depth += 1 76 yield depth 77 finally: 78 self.depth -= 1 79 80 def __contains__(self, glyphName): 81 return glyphName in self.glyphsMapping 82 83 def __iter__(self): 84 return iter(self.glyphsMapping.keys()) 85 86 def __len__(self): 87 return len(self.glyphsMapping) 88 89 @deprecateFunction( 90 "use 'glyphName in glyphSet' instead", category=DeprecationWarning 91 ) 92 def has_key(self, glyphName): 93 return glyphName in self.glyphsMapping 94 95 96class _TTGlyphSetGlyf(_TTGlyphSet): 97 def __init__(self, font, location, recalcBounds=True): 98 self.glyfTable = font["glyf"] 99 super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds) 100 self.gvarTable = font.get("gvar") 101 102 def __getitem__(self, glyphName): 103 return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) 104 105 106class _TTGlyphSetCFF(_TTGlyphSet): 107 def __init__(self, font, location): 108 tableTag = "CFF2" if "CFF2" in font else "CFF " 109 self.charStrings = list(font[tableTag].cff.values())[0].CharStrings 110 super().__init__(font, location, self.charStrings) 111 self.blender = None 112 if location: 113 from fontTools.varLib.varStore import VarStoreInstancer 114 115 varStore = getattr(self.charStrings, "varStore", None) 116 if varStore is not None: 117 instancer = VarStoreInstancer( 118 varStore.otVarStore, font["fvar"].axes, location 119 ) 120 self.blender = instancer.interpolateFromDeltas 121 122 def __getitem__(self, glyphName): 123 return _TTGlyphCFF(self, glyphName) 124 125 126class _TTGlyph(ABC): 127 """Glyph object that supports the Pen protocol, meaning that it has 128 .draw() and .drawPoints() methods that take a pen object as their only 129 argument. Additionally there are 'width' and 'lsb' attributes, read from 130 the 'hmtx' table. 131 132 If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' 133 attributes. 134 """ 135 136 def __init__(self, glyphSet, glyphName, *, recalcBounds=True): 137 self.glyphSet = glyphSet 138 self.name = glyphName 139 self.recalcBounds = recalcBounds 140 self.width, self.lsb = glyphSet.hMetrics[glyphName] 141 if glyphSet.vMetrics is not None: 142 self.height, self.tsb = glyphSet.vMetrics[glyphName] 143 else: 144 self.height, self.tsb = None, None 145 if glyphSet.location and glyphSet.hvarTable is not None: 146 varidx = ( 147 glyphSet.font.getGlyphID(glyphName) 148 if glyphSet.hvarTable.AdvWidthMap is None 149 else glyphSet.hvarTable.AdvWidthMap.mapping[glyphName] 150 ) 151 self.width += glyphSet.hvarInstancer[varidx] 152 # TODO: VVAR/VORG 153 154 @abstractmethod 155 def draw(self, pen): 156 """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details 157 how that works. 158 """ 159 raise NotImplementedError 160 161 def drawPoints(self, pen): 162 """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details 163 how that works. 164 """ 165 from fontTools.pens.pointPen import SegmentToPointPen 166 167 self.draw(SegmentToPointPen(pen)) 168 169 170class _TTGlyphGlyf(_TTGlyph): 171 def draw(self, pen): 172 """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details 173 how that works. 174 """ 175 glyph, offset = self._getGlyphAndOffset() 176 177 with self.glyphSet.pushDepth() as depth: 178 if depth: 179 offset = 0 # Offset should only apply at top-level 180 181 if glyph.isVarComposite(): 182 self._drawVarComposite(glyph, pen, False) 183 return 184 185 glyph.draw(pen, self.glyphSet.glyfTable, offset) 186 187 def drawPoints(self, pen): 188 """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details 189 how that works. 190 """ 191 glyph, offset = self._getGlyphAndOffset() 192 193 with self.glyphSet.pushDepth() as depth: 194 if depth: 195 offset = 0 # Offset should only apply at top-level 196 197 if glyph.isVarComposite(): 198 self._drawVarComposite(glyph, pen, True) 199 return 200 201 glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) 202 203 def _drawVarComposite(self, glyph, pen, isPointPen): 204 from fontTools.ttLib.tables._g_l_y_f import ( 205 VarComponentFlags, 206 VAR_COMPONENT_TRANSFORM_MAPPING, 207 ) 208 209 for comp in glyph.components: 210 with self.glyphSet.pushLocation( 211 comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES 212 ): 213 try: 214 pen.addVarComponent( 215 comp.glyphName, comp.transform, self.glyphSet.rawLocation 216 ) 217 except AttributeError: 218 t = comp.transform.toTransform() 219 if isPointPen: 220 tPen = TransformPointPen(pen, t) 221 self.glyphSet[comp.glyphName].drawPoints(tPen) 222 else: 223 tPen = TransformPen(pen, t) 224 self.glyphSet[comp.glyphName].draw(tPen) 225 226 def _getGlyphAndOffset(self): 227 if self.glyphSet.location and self.glyphSet.gvarTable is not None: 228 glyph = self._getGlyphInstance() 229 else: 230 glyph = self.glyphSet.glyfTable[self.name] 231 232 offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 233 return glyph, offset 234 235 def _getGlyphInstance(self): 236 from fontTools.varLib.iup import iup_delta 237 from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates 238 from fontTools.varLib.models import supportScalar 239 240 glyphSet = self.glyphSet 241 glyfTable = glyphSet.glyfTable 242 variations = glyphSet.gvarTable.variations[self.name] 243 hMetrics = glyphSet.hMetrics 244 vMetrics = glyphSet.vMetrics 245 coordinates, _ = glyfTable._getCoordinatesAndControls( 246 self.name, hMetrics, vMetrics 247 ) 248 origCoords, endPts = None, None 249 for var in variations: 250 scalar = supportScalar(glyphSet.location, var.axes) 251 if not scalar: 252 continue 253 delta = var.coordinates 254 if None in delta: 255 if origCoords is None: 256 origCoords, control = glyfTable._getCoordinatesAndControls( 257 self.name, hMetrics, vMetrics 258 ) 259 endPts = ( 260 control[1] if control[0] >= 1 else list(range(len(control[1]))) 261 ) 262 delta = iup_delta(delta, origCoords, endPts) 263 coordinates += GlyphCoordinates(delta) * scalar 264 265 glyph = copy(glyfTable[self.name]) # Shallow copy 266 width, lsb, height, tsb = _setCoordinates( 267 glyph, coordinates, glyfTable, recalcBounds=self.recalcBounds 268 ) 269 self.lsb = lsb 270 self.tsb = tsb 271 if glyphSet.hvarTable is None: 272 # no HVAR: let's set metrics from the phantom points 273 self.width = width 274 self.height = height 275 return glyph 276 277 278class _TTGlyphCFF(_TTGlyph): 279 def draw(self, pen): 280 """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details 281 how that works. 282 """ 283 self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender) 284 285 286def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True): 287 # Handle phantom points for (left, right, top, bottom) positions. 288 assert len(coord) >= 4 289 leftSideX = coord[-4][0] 290 rightSideX = coord[-3][0] 291 topSideY = coord[-2][1] 292 bottomSideY = coord[-1][1] 293 294 for _ in range(4): 295 del coord[-1] 296 297 if glyph.isComposite(): 298 assert len(coord) == len(glyph.components) 299 glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy 300 for p, comp in zip(coord, glyph.components): 301 if hasattr(comp, "x"): 302 comp.x, comp.y = p 303 elif glyph.isVarComposite(): 304 glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy 305 for comp in glyph.components: 306 coord = comp.setCoordinates(coord) 307 assert not coord 308 elif glyph.numberOfContours == 0: 309 assert len(coord) == 0 310 else: 311 assert len(coord) == len(glyph.coordinates) 312 glyph.coordinates = coord 313 314 if recalcBounds: 315 glyph.recalcBounds(glyfTable) 316 317 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 318 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 319 leftSideBearing = otRound(glyph.xMin - leftSideX) 320 topSideBearing = otRound(topSideY - glyph.yMax) 321 return ( 322 horizontalAdvanceWidth, 323 leftSideBearing, 324 verticalAdvanceWidth, 325 topSideBearing, 326 ) 327 328 329class LerpGlyphSet(Mapping): 330 """A glyphset that interpolates between two other glyphsets. 331 332 Factor is typically between 0 and 1. 0 means the first glyphset, 333 1 means the second glyphset, and 0.5 means the average of the 334 two glyphsets. Other values are possible, and can be useful to 335 extrapolate. Defaults to 0.5. 336 """ 337 338 def __init__(self, glyphset1, glyphset2, factor=0.5): 339 self.glyphset1 = glyphset1 340 self.glyphset2 = glyphset2 341 self.factor = factor 342 343 def __getitem__(self, glyphname): 344 if glyphname in self.glyphset1 and glyphname in self.glyphset2: 345 return LerpGlyph(glyphname, self) 346 raise KeyError(glyphname) 347 348 def __contains__(self, glyphname): 349 return glyphname in self.glyphset1 and glyphname in self.glyphset2 350 351 def __iter__(self): 352 set1 = set(self.glyphset1) 353 set2 = set(self.glyphset2) 354 return iter(set1.intersection(set2)) 355 356 def __len__(self): 357 set1 = set(self.glyphset1) 358 set2 = set(self.glyphset2) 359 return len(set1.intersection(set2)) 360 361 362class LerpGlyph: 363 def __init__(self, glyphname, glyphset): 364 self.glyphset = glyphset 365 self.glyphname = glyphname 366 367 def draw(self, pen): 368 recording1 = DecomposingRecordingPen(self.glyphset.glyphset1) 369 self.glyphset.glyphset1[self.glyphname].draw(recording1) 370 recording2 = DecomposingRecordingPen(self.glyphset.glyphset2) 371 self.glyphset.glyphset2[self.glyphname].draw(recording2) 372 373 factor = self.glyphset.factor 374 375 replayRecording(lerpRecordings(recording1.value, recording2.value, factor), pen) 376