_g_v_a_r.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. from collections import UserDict, deque
  2. from functools import partial
  3. from fontTools.misc import sstruct
  4. from fontTools.misc.textTools import safeEval
  5. from . import DefaultTable
  6. import array
  7. import itertools
  8. import logging
  9. import struct
  10. import sys
  11. import fontTools.ttLib.tables.TupleVariation as tv
  12. log = logging.getLogger(__name__)
  13. TupleVariation = tv.TupleVariation
  14. # https://www.microsoft.com/typography/otspec/gvar.htm
  15. # https://www.microsoft.com/typography/otspec/otvarcommonformats.htm
  16. #
  17. # Apple's documentation of 'gvar':
  18. # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
  19. #
  20. # FreeType2 source code for parsing 'gvar':
  21. # http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c
  22. GVAR_HEADER_FORMAT = """
  23. > # big endian
  24. version: H
  25. reserved: H
  26. axisCount: H
  27. sharedTupleCount: H
  28. offsetToSharedTuples: I
  29. glyphCount: H
  30. flags: H
  31. offsetToGlyphVariationData: I
  32. """
  33. GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT)
  34. class _LazyDict(UserDict):
  35. def __init__(self, data):
  36. super().__init__()
  37. self.data = data
  38. def __getitem__(self, k):
  39. v = self.data[k]
  40. if callable(v):
  41. v = v()
  42. self.data[k] = v
  43. return v
  44. class table__g_v_a_r(DefaultTable.DefaultTable):
  45. dependencies = ["fvar", "glyf"]
  46. def __init__(self, tag=None):
  47. DefaultTable.DefaultTable.__init__(self, tag)
  48. self.version, self.reserved = 1, 0
  49. self.variations = {}
  50. def compile(self, ttFont):
  51. axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
  52. sharedTuples = tv.compileSharedTuples(
  53. axisTags, itertools.chain(*self.variations.values())
  54. )
  55. sharedTupleIndices = {coord: i for i, coord in enumerate(sharedTuples)}
  56. sharedTupleSize = sum([len(c) for c in sharedTuples])
  57. compiledGlyphs = self.compileGlyphs_(ttFont, axisTags, sharedTupleIndices)
  58. offset = 0
  59. offsets = []
  60. for glyph in compiledGlyphs:
  61. offsets.append(offset)
  62. offset += len(glyph)
  63. offsets.append(offset)
  64. compiledOffsets, tableFormat = self.compileOffsets_(offsets)
  65. header = {}
  66. header["version"] = self.version
  67. header["reserved"] = self.reserved
  68. header["axisCount"] = len(axisTags)
  69. header["sharedTupleCount"] = len(sharedTuples)
  70. header["offsetToSharedTuples"] = GVAR_HEADER_SIZE + len(compiledOffsets)
  71. header["glyphCount"] = len(compiledGlyphs)
  72. header["flags"] = tableFormat
  73. header["offsetToGlyphVariationData"] = (
  74. header["offsetToSharedTuples"] + sharedTupleSize
  75. )
  76. compiledHeader = sstruct.pack(GVAR_HEADER_FORMAT, header)
  77. result = [compiledHeader, compiledOffsets]
  78. result.extend(sharedTuples)
  79. result.extend(compiledGlyphs)
  80. return b"".join(result)
  81. def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices):
  82. result = []
  83. glyf = ttFont["glyf"]
  84. for glyphName in ttFont.getGlyphOrder():
  85. variations = self.variations.get(glyphName, [])
  86. if not variations:
  87. result.append(b"")
  88. continue
  89. pointCountUnused = 0 # pointCount is actually unused by compileGlyph
  90. result.append(
  91. compileGlyph_(
  92. variations, pointCountUnused, axisTags, sharedCoordIndices
  93. )
  94. )
  95. return result
  96. def decompile(self, data, ttFont):
  97. axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
  98. glyphs = ttFont.getGlyphOrder()
  99. sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self)
  100. assert len(glyphs) == self.glyphCount
  101. assert len(axisTags) == self.axisCount
  102. offsets = self.decompileOffsets_(
  103. data[GVAR_HEADER_SIZE:],
  104. tableFormat=(self.flags & 1),
  105. glyphCount=self.glyphCount,
  106. )
  107. sharedCoords = tv.decompileSharedTuples(
  108. axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples
  109. )
  110. variations = {}
  111. offsetToData = self.offsetToGlyphVariationData
  112. glyf = ttFont["glyf"]
  113. def decompileVarGlyph(glyphName, gid):
  114. gvarData = data[
  115. offsetToData + offsets[gid] : offsetToData + offsets[gid + 1]
  116. ]
  117. if not gvarData:
  118. return []
  119. glyph = glyf[glyphName]
  120. numPointsInGlyph = self.getNumPoints_(glyph)
  121. return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData)
  122. for gid in range(self.glyphCount):
  123. glyphName = glyphs[gid]
  124. variations[glyphName] = partial(decompileVarGlyph, glyphName, gid)
  125. self.variations = _LazyDict(variations)
  126. if ttFont.lazy is False: # Be lazy for None and True
  127. self.ensureDecompiled()
  128. def ensureDecompiled(self, recurse=False):
  129. # The recurse argument is unused, but part of the signature of
  130. # ensureDecompiled across the library.
  131. # Use a zero-length deque to consume the lazy dict
  132. deque(self.variations.values(), maxlen=0)
  133. @staticmethod
  134. def decompileOffsets_(data, tableFormat, glyphCount):
  135. if tableFormat == 0:
  136. # Short format: array of UInt16
  137. offsets = array.array("H")
  138. offsetsSize = (glyphCount + 1) * 2
  139. else:
  140. # Long format: array of UInt32
  141. offsets = array.array("I")
  142. offsetsSize = (glyphCount + 1) * 4
  143. offsets.frombytes(data[0:offsetsSize])
  144. if sys.byteorder != "big":
  145. offsets.byteswap()
  146. # In the short format, offsets need to be multiplied by 2.
  147. # This is not documented in Apple's TrueType specification,
  148. # but can be inferred from the FreeType implementation, and
  149. # we could verify it with two sample GX fonts.
  150. if tableFormat == 0:
  151. offsets = [off * 2 for off in offsets]
  152. return offsets
  153. @staticmethod
  154. def compileOffsets_(offsets):
  155. """Packs a list of offsets into a 'gvar' offset table.
  156. Returns a pair (bytestring, tableFormat). Bytestring is the
  157. packed offset table. Format indicates whether the table
  158. uses short (tableFormat=0) or long (tableFormat=1) integers.
  159. The returned tableFormat should get packed into the flags field
  160. of the 'gvar' header.
  161. """
  162. assert len(offsets) >= 2
  163. for i in range(1, len(offsets)):
  164. assert offsets[i - 1] <= offsets[i]
  165. if max(offsets) <= 0xFFFF * 2:
  166. packed = array.array("H", [n >> 1 for n in offsets])
  167. tableFormat = 0
  168. else:
  169. packed = array.array("I", offsets)
  170. tableFormat = 1
  171. if sys.byteorder != "big":
  172. packed.byteswap()
  173. return (packed.tobytes(), tableFormat)
  174. def toXML(self, writer, ttFont):
  175. writer.simpletag("version", value=self.version)
  176. writer.newline()
  177. writer.simpletag("reserved", value=self.reserved)
  178. writer.newline()
  179. axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
  180. for glyphName in ttFont.getGlyphNames():
  181. variations = self.variations.get(glyphName)
  182. if not variations:
  183. continue
  184. writer.begintag("glyphVariations", glyph=glyphName)
  185. writer.newline()
  186. for gvar in variations:
  187. gvar.toXML(writer, axisTags)
  188. writer.endtag("glyphVariations")
  189. writer.newline()
  190. def fromXML(self, name, attrs, content, ttFont):
  191. if name == "version":
  192. self.version = safeEval(attrs["value"])
  193. elif name == "reserved":
  194. self.reserved = safeEval(attrs["value"])
  195. elif name == "glyphVariations":
  196. if not hasattr(self, "variations"):
  197. self.variations = {}
  198. glyphName = attrs["glyph"]
  199. glyph = ttFont["glyf"][glyphName]
  200. numPointsInGlyph = self.getNumPoints_(glyph)
  201. glyphVariations = []
  202. for element in content:
  203. if isinstance(element, tuple):
  204. name, attrs, content = element
  205. if name == "tuple":
  206. gvar = TupleVariation({}, [None] * numPointsInGlyph)
  207. glyphVariations.append(gvar)
  208. for tupleElement in content:
  209. if isinstance(tupleElement, tuple):
  210. tupleName, tupleAttrs, tupleContent = tupleElement
  211. gvar.fromXML(tupleName, tupleAttrs, tupleContent)
  212. self.variations[glyphName] = glyphVariations
  213. @staticmethod
  214. def getNumPoints_(glyph):
  215. NUM_PHANTOM_POINTS = 4
  216. if glyph.isComposite():
  217. return len(glyph.components) + NUM_PHANTOM_POINTS
  218. elif glyph.isVarComposite():
  219. count = 0
  220. for component in glyph.components:
  221. count += component.getPointCount()
  222. return count + NUM_PHANTOM_POINTS
  223. else:
  224. # Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute.
  225. return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS
  226. def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices):
  227. tupleVariationCount, tuples, data = tv.compileTupleVariationStore(
  228. variations, pointCount, axisTags, sharedCoordIndices
  229. )
  230. if tupleVariationCount == 0:
  231. return b""
  232. result = [struct.pack(">HH", tupleVariationCount, 4 + len(tuples)), tuples, data]
  233. if (len(tuples) + len(data)) % 2 != 0:
  234. result.append(b"\0") # padding
  235. return b"".join(result)
  236. def decompileGlyph_(pointCount, sharedTuples, axisTags, data):
  237. if len(data) < 4:
  238. return []
  239. tupleVariationCount, offsetToData = struct.unpack(">HH", data[:4])
  240. dataPos = offsetToData
  241. return tv.decompileTupleVariationStore(
  242. "gvar",
  243. axisTags,
  244. tupleVariationCount,
  245. pointCount,
  246. sharedTuples,
  247. data,
  248. 4,
  249. offsetToData,
  250. )