• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.misc.loggingTools import CapturingLogHandler
2from fontTools.misc.testTools import parseXML
3from fontTools.misc.textTools import deHexStr, hexStr
4from fontTools.misc.xmlWriter import XMLWriter
5from fontTools.ttLib.tables.TupleVariation import \
6	log, TupleVariation, compileSharedTuples, decompileSharedTuples, \
7	compileTupleVariationStore, decompileTupleVariationStore, inferRegion_
8from io import BytesIO
9import random
10import unittest
11
12
13def hexencode(s):
14	h = hexStr(s).upper()
15	return ' '.join([h[i:i+2] for i in range(0, len(h), 2)])
16
17
18AXES = {
19	"wdth": (0.25, 0.375, 0.5),
20	"wght": (0.0, 1.0, 1.0),
21	"opsz": (-0.75, -0.75, 0.0)
22}
23
24
25# Shared tuples in the 'gvar' table of the Skia font, as printed
26# in Apple's TrueType specification.
27# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
28SKIA_GVAR_SHARED_TUPLES_DATA = deHexStr(
29	"40 00 00 00 C0 00 00 00 00 00 40 00 00 00 C0 00 "
30	"C0 00 C0 00 40 00 C0 00 40 00 40 00 C0 00 40 00")
31
32SKIA_GVAR_SHARED_TUPLES = [
33	{"wght": 1.0, "wdth": 0.0},
34	{"wght": -1.0, "wdth": 0.0},
35	{"wght": 0.0, "wdth": 1.0},
36	{"wght": 0.0, "wdth": -1.0},
37	{"wght": -1.0, "wdth": -1.0},
38	{"wght": 1.0, "wdth": -1.0},
39	{"wght": 1.0, "wdth": 1.0},
40	{"wght": -1.0, "wdth": 1.0}
41]
42
43
44# Tuple Variation Store of uppercase I in the Skia font, as printed in Apple's
45# TrueType spec. The actual Skia font uses a different table for uppercase I
46# than what is printed in Apple's spec, but we still want to make sure that
47# we can parse the data as it appears in the specification.
48# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
49SKIA_GVAR_I_DATA = deHexStr(
50	"00 08 00 24 00 33 20 00 00 15 20 01 00 1B 20 02 "
51	"00 24 20 03 00 15 20 04 00 26 20 07 00 0D 20 06 "
52	"00 1A 20 05 00 40 01 01 01 81 80 43 FF 7E FF 7E "
53	"FF 7E FF 7E 00 81 45 01 01 01 03 01 04 01 04 01 "
54	"04 01 02 80 40 00 82 81 81 04 3A 5A 3E 43 20 81 "
55	"04 0E 40 15 45 7C 83 00 0D 9E F3 F2 F0 F0 F0 F0 "
56	"F3 9E A0 A1 A1 A1 9F 80 00 91 81 91 00 0D 0A 0A "
57	"09 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0B 80 00 15 81 "
58	"81 00 C4 89 00 C4 83 00 0D 80 99 98 96 96 96 96 "
59	"99 80 82 83 83 83 81 80 40 FF 18 81 81 04 E6 F9 "
60	"10 21 02 81 04 E8 E5 EB 4D DA 83 00 0D CE D3 D4 "
61	"D3 D3 D3 D5 D2 CE CC CD CD CD CD 80 00 A1 81 91 "
62	"00 0D 07 03 04 02 02 02 03 03 07 07 08 08 08 07 "
63	"80 00 09 81 81 00 28 40 00 A4 02 24 24 66 81 04 "
64	"08 FA FA FA 28 83 00 82 02 FF FF FF 83 02 01 01 "
65	"01 84 91 00 80 06 07 08 08 08 08 0A 07 80 03 FE "
66	"FF FF FF 81 00 08 81 82 02 EE EE EE 8B 6D 00")
67
68
69class TupleVariationTest(unittest.TestCase):
70	def __init__(self, methodName):
71		unittest.TestCase.__init__(self, methodName)
72		# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
73		# and fires deprecation warnings if a program uses the old name.
74		if not hasattr(self, "assertRaisesRegex"):
75			self.assertRaisesRegex = self.assertRaisesRegexp
76
77	def test_equal(self):
78		var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
79		var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
80		self.assertEqual(var1, var2)
81
82	def test_equal_differentAxes(self):
83		var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
84		var2 = TupleVariation({"wght":(0.7, 0.8, 0.9)}, [(0,0), (9,8), (7,6)])
85		self.assertNotEqual(var1, var2)
86
87	def test_equal_differentCoordinates(self):
88		var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
89		var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8)])
90		self.assertNotEqual(var1, var2)
91
92	def test_hasImpact_someDeltasNotZero(self):
93		axes = {"wght":(0.0, 1.0, 1.0)}
94		var = TupleVariation(axes, [(0,0), (9,8), (7,6)])
95		self.assertTrue(var.hasImpact())
96
97	def test_hasImpact_allDeltasZero(self):
98		axes = {"wght":(0.0, 1.0, 1.0)}
99		var = TupleVariation(axes, [(0,0), (0,0), (0,0)])
100		self.assertTrue(var.hasImpact())
101
102	def test_hasImpact_allDeltasNone(self):
103		axes = {"wght":(0.0, 1.0, 1.0)}
104		var = TupleVariation(axes, [None, None, None])
105		self.assertFalse(var.hasImpact())
106
107	def test_toXML_badDeltaFormat(self):
108		writer = XMLWriter(BytesIO())
109		g = TupleVariation(AXES, ["String"])
110		with CapturingLogHandler(log, "ERROR") as captor:
111			g.toXML(writer, ["wdth"])
112		self.assertIn("bad delta format", [r.msg for r in captor.records])
113		self.assertEqual([
114			'<tuple>',
115			  '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>',
116			  '<!-- bad delta #0 -->',
117			'</tuple>',
118		], TupleVariationTest.xml_lines(writer))
119
120	def test_toXML_constants(self):
121		writer = XMLWriter(BytesIO())
122		g = TupleVariation(AXES, [42, None, 23, 0, -17, None])
123		g.toXML(writer, ["wdth", "wght", "opsz"])
124		self.assertEqual([
125			'<tuple>',
126			  '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>',
127			  '<coord axis="wght" value="1.0"/>',
128			  '<coord axis="opsz" value="-0.75"/>',
129			  '<delta cvt="0" value="42"/>',
130			  '<delta cvt="2" value="23"/>',
131			  '<delta cvt="3" value="0"/>',
132			  '<delta cvt="4" value="-17"/>',
133			'</tuple>'
134		], TupleVariationTest.xml_lines(writer))
135
136	def test_toXML_points(self):
137		writer = XMLWriter(BytesIO())
138		g = TupleVariation(AXES, [(9,8), None, (7,6), (0,0), (-1,-2), None])
139		g.toXML(writer, ["wdth", "wght", "opsz"])
140		self.assertEqual([
141			'<tuple>',
142			  '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>',
143			  '<coord axis="wght" value="1.0"/>',
144			  '<coord axis="opsz" value="-0.75"/>',
145			  '<delta pt="0" x="9" y="8"/>',
146			  '<delta pt="2" x="7" y="6"/>',
147			  '<delta pt="3" x="0" y="0"/>',
148			  '<delta pt="4" x="-1" y="-2"/>',
149			'</tuple>'
150		], TupleVariationTest.xml_lines(writer))
151
152	def test_toXML_allDeltasNone(self):
153		writer = XMLWriter(BytesIO())
154		axes = {"wght":(0.0, 1.0, 1.0)}
155		g = TupleVariation(axes, [None] * 5)
156		g.toXML(writer, ["wght", "wdth"])
157		self.assertEqual([
158			'<tuple>',
159			  '<coord axis="wght" value="1.0"/>',
160			  '<!-- no deltas -->',
161			'</tuple>'
162		], TupleVariationTest.xml_lines(writer))
163
164	def test_toXML_axes_floats(self):
165		writer = XMLWriter(BytesIO())
166		axes = {
167			"wght": (0.0, 0.2999878, 0.7000122),
168			"wdth": (0.0, 0.4000244, 0.4000244),
169		}
170		g = TupleVariation(axes, [None] * 5)
171		g.toXML(writer, ["wght", "wdth"])
172		self.assertEqual(
173			[
174				'<coord axis="wght" min="0.0" value="0.3" max="0.7"/>',
175				'<coord axis="wdth" value="0.4"/>',
176			],
177			TupleVariationTest.xml_lines(writer)[1:3]
178		)
179
180	def test_fromXML_badDeltaFormat(self):
181		g = TupleVariation({}, [])
182		with CapturingLogHandler(log, "WARNING") as captor:
183			for name, attrs, content in parseXML('<delta a="1" b="2"/>'):
184				g.fromXML(name, attrs, content)
185		self.assertIn("bad delta format: a, b",
186		              [r.msg for r in captor.records])
187
188	def test_fromXML_constants(self):
189		g = TupleVariation({}, [None] * 4)
190		for name, attrs, content in parseXML(
191				'<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>'
192				'<coord axis="wght" value="1.0"/>'
193				'<coord axis="opsz" value="-0.75"/>'
194				'<delta cvt="1" value="42"/>'
195				'<delta cvt="2" value="-23"/>'):
196			g.fromXML(name, attrs, content)
197		self.assertEqual(AXES, g.axes)
198		self.assertEqual([None, 42, -23, None], g.coordinates)
199
200	def test_fromXML_points(self):
201		g = TupleVariation({}, [None] * 4)
202		for name, attrs, content in parseXML(
203				'<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>'
204				'<coord axis="wght" value="1.0"/>'
205				'<coord axis="opsz" value="-0.75"/>'
206				'<delta pt="1" x="33" y="44"/>'
207				'<delta pt="2" x="-2" y="170"/>'):
208			g.fromXML(name, attrs, content)
209		self.assertEqual(AXES, g.axes)
210		self.assertEqual([None, (33, 44), (-2, 170), None], g.coordinates)
211
212	def test_fromXML_axes_floats(self):
213		g = TupleVariation({}, [None] * 4)
214		for name, attrs, content in parseXML(
215			'<coord axis="wght" min="0.0" value="0.3" max="0.7"/>'
216			'<coord axis="wdth" value="0.4"/>'
217		):
218			g.fromXML(name, attrs, content)
219
220		self.assertEqual(g.axes["wght"][0], 0)
221		self.assertAlmostEqual(g.axes["wght"][1], 0.2999878)
222		self.assertAlmostEqual(g.axes["wght"][2], 0.7000122)
223
224		self.assertEqual(g.axes["wdth"][0], 0)
225		self.assertAlmostEqual(g.axes["wdth"][1], 0.4000244)
226		self.assertAlmostEqual(g.axes["wdth"][2], 0.4000244)
227
228	def test_compile_sharedPeaks_nonIntermediate_sharedPoints(self):
229		var = TupleVariation(
230			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
231			[(7,4), (8,5), (9,6)])
232		axisTags = ["wght", "wdth"]
233		sharedPeakIndices = { var.compileCoord(axisTags): 0x77 }
234		tup, deltas = var.compile(axisTags, sharedPeakIndices, pointData=b'')
235		# len(deltas)=8; flags=None; tupleIndex=0x77
236		# embeddedPeaks=[]; intermediateCoord=[]
237		self.assertEqual("00 08 00 77", hexencode(tup))
238		self.assertEqual("02 07 08 09 "     # deltaX: [7, 8, 9]
239						 "02 04 05 06",     # deltaY: [4, 5, 6]
240						 hexencode(deltas))
241
242	def test_compile_sharedPeaks_intermediate_sharedPoints(self):
243		var = TupleVariation(
244			{"wght": (0.3, 0.5, 0.7), "wdth": (0.1, 0.8, 0.9)},
245			[(7,4), (8,5), (9,6)])
246		axisTags = ["wght", "wdth"]
247		sharedPeakIndices = { var.compileCoord(axisTags): 0x77 }
248		tup, deltas = var.compile(axisTags, sharedPeakIndices, pointData=b'')
249		# len(deltas)=8; flags=INTERMEDIATE_REGION; tupleIndex=0x77
250		# embeddedPeak=[]; intermediateCoord=[(0.3, 0.1), (0.7, 0.9)]
251		self.assertEqual("00 08 40 77 13 33 06 66 2C CD 39 9A", hexencode(tup))
252		self.assertEqual("02 07 08 09 "     # deltaX: [7, 8, 9]
253						 "02 04 05 06",     # deltaY: [4, 5, 6]
254						 hexencode(deltas))
255
256	def test_compile_sharedPeaks_nonIntermediate_privatePoints(self):
257		var = TupleVariation(
258			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
259			[(7,4), (8,5), (9,6)])
260		axisTags = ["wght", "wdth"]
261		sharedPeakIndices = { var.compileCoord(axisTags): 0x77 }
262		tup, deltas = var.compile(axisTags, sharedPeakIndices)
263		# len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77
264		# embeddedPeak=[]; intermediateCoord=[]
265		self.assertEqual("00 09 20 77", hexencode(tup))
266		self.assertEqual("00 "              # all points in glyph
267						 "02 07 08 09 "     # deltaX: [7, 8, 9]
268						 "02 04 05 06",     # deltaY: [4, 5, 6]
269						 hexencode(deltas))
270
271	def test_compile_sharedPeaks_intermediate_privatePoints(self):
272		var = TupleVariation(
273			{"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 1.0)},
274			[(7,4), (8,5), (9,6)])
275		axisTags = ["wght", "wdth"]
276		sharedPeakIndices = { var.compileCoord(axisTags): 0x77 }
277		tuple, deltas = var.compile(axisTags, sharedPeakIndices)
278		# len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77
279		# embeddedPeak=[]; intermediateCoord=[(0.0, 0.0), (1.0, 1.0)]
280		self.assertEqual("00 09 60 77 00 00 00 00 40 00 40 00",
281		                 hexencode(tuple))
282		self.assertEqual("00 "              # all points in glyph
283						 "02 07 08 09 "     # deltaX: [7, 8, 9]
284						 "02 04 05 06",     # deltaY: [4, 5, 6]
285						 hexencode(deltas))
286
287	def test_compile_embeddedPeak_nonIntermediate_sharedPoints(self):
288		var = TupleVariation(
289			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
290			[(7,4), (8,5), (9,6)])
291		tup, deltas = var.compile(axisTags=["wght", "wdth"], pointData=b'')
292		# len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE
293		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
294		self.assertEqual("00 08 80 00 20 00 33 33", hexencode(tup))
295		self.assertEqual("02 07 08 09 "     # deltaX: [7, 8, 9]
296						 "02 04 05 06",     # deltaY: [4, 5, 6]
297						 hexencode(deltas))
298
299	def test_compile_embeddedPeak_nonIntermediate_sharedConstants(self):
300		var = TupleVariation(
301			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
302			[3, 1, 4])
303		tup, deltas = var.compile(axisTags=["wght", "wdth"], pointData=b'')
304		# len(deltas)=4; flags=EMBEDDED_PEAK_TUPLE
305		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
306		self.assertEqual("00 04 80 00 20 00 33 33", hexencode(tup))
307		self.assertEqual("02 03 01 04",     # delta: [3, 1, 4]
308						 hexencode(deltas))
309
310	def test_compile_embeddedPeak_intermediate_sharedPoints(self):
311		var = TupleVariation(
312			{"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 0.8)},
313			[(7,4), (8,5), (9,6)])
314		tup, deltas = var.compile(axisTags=["wght", "wdth"], pointData=b'')
315		# len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE
316		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[(0.0, 0.0), (1.0, 0.8)]
317		self.assertEqual("00 08 C0 00 20 00 33 33 00 00 00 00 40 00 33 33",
318		                hexencode(tup))
319		self.assertEqual("02 07 08 09 "  # deltaX: [7, 8, 9]
320						 "02 04 05 06",  # deltaY: [4, 5, 6]
321						 hexencode(deltas))
322
323	def test_compile_embeddedPeak_nonIntermediate_privatePoints(self):
324		var = TupleVariation(
325			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
326			[(7,4), (8,5), (9,6)])
327		tup, deltas = var.compile(axisTags=["wght", "wdth"])
328		# len(deltas)=9; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE
329		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
330		self.assertEqual("00 09 A0 00 20 00 33 33", hexencode(tup))
331		self.assertEqual("00 "           # all points in glyph
332		                 "02 07 08 09 "  # deltaX: [7, 8, 9]
333		                 "02 04 05 06",  # deltaY: [4, 5, 6]
334		                 hexencode(deltas))
335
336	def test_compile_embeddedPeak_nonIntermediate_privateConstants(self):
337		var = TupleVariation(
338			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
339			[7, 8, 9])
340		tup, deltas = var.compile(axisTags=["wght", "wdth"])
341		# len(deltas)=5; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE
342		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
343		self.assertEqual("00 05 A0 00 20 00 33 33", hexencode(tup))
344		self.assertEqual("00 "           # all points in glyph
345		                 "02 07 08 09",  # delta: [7, 8, 9]
346		                 hexencode(deltas))
347
348	def test_compile_embeddedPeak_intermediate_privatePoints(self):
349		var = TupleVariation(
350			{"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)},
351			[(7,4), (8,5), (9,6)])
352		tup, deltas = var.compile(axisTags = ["wght", "wdth"])
353		# len(deltas)=9;
354		# flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE
355		# embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)]
356		self.assertEqual("00 09 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A",
357		                 hexencode(tup))
358		self.assertEqual("00 "              # all points in glyph
359		                 "02 07 08 09 "     # deltaX: [7, 8, 9]
360		                 "02 04 05 06",     # deltaY: [4, 5, 6]
361		                 hexencode(deltas))
362
363	def test_compile_embeddedPeak_intermediate_privateConstants(self):
364		var = TupleVariation(
365			{"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)},
366			[7, 8, 9])
367		tup, deltas = var.compile(axisTags = ["wght", "wdth"])
368		# len(deltas)=5;
369		# flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE
370		# embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)]
371		self.assertEqual("00 05 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A",
372		                 hexencode(tup))
373		self.assertEqual("00 "             # all points in glyph
374		                 "02 07 08 09",    # delta: [7, 8, 9]
375		                 hexencode(deltas))
376
377	def test_compileCoord(self):
378		var = TupleVariation({"wght": (-1.0, -1.0, -1.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4)
379		self.assertEqual("C0 00 20 00", hexencode(var.compileCoord(["wght", "wdth"])))
380		self.assertEqual("20 00 C0 00", hexencode(var.compileCoord(["wdth", "wght"])))
381		self.assertEqual("C0 00", hexencode(var.compileCoord(["wght"])))
382
383	def test_compileIntermediateCoord(self):
384		var = TupleVariation({"wght": (-1.0, -1.0, 0.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4)
385		self.assertEqual("C0 00 19 9A 00 00 26 66", hexencode(var.compileIntermediateCoord(["wght", "wdth"])))
386		self.assertEqual("19 9A C0 00 26 66 00 00", hexencode(var.compileIntermediateCoord(["wdth", "wght"])))
387		self.assertEqual(None, var.compileIntermediateCoord(["wght"]))
388		self.assertEqual("19 9A 26 66", hexencode(var.compileIntermediateCoord(["wdth"])))
389
390	def test_decompileCoord(self):
391		decompileCoord = TupleVariation.decompileCoord_
392		data = deHexStr("DE AD C0 00 20 00 DE AD")
393		self.assertEqual(({"wght": -1.0, "wdth": 0.5}, 6), decompileCoord(["wght", "wdth"], data, 2))
394
395	def test_decompileCoord_roundTrip(self):
396		# Make sure we are not affected by https://github.com/fonttools/fonttools/issues/286
397		data = deHexStr("7F B9 80 35")
398		values, _ = TupleVariation.decompileCoord_(["wght", "wdth"], data, 0)
399		axisValues = {axis:(val, val, val) for axis, val in  values.items()}
400		var = TupleVariation(axisValues, [None] * 4)
401		self.assertEqual("7F B9 80 35", hexencode(var.compileCoord(["wght", "wdth"])))
402
403	def test_compilePoints(self):
404		compilePoints = lambda p: TupleVariation.compilePoints(set(p))
405		self.assertEqual("00", hexencode(compilePoints(set())))  # all points in glyph
406		self.assertEqual("01 00 07", hexencode(compilePoints([7])))
407		self.assertEqual("01 80 FF FF", hexencode(compilePoints([65535])))
408		self.assertEqual("02 01 09 06", hexencode(compilePoints([9, 15])))
409		self.assertEqual("06 05 07 01 F7 02 01 F2", hexencode(compilePoints([7, 8, 255, 257, 258, 500])))
410		self.assertEqual("03 01 07 01 80 01 EC", hexencode(compilePoints([7, 8, 500])))
411		self.assertEqual("04 01 07 01 81 BE E7 0C 0F", hexencode(compilePoints([7, 8, 0xBEEF, 0xCAFE])))
412		self.maxDiff = None
413		self.assertEqual("81 2C" +  # 300 points (0x12c) in total
414				 " 7F 00" + (127 * " 01") +  # first run, contains 128 points: [0 .. 127]
415				 " 7F" + (128 * " 01") +  # second run, contains 128 points: [128 .. 255]
416				 " 2B" + (44 * " 01"),  # third run, contains 44 points: [256 .. 299]
417				 hexencode(compilePoints(range(300))))
418		self.assertEqual("81 8F" +  # 399 points (0x18f) in total
419				 " 7F 00" + (127 * " 01") +  # first run, contains 128 points: [0 .. 127]
420				 " 7F" + (128 * " 01") +  # second run, contains 128 points: [128 .. 255]
421				 " 7F" + (128 * " 01") +  # third run, contains 128 points: [256 .. 383]
422				 " 0E" + (15 * " 01"),  # fourth run, contains 15 points: [384 .. 398]
423				 hexencode(compilePoints(range(399))))
424
425	def test_decompilePoints(self):
426		numPointsInGlyph = 65536
427		allPoints = list(range(numPointsInGlyph))
428		def decompilePoints(data, offset):
429			points, offset = TupleVariation.decompilePoints_(numPointsInGlyph, deHexStr(data), offset, "gvar")
430			# Conversion to list needed for Python 3.
431			return (list(points), offset)
432		# all points in glyph
433		self.assertEqual((allPoints, 1), decompilePoints("00", 0))
434		# all points in glyph (in overly verbose encoding, not explicitly prohibited by spec)
435		self.assertEqual((allPoints, 2), decompilePoints("80 00", 0))
436		# 2 points; first run: [9, 9+6]
437		self.assertEqual(([9, 15], 4), decompilePoints("02 01 09 06", 0))
438		# 2 points; first run: [0xBEEF, 0xCAFE]. (0x0C0F = 0xCAFE - 0xBEEF)
439		self.assertEqual(([0xBEEF, 0xCAFE], 6), decompilePoints("02 81 BE EF 0C 0F", 0))
440		# 1 point; first run: [7]
441		self.assertEqual(([7], 3), decompilePoints("01 00 07", 0))
442		# 1 point; first run: [7] in overly verbose encoding
443		self.assertEqual(([7], 4), decompilePoints("01 80 00 07", 0))
444		# 1 point; first run: [65535]; requires words to be treated as unsigned numbers
445		self.assertEqual(([65535], 4), decompilePoints("01 80 FF FF", 0))
446		# 4 points; first run: [7, 8]; second run: [255, 257]. 257 is stored in delta-encoded bytes (0xFF + 2).
447		self.assertEqual(([7, 8, 263, 265], 7), decompilePoints("04 01 07 01 01 FF 02", 0))
448		# combination of all encodings, preceded and followed by 4 bytes of unused data
449		data = "DE AD DE AD 04 01 07 01 81 BE E7 0C 0F DE AD DE AD"
450		self.assertEqual(([7, 8, 0xBEEF, 0xCAFE], 13), decompilePoints(data, 4))
451		self.assertSetEqual(set(range(300)), set(decompilePoints(
452		    "81 2C" +  # 300 points (0x12c) in total
453		    " 7F 00" + (127 * " 01") +  # first run, contains 128 points: [0 .. 127]
454		    " 7F" + (128 * " 01") +  # second run, contains 128 points: [128 .. 255]
455		    " AB" + (44 * " 00 01"),  # third run, contains 44 points: [256 .. 299]
456		    0)[0]))
457		self.assertSetEqual(set(range(399)), set(decompilePoints(
458		    "81 8F" +  # 399 points (0x18f) in total
459		    " 7F 00" + (127 * " 01") +  # first run, contains 128 points: [0 .. 127]
460		    " 7F" + (128 * " 01") +  # second run, contains 128 points: [128 .. 255]
461		    " FF" + (128 * " 00 01") + # third run, contains 128 points: [256 .. 383]
462		    " 8E" + (15 * " 00 01"),  # fourth run, contains 15 points: [384 .. 398]
463		    0)[0]))
464
465	def test_decompilePoints_shouldAcceptBadPointNumbers(self):
466		decompilePoints = TupleVariation.decompilePoints_
467		# 2 points; first run: [3, 9].
468		numPointsInGlyph = 8
469		with CapturingLogHandler(log, "WARNING") as captor:
470			decompilePoints(numPointsInGlyph,
471			                deHexStr("02 01 03 06"), 0, "cvar")
472		self.assertIn("point 9 out of range in 'cvar' table",
473		              [r.msg for r in captor.records])
474
475	def test_decompilePoints_roundTrip(self):
476		numPointsInGlyph = 500  # greater than 255, so we also exercise code path for 16-bit encoding
477		compile = lambda points: TupleVariation.compilePoints(points)
478		decompile = lambda data: set(TupleVariation.decompilePoints_(numPointsInGlyph, data, 0, "gvar")[0])
479		for i in range(50):
480			points = set(random.sample(range(numPointsInGlyph), 30))
481			self.assertSetEqual(points, decompile(compile(points)),
482					    "failed round-trip decompile/compilePoints; points=%s" % points)
483		allPoints = set(range(numPointsInGlyph))
484		self.assertSetEqual(allPoints, decompile(compile(allPoints)))
485		self.assertSetEqual(allPoints, decompile(compile(set())))
486
487	def test_compileDeltas_points(self):
488		var = TupleVariation({}, [None, (1, 0), (2, 0), None, (4, 0), None])
489		# deltaX for points: [1, 2, 4]; deltaY for points: [0, 0, 0]
490		self.assertEqual("02 01 02 04 82", hexencode(var.compileDeltas()))
491
492	def test_compileDeltas_constants(self):
493		var = TupleVariation({}, [None, 1, 2, None, 4, None])
494		# delta for cvts: [1, 2, 4]
495		self.assertEqual("02 01 02 04", hexencode(var.compileDeltas()))
496
497	def test_compileDeltaValues(self):
498		compileDeltaValues = lambda values: hexencode(TupleVariation.compileDeltaValues_(values))
499		# zeroes
500		self.assertEqual("80", compileDeltaValues([0]))
501		self.assertEqual("BF", compileDeltaValues([0] * 64))
502		self.assertEqual("BF 80", compileDeltaValues([0] * 65))
503		self.assertEqual("BF A3", compileDeltaValues([0] * 100))
504		self.assertEqual("BF BF BF BF", compileDeltaValues([0] * 256))
505		# bytes
506		self.assertEqual("00 01", compileDeltaValues([1]))
507		self.assertEqual("06 01 02 03 7F 80 FF FE", compileDeltaValues([1, 2, 3, 127, -128, -1, -2]))
508		self.assertEqual("3F" + (64 * " 7F"), compileDeltaValues([127] * 64))
509		self.assertEqual("3F" + (64 * " 7F") + " 00 7F", compileDeltaValues([127] * 65))
510		# words
511		self.assertEqual("40 66 66", compileDeltaValues([0x6666]))
512		self.assertEqual("43 66 66 7F FF FF FF 80 00", compileDeltaValues([0x6666, 32767, -1, -32768]))
513		self.assertEqual("7F" + (64 * " 11 22"), compileDeltaValues([0x1122] * 64))
514		self.assertEqual("7F" + (64 * " 11 22") + " 40 11 22", compileDeltaValues([0x1122] * 65))
515		# bytes, zeroes, bytes: a single zero is more compact when encoded as part of the bytes run
516		self.assertEqual("04 7F 7F 00 7F 7F", compileDeltaValues([127, 127, 0, 127, 127]))
517		self.assertEqual("01 7F 7F 81 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 127, 127]))
518		self.assertEqual("01 7F 7F 82 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 127, 127]))
519		self.assertEqual("01 7F 7F 83 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 0, 127, 127]))
520		# bytes, zeroes
521		self.assertEqual("01 01 00", compileDeltaValues([1, 0]))
522		self.assertEqual("00 01 81", compileDeltaValues([1, 0, 0]))
523		# words, bytes, words: a single byte is more compact when encoded as part of the words run
524		self.assertEqual("42 66 66 00 02 77 77", compileDeltaValues([0x6666, 2, 0x7777]))
525		self.assertEqual("40 66 66 01 02 02 40 77 77", compileDeltaValues([0x6666, 2, 2, 0x7777]))
526		# words, zeroes, words
527		self.assertEqual("40 66 66 80 40 77 77", compileDeltaValues([0x6666, 0, 0x7777]))
528		self.assertEqual("40 66 66 81 40 77 77", compileDeltaValues([0x6666, 0, 0, 0x7777]))
529		self.assertEqual("40 66 66 82 40 77 77", compileDeltaValues([0x6666, 0, 0, 0, 0x7777]))
530		# words, zeroes, bytes
531		self.assertEqual("40 66 66 80 02 01 02 03", compileDeltaValues([0x6666, 0, 1, 2, 3]))
532		self.assertEqual("40 66 66 81 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 1, 2, 3]))
533		self.assertEqual("40 66 66 82 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 0, 1, 2, 3]))
534		# words, zeroes
535		self.assertEqual("40 66 66 80", compileDeltaValues([0x6666, 0]))
536		self.assertEqual("40 66 66 81", compileDeltaValues([0x6666, 0, 0]))
537
538	def test_decompileDeltas(self):
539		decompileDeltas = TupleVariation.decompileDeltas_
540		# 83 = zero values (0x80), count = 4 (1 + 0x83 & 0x3F)
541		self.assertEqual(([0, 0, 0, 0], 1), decompileDeltas(4, deHexStr("83"), 0))
542		# 41 01 02 FF FF = signed 16-bit values (0x40), count = 2 (1 + 0x41 & 0x3F)
543		self.assertEqual(([258, -1], 5), decompileDeltas(2, deHexStr("41 01 02 FF FF"), 0))
544		# 01 81 07 = signed 8-bit values, count = 2 (1 + 0x01 & 0x3F)
545		self.assertEqual(([-127, 7], 3), decompileDeltas(2, deHexStr("01 81 07"), 0))
546		# combination of all three encodings, preceded and followed by 4 bytes of unused data
547		data = deHexStr("DE AD BE EF 83 40 01 02 01 81 80 DE AD BE EF")
548		self.assertEqual(([0, 0, 0, 0, 258, -127, -128], 11), decompileDeltas(7, data, 4))
549
550	def test_decompileDeltas_roundTrip(self):
551		numDeltas = 30
552		compile = TupleVariation.compileDeltaValues_
553		decompile = lambda data: TupleVariation.decompileDeltas_(numDeltas, data, 0)[0]
554		for i in range(50):
555			deltas = random.sample(range(-128, 127), 10)
556			deltas.extend(random.sample(range(-32768, 32767), 10))
557			deltas.extend([0] * 10)
558			random.shuffle(deltas)
559			self.assertListEqual(deltas, decompile(compile(deltas)))
560
561	def test_compileSharedTuples(self):
562		# Below, the peak coordinate {"wght": 1.0, "wdth": 0.8} appears
563		# three times (most frequent sorted first); {"wght": 1.0, "wdth": 0.5}
564		# and {"wght": 1.0, "wdth": 0.7} both appears two times (tie) and
565		# are sorted alphanumerically to ensure determinism.
566		# The peak coordinate {"wght": 1.0, "wdth": 0.9} appears only once
567		# and is thus ignored.
568		# Because the start and end of variation ranges is not encoded
569		# into the shared pool, they should get ignored.
570		deltas = [None] * 4
571		variations = [
572			TupleVariation({
573				"wght": (1.0, 1.0, 1.0),
574				"wdth": (0.5, 0.7, 1.0)
575			}, deltas),
576			TupleVariation({
577				"wght": (1.0, 1.0, 1.0),
578				"wdth": (0.2, 0.7, 1.0)
579			}, deltas),
580			TupleVariation({
581				"wght": (1.0, 1.0, 1.0),
582				"wdth": (0.2, 0.8, 1.0)
583			}, deltas),
584			TupleVariation({
585				"wght": (1.0, 1.0, 1.0),
586				"wdth": (0.3, 0.5, 1.0)
587			}, deltas),
588			TupleVariation({
589				"wght": (1.0, 1.0, 1.0),
590				"wdth": (0.3, 0.8, 1.0)
591			}, deltas),
592			TupleVariation({
593				"wght": (1.0, 1.0, 1.0),
594				"wdth": (0.3, 0.9, 1.0)
595			}, deltas),
596			TupleVariation({
597				"wght": (1.0, 1.0, 1.0),
598				"wdth": (0.4, 0.8, 1.0)
599			}, deltas),
600			TupleVariation({
601				"wght": (1.0, 1.0, 1.0),
602				"wdth": (0.5, 0.5, 1.0)
603			}, deltas),
604		]
605		result = compileSharedTuples(["wght", "wdth"], variations)
606		self.assertEqual([hexencode(c) for c in result],
607		                 ["40 00 33 33", "40 00 20 00", "40 00 2C CD"])
608
609	def test_decompileSharedTuples_Skia(self):
610		sharedTuples = decompileSharedTuples(
611			axisTags=["wght", "wdth"], sharedTupleCount=8,
612			data=SKIA_GVAR_SHARED_TUPLES_DATA, offset=0)
613		self.assertEqual(sharedTuples, SKIA_GVAR_SHARED_TUPLES)
614
615	def test_decompileSharedTuples_empty(self):
616		self.assertEqual(decompileSharedTuples(["wght"], 0, b"", 0), [])
617
618	def test_compileTupleVariationStore_allVariationsRedundant(self):
619		axes = {"wght": (0.3, 0.4, 0.5), "opsz": (0.7, 0.8, 0.9)}
620		variations = [
621			TupleVariation(axes, [None] * 4),
622			TupleVariation(axes, [None] * 4),
623			TupleVariation(axes, [None] * 4)
624		]
625		self.assertEqual(
626			compileTupleVariationStore(variations, pointCount=8,
627			                           axisTags=["wght", "opsz"],
628			                           sharedTupleIndices={}),
629            (0, b"", b""))
630
631	def test_compileTupleVariationStore_noVariations(self):
632		self.assertEqual(
633			compileTupleVariationStore(variations=[], pointCount=8,
634			                           axisTags=["wght", "opsz"],
635			                           sharedTupleIndices={}),
636            (0, b"", b""))
637
638	def test_compileTupleVariationStore_roundTrip_cvar(self):
639		deltas = [1, 2, 3, 4]
640		variations = [
641			TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)},
642			               deltas),
643			TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)},
644			               deltas)
645		]
646		tupleVariationCount, tuples, data = compileTupleVariationStore(
647			variations, pointCount=4, axisTags=["wght", "wdth"],
648			sharedTupleIndices={})
649		self.assertEqual(
650			decompileTupleVariationStore("cvar", ["wght", "wdth"],
651			                             tupleVariationCount, pointCount=4,
652			                             sharedTuples={}, data=(tuples + data),
653			                             pos=0, dataPos=len(tuples)),
654            variations)
655
656	def test_compileTupleVariationStore_roundTrip_gvar(self):
657		deltas = [(1,1), (2,2), (3,3), (4,4)]
658		variations = [
659			TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)},
660			               deltas),
661			TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)},
662			               deltas)
663		]
664		tupleVariationCount, tuples, data = compileTupleVariationStore(
665			variations, pointCount=4, axisTags=["wght", "wdth"],
666			sharedTupleIndices={})
667		self.assertEqual(
668			decompileTupleVariationStore("gvar", ["wght", "wdth"],
669			                             tupleVariationCount, pointCount=4,
670			                             sharedTuples={}, data=(tuples + data),
671			                             pos=0, dataPos=len(tuples)),
672            variations)
673
674	def test_decompileTupleVariationStore_Skia_I(self):
675		tvar = decompileTupleVariationStore(
676			tableTag="gvar", axisTags=["wght", "wdth"],
677			tupleVariationCount=8, pointCount=18,
678			sharedTuples=SKIA_GVAR_SHARED_TUPLES,
679			data=SKIA_GVAR_I_DATA, pos=4, dataPos=36)
680		self.assertEqual(len(tvar), 8)
681		self.assertEqual(tvar[0].axes, {"wght": (0.0, 1.0, 1.0)})
682		self.assertEqual(
683			" ".join(["%d,%d" % c for c in tvar[0].coordinates]),
684			"257,0 -127,0 -128,58 -130,90 -130,62 -130,67 -130,32 -127,0 "
685			"257,0 259,14 260,64 260,21 260,69 258,124 0,0 130,0 0,0 0,0")
686
687	def test_decompileTupleVariationStore_empty(self):
688		self.assertEqual(
689			decompileTupleVariationStore(tableTag="gvar", axisTags=[],
690			                             tupleVariationCount=0, pointCount=5,
691			                             sharedTuples=[],
692			                             data=b"", pos=4, dataPos=4),
693			[])
694
695	def test_getTupleSize(self):
696		getTupleSize = TupleVariation.getTupleSize_
697		numAxes = 3
698		self.assertEqual(4 + numAxes * 2, getTupleSize(0x8042, numAxes))
699		self.assertEqual(4 + numAxes * 4, getTupleSize(0x4077, numAxes))
700		self.assertEqual(4, getTupleSize(0x2077, numAxes))
701		self.assertEqual(4, getTupleSize(11, numAxes))
702
703	def test_inferRegion(self):
704		start, end = inferRegion_({"wght": -0.3, "wdth": 0.7})
705		self.assertEqual(start, {"wght": -0.3, "wdth": 0.0})
706		self.assertEqual(end, {"wght": 0.0, "wdth": 0.7})
707
708	@staticmethod
709	def xml_lines(writer):
710		content = writer.file.getvalue().decode("utf-8")
711		return [line.strip() for line in content.splitlines()][1:]
712
713	def test_getCoordWidth(self):
714		empty = TupleVariation({}, [])
715		self.assertEqual(empty.getCoordWidth(), 0)
716
717		empty = TupleVariation({}, [None])
718		self.assertEqual(empty.getCoordWidth(), 0)
719
720		gvarTuple = TupleVariation({}, [None, (0, 0)])
721		self.assertEqual(gvarTuple.getCoordWidth(), 2)
722
723		cvarTuple = TupleVariation({}, [None, 0])
724		self.assertEqual(cvarTuple.getCoordWidth(), 1)
725
726		cvarTuple.coordinates[1] *= 1.0
727		self.assertEqual(cvarTuple.getCoordWidth(), 1)
728
729		with self.assertRaises(TypeError):
730			TupleVariation({}, [None, "a"]).getCoordWidth()
731
732	def test_scaleDeltas_cvar(self):
733		var = TupleVariation({}, [100, None])
734
735		var.scaleDeltas(1.0)
736		self.assertEqual(var.coordinates, [100, None])
737
738		var.scaleDeltas(0.333)
739		self.assertAlmostEqual(var.coordinates[0], 33.3)
740		self.assertIsNone(var.coordinates[1])
741
742		var.scaleDeltas(0.0)
743		self.assertEqual(var.coordinates, [0, None])
744
745	def test_scaleDeltas_gvar(self):
746		var = TupleVariation({}, [(100, 200), None])
747
748		var.scaleDeltas(1.0)
749		self.assertEqual(var.coordinates, [(100, 200), None])
750
751		var.scaleDeltas(0.333)
752		self.assertAlmostEqual(var.coordinates[0][0], 33.3)
753		self.assertAlmostEqual(var.coordinates[0][1], 66.6)
754		self.assertIsNone(var.coordinates[1])
755
756		var.scaleDeltas(0.0)
757		self.assertEqual(var.coordinates, [(0, 0), None])
758
759	def test_roundDeltas_cvar(self):
760		var = TupleVariation({}, [55.5, None, 99.9])
761		var.roundDeltas()
762		self.assertEqual(var.coordinates, [56, None, 100])
763
764	def test_roundDeltas_gvar(self):
765		var = TupleVariation({}, [(55.5, 100.0), None, (99.9, 100.0)])
766		var.roundDeltas()
767		self.assertEqual(var.coordinates, [(56, 100), None, (100, 100)])
768
769	def test_calcInferredDeltas(self):
770		var = TupleVariation({}, [(0, 0), None, None, None])
771		coords = [(1, 1), (1, 1), (1, 1), (1, 1)]
772
773		var.calcInferredDeltas(coords, [])
774
775		self.assertEqual(
776			var.coordinates,
777			[(0, 0), (0, 0), (0, 0), (0, 0)]
778		)
779
780	def test_calcInferredDeltas_invalid(self):
781		# cvar tuples can't have inferred deltas
782		with self.assertRaises(TypeError):
783			TupleVariation({}, [0]).calcInferredDeltas([], [])
784
785		# origCoords must have same length as self.coordinates
786		with self.assertRaises(ValueError):
787			TupleVariation({}, [(0, 0), None]).calcInferredDeltas([], [])
788
789		# at least 4 phantom points required
790		with self.assertRaises(AssertionError):
791			TupleVariation({}, [(0, 0), None]).calcInferredDeltas([(0, 0), (0, 0)], [])
792
793		with self.assertRaises(AssertionError):
794			TupleVariation({}, [(0, 0)] + [None]*5).calcInferredDeltas(
795				[(0, 0)]*6,
796				[1, 0]  # endPts not in increasing order
797			)
798
799	def test_optimize(self):
800		var = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0)]*5)
801
802		var.optimize([(0, 0)]*5, [0])
803
804		self.assertEqual(var.coordinates, [None, None, None, None, None])
805
806	def test_optimize_isComposite(self):
807		# when a composite glyph's deltas are all (0, 0), we still want
808		# to write out an entry in gvar, else macOS doesn't apply any
809		# variations to the composite glyph (even if its individual components
810		# do vary).
811		# https://github.com/fonttools/fonttools/issues/1381
812		var = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0)]*5)
813		var.optimize([(0, 0)]*5, [0], isComposite=True)
814		self.assertEqual(var.coordinates, [(0, 0)]*5)
815
816		# it takes more than 128 (0, 0) deltas before the optimized tuple with
817		# (None) inferred deltas (except for the first) becomes smaller than
818		# the un-optimized one that has all deltas explicitly set to (0, 0).
819		var = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0)]*129)
820		var.optimize([(0, 0)]*129, list(range(129-4)), isComposite=True)
821		self.assertEqual(var.coordinates, [(0, 0)] + [None]*128)
822
823	def test_sum_deltas_gvar(self):
824		var1 = TupleVariation(
825			{},
826			[
827				(-20, 0), (-20, 0), (20, 0), (20, 0),
828				(0, 0), (0, 0), (0, 0), (0, 0),
829			]
830		)
831		var2 = TupleVariation(
832			{},
833			[
834				(-10, 0), (-10, 0), (10, 0), (10, 0),
835				(0, 0), (20, 0), (0, 0), (0, 0),
836			]
837		)
838
839		var1 += var2
840
841		self.assertEqual(
842			var1.coordinates,
843			[
844				(-30, 0), (-30, 0), (30, 0), (30, 0),
845				(0, 0), (20, 0), (0, 0), (0, 0),
846			]
847		)
848
849	def test_sum_deltas_gvar_invalid_length(self):
850		var1 = TupleVariation({}, [(1, 2)])
851		var2 = TupleVariation({}, [(1, 2), (3, 4)])
852
853		with self.assertRaisesRegex(ValueError, "deltas with different lengths"):
854			var1 += var2
855
856	def test_sum_deltas_gvar_with_inferred_points(self):
857		var1 = TupleVariation({}, [(1, 2), None])
858		var2 = TupleVariation({}, [(2, 3), None])
859
860		with self.assertRaisesRegex(ValueError, "deltas with inferred points"):
861			var1 += var2
862
863	def test_sum_deltas_cvar(self):
864		axes = {"wght": (0.0, 1.0, 1.0)}
865		var1 = TupleVariation(axes, [0, 1, None, None])
866		var2 = TupleVariation(axes, [None, 2, None, 3])
867		var3 = TupleVariation(axes, [None, None, None, 4])
868
869		var1 += var2
870		var1 += var3
871
872		self.assertEqual(var1.coordinates, [0, 3, None, 7])
873
874
875if __name__ == "__main__":
876	import sys
877	sys.exit(unittest.main())
878