ttGlyphSet.py 11 KB

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