ttGlyphSet.py 17 KB

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