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