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