ttGlyphSet.py 13 KB

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