ttGlyphSet.py 17 KB

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