1from __future__ import \
2	print_function, division, absolute_import, unicode_literals
3from fontTools.misc.py23 import *
4from fontTools.misc.loggingTools import CapturingLogHandler
5from fontTools.misc.testTools import parseXML
6from fontTools.misc.textTools import deHexStr, hexStr
7from fontTools.misc.xmlWriter import XMLWriter
8from fontTools.ttLib.tables.TupleVariation import \
9	log, TupleVariation, compileSharedTuples, decompileSharedTuples, \
10	compileTupleVariationStore, decompileTupleVariationStore, inferRegion_
11import random
12import unittest
13
14
15def hexencode(s):
16	h = hexStr(s).upper()
17	return ' '.join([h[i:i+2] for i in range(0, len(h), 2)])
18
19
20AXES = {
21	"wdth": (0.3, 0.4, 0.5),
22	"wght": (0.0, 1.0, 1.0),
23	"opsz": (-0.7, -0.7, 0.0)
24}
25
26
27# Shared tuples in the 'gvar' table of the Skia font, as printed
28# in Apple's TrueType specification.
29# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
30SKIA_GVAR_SHARED_TUPLES_DATA = deHexStr(
31	"40 00 00 00 C0 00 00 00 00 00 40 00 00 00 C0 00 "
32	"C0 00 C0 00 40 00 C0 00 40 00 40 00 C0 00 40 00")
33
34SKIA_GVAR_SHARED_TUPLES = [
35	{"wght": 1.0, "wdth": 0.0},
36	{"wght": -1.0, "wdth": 0.0},
37	{"wght": 0.0, "wdth": 1.0},
38	{"wght": 0.0, "wdth": -1.0},
39	{"wght": -1.0, "wdth": -1.0},
40	{"wght": 1.0, "wdth": -1.0},
41	{"wght": 1.0, "wdth": 1.0},
42	{"wght": -1.0, "wdth": 1.0}
43]
44
45
46# Tuple Variation Store of uppercase I in the Skia font, as printed in Apple's
47# TrueType spec. The actual Skia font uses a different table for uppercase I
48# than what is printed in Apple's spec, but we still want to make sure that
49# we can parse the data as it appears in the specification.
50# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
51SKIA_GVAR_I_DATA = deHexStr(
52	"00 08 00 24 00 33 20 00 00 15 20 01 00 1B 20 02 "
53	"00 24 20 03 00 15 20 04 00 26 20 07 00 0D 20 06 "
54	"00 1A 20 05 00 40 01 01 01 81 80 43 FF 7E FF 7E "
55	"FF 7E FF 7E 00 81 45 01 01 01 03 01 04 01 04 01 "
56	"04 01 02 80 40 00 82 81 81 04 3A 5A 3E 43 20 81 "
57	"04 0E 40 15 45 7C 83 00 0D 9E F3 F2 F0 F0 F0 F0 "
58	"F3 9E A0 A1 A1 A1 9F 80 00 91 81 91 00 0D 0A 0A "
59	"09 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0B 80 00 15 81 "
60	"81 00 C4 89 00 C4 83 00 0D 80 99 98 96 96 96 96 "
61	"99 80 82 83 83 83 81 80 40 FF 18 81 81 04 E6 F9 "
62	"10 21 02 81 04 E8 E5 EB 4D DA 83 00 0D CE D3 D4 "
63	"D3 D3 D3 D5 D2 CE CC CD CD CD CD 80 00 A1 81 91 "
64	"00 0D 07 03 04 02 02 02 03 03 07 07 08 08 08 07 "
65	"80 00 09 81 81 00 28 40 00 A4 02 24 24 66 81 04 "
66	"08 FA FA FA 28 83 00 82 02 FF FF FF 83 02 01 01 "
67	"01 84 91 00 80 06 07 08 08 08 08 0A 07 80 03 FE "
68	"FF FF FF 81 00 08 81 82 02 EE EE EE 8B 6D 00")
69
70
71class TupleVariationTest(unittest.TestCase):
72	def test_equal(self):
73		var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
74		var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
75		self.assertEqual(var1, var2)
76
77	def test_equal_differentAxes(self):
78		var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
79		var2 = TupleVariation({"wght":(0.7, 0.8, 0.9)}, [(0,0), (9,8), (7,6)])
80		self.assertNotEqual(var1, var2)
81
82	def test_equal_differentCoordinates(self):
83		var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)])
84		var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8)])
85		self.assertNotEqual(var1, var2)
86
87	def test_hasImpact_someDeltasNotZero(self):
88		axes = {"wght":(0.0, 1.0, 1.0)}
89		var = TupleVariation(axes, [(0,0), (9,8), (7,6)])
90		self.assertTrue(var.hasImpact())
91
92	def test_hasImpact_allDeltasZero(self):
93		axes = {"wght":(0.0, 1.0, 1.0)}
94		var = TupleVariation(axes, [(0,0), (0,0), (0,0)])
95		self.assertTrue(var.hasImpact())
96
97	def test_hasImpact_allDeltasNone(self):
98		axes = {"wght":(0.0, 1.0, 1.0)}
99		var = TupleVariation(axes, [None, None, None])
100		self.assertFalse(var.hasImpact())
101
102	def test_toXML_badDeltaFormat(self):
103		writer = XMLWriter(BytesIO())
104		g = TupleVariation(AXES, ["String"])
105		with CapturingLogHandler(log, "ERROR") as captor:
106			g.toXML(writer, ["wdth"])
107		self.assertIn("bad delta format", [r.msg for r in captor.records])
108		self.assertEqual([
109			'<tuple>',
110			  '<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>',
111			  '<!-- bad delta #0 -->',
112			'</tuple>',
113		], TupleVariationTest.xml_lines(writer))
114
115	def test_toXML_constants(self):
116		writer = XMLWriter(BytesIO())
117		g = TupleVariation(AXES, [42, None, 23, 0, -17, None])
118		g.toXML(writer, ["wdth", "wght", "opsz"])
119		self.assertEqual([
120			'<tuple>',
121			  '<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>',
122			  '<coord axis="wght" value="1.0"/>',
123			  '<coord axis="opsz" value="-0.7"/>',
124			  '<delta cvt="0" value="42"/>',
125			  '<delta cvt="2" value="23"/>',
126			  '<delta cvt="3" value="0"/>',
127			  '<delta cvt="4" value="-17"/>',
128			'</tuple>'
129		], TupleVariationTest.xml_lines(writer))
130
131	def test_toXML_points(self):
132		writer = XMLWriter(BytesIO())
133		g = TupleVariation(AXES, [(9,8), None, (7,6), (0,0), (-1,-2), None])
134		g.toXML(writer, ["wdth", "wght", "opsz"])
135		self.assertEqual([
136			'<tuple>',
137			  '<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>',
138			  '<coord axis="wght" value="1.0"/>',
139			  '<coord axis="opsz" value="-0.7"/>',
140			  '<delta pt="0" x="9" y="8"/>',
141			  '<delta pt="2" x="7" y="6"/>',
142			  '<delta pt="3" x="0" y="0"/>',
143			  '<delta pt="4" x="-1" y="-2"/>',
144			'</tuple>'
145		], TupleVariationTest.xml_lines(writer))
146
147	def test_toXML_allDeltasNone(self):
148		writer = XMLWriter(BytesIO())
149		axes = {"wght":(0.0, 1.0, 1.0)}
150		g = TupleVariation(axes, [None] * 5)
151		g.toXML(writer, ["wght", "wdth"])
152		self.assertEqual([
153			'<tuple>',
154			  '<coord axis="wght" value="1.0"/>',
155			  '<!-- no deltas -->',
156			'</tuple>'
157		], TupleVariationTest.xml_lines(writer))
158
159	def test_fromXML_badDeltaFormat(self):
160		g = TupleVariation({}, [])
161		with CapturingLogHandler(log, "WARNING") as captor:
162			for name, attrs, content in parseXML('<delta a="1" b="2"/>'):
163				g.fromXML(name, attrs, content)
164		self.assertIn("bad delta format: a, b",
165		              [r.msg for r in captor.records])
166
167	def test_fromXML_constants(self):
168		g = TupleVariation({}, [None] * 4)
169		for name, attrs, content in parseXML(
170				'<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>'
171				'<coord axis="wght" value="1.0"/>'
172				'<coord axis="opsz" value="-0.7"/>'
173				'<delta cvt="1" value="42"/>'
174				'<delta cvt="2" value="-23"/>'):
175			g.fromXML(name, attrs, content)
176		self.assertEqual(AXES, g.axes)
177		self.assertEqual([None, 42, -23, None], g.coordinates)
178
179	def test_fromXML_points(self):
180		g = TupleVariation({}, [None] * 4)
181		for name, attrs, content in parseXML(
182				'<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>'
183				'<coord axis="wght" value="1.0"/>'
184				'<coord axis="opsz" value="-0.7"/>'
185				'<delta pt="1" x="33" y="44"/>'
186				'<delta pt="2" x="-2" y="170"/>'):
187			g.fromXML(name, attrs, content)
188		self.assertEqual(AXES, g.axes)
189		self.assertEqual([None, (33, 44), (-2, 170), None], g.coordinates)
190
191	def test_compile_sharedPeaks_nonIntermediate_sharedPoints(self):
192		var = TupleVariation(
193			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
194			[(7,4), (8,5), (9,6)])
195		axisTags = ["wght", "wdth"]
196		sharedPeakIndices = { var.compileCoord(axisTags): 0x77 }
197		tup, deltas, _ = var.compile(axisTags, sharedPeakIndices,
198		                          sharedPoints={0,1,2})
199		# len(deltas)=8; flags=None; tupleIndex=0x77
200		# embeddedPeaks=[]; intermediateCoord=[]
201		self.assertEqual("00 08 00 77", hexencode(tup))
202		self.assertEqual("02 07 08 09 "     # deltaX: [7, 8, 9]
203						 "02 04 05 06",     # deltaY: [4, 5, 6]
204						 hexencode(deltas))
205
206	def test_compile_sharedPeaks_intermediate_sharedPoints(self):
207		var = TupleVariation(
208			{"wght": (0.3, 0.5, 0.7), "wdth": (0.1, 0.8, 0.9)},
209			[(7,4), (8,5), (9,6)])
210		axisTags = ["wght", "wdth"]
211		sharedPeakIndices = { var.compileCoord(axisTags): 0x77 }
212		tup, deltas, _ = var.compile(axisTags, sharedPeakIndices,
213		                          sharedPoints={0,1,2})
214		# len(deltas)=8; flags=INTERMEDIATE_REGION; tupleIndex=0x77
215		# embeddedPeak=[]; intermediateCoord=[(0.3, 0.1), (0.7, 0.9)]
216		self.assertEqual("00 08 40 77 13 33 06 66 2C CD 39 9A", hexencode(tup))
217		self.assertEqual("02 07 08 09 "     # deltaX: [7, 8, 9]
218						 "02 04 05 06",     # deltaY: [4, 5, 6]
219						 hexencode(deltas))
220
221	def test_compile_sharedPeaks_nonIntermediate_privatePoints(self):
222		var = TupleVariation(
223			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
224			[(7,4), (8,5), (9,6)])
225		axisTags = ["wght", "wdth"]
226		sharedPeakIndices = { var.compileCoord(axisTags): 0x77 }
227		tup, deltas, _ = var.compile(axisTags, sharedPeakIndices,
228		                          sharedPoints=None)
229		# len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77
230		# embeddedPeak=[]; intermediateCoord=[]
231		self.assertEqual("00 09 20 77", hexencode(tup))
232		self.assertEqual("00 "              # all points in glyph
233						 "02 07 08 09 "     # deltaX: [7, 8, 9]
234						 "02 04 05 06",     # deltaY: [4, 5, 6]
235						 hexencode(deltas))
236
237	def test_compile_sharedPeaks_intermediate_privatePoints(self):
238		var = TupleVariation(
239			{"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 1.0)},
240			[(7,4), (8,5), (9,6)])
241		axisTags = ["wght", "wdth"]
242		sharedPeakIndices = { var.compileCoord(axisTags): 0x77 }
243		tuple, deltas, _ = var.compile(axisTags,
244		                            sharedPeakIndices, sharedPoints=None)
245		# len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77
246		# embeddedPeak=[]; intermediateCoord=[(0.0, 0.0), (1.0, 1.0)]
247		self.assertEqual("00 09 60 77 00 00 00 00 40 00 40 00",
248		                 hexencode(tuple))
249		self.assertEqual("00 "              # all points in glyph
250						 "02 07 08 09 "     # deltaX: [7, 8, 9]
251						 "02 04 05 06",     # deltaY: [4, 5, 6]
252						 hexencode(deltas))
253
254	def test_compile_embeddedPeak_nonIntermediate_sharedPoints(self):
255		var = TupleVariation(
256			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
257			[(7,4), (8,5), (9,6)])
258		tup, deltas, _ = var.compile(axisTags=["wght", "wdth"],
259		                          sharedCoordIndices={}, sharedPoints={0, 1, 2})
260		# len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE
261		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
262		self.assertEqual("00 08 80 00 20 00 33 33", hexencode(tup))
263		self.assertEqual("02 07 08 09 "     # deltaX: [7, 8, 9]
264						 "02 04 05 06",     # deltaY: [4, 5, 6]
265						 hexencode(deltas))
266
267	def test_compile_embeddedPeak_nonIntermediate_sharedConstants(self):
268		var = TupleVariation(
269			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
270			[3, 1, 4])
271		tup, deltas, _ = var.compile(axisTags=["wght", "wdth"],
272		                          sharedCoordIndices={}, sharedPoints={0, 1, 2})
273		# len(deltas)=4; flags=EMBEDDED_PEAK_TUPLE
274		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
275		self.assertEqual("00 04 80 00 20 00 33 33", hexencode(tup))
276		self.assertEqual("02 03 01 04",     # delta: [3, 1, 4]
277						 hexencode(deltas))
278
279	def test_compile_embeddedPeak_intermediate_sharedPoints(self):
280		var = TupleVariation(
281			{"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 0.8)},
282			[(7,4), (8,5), (9,6)])
283		tup, deltas, _ = var.compile(axisTags=["wght", "wdth"],
284		                          sharedCoordIndices={},
285		                          sharedPoints={0, 1, 2})
286		# len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE
287		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[(0.0, 0.0), (1.0, 0.8)]
288		self.assertEqual("00 08 C0 00 20 00 33 33 00 00 00 00 40 00 33 33",
289		                hexencode(tup))
290		self.assertEqual("02 07 08 09 "  # deltaX: [7, 8, 9]
291						 "02 04 05 06",  # deltaY: [4, 5, 6]
292						 hexencode(deltas))
293
294	def test_compile_embeddedPeak_nonIntermediate_privatePoints(self):
295		var = TupleVariation(
296			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
297			[(7,4), (8,5), (9,6)])
298		tup, deltas, _ = var.compile(
299			axisTags=["wght", "wdth"], sharedCoordIndices={}, sharedPoints=None)
300		# len(deltas)=9; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE
301		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
302		self.assertEqual("00 09 A0 00 20 00 33 33", hexencode(tup))
303		self.assertEqual("00 "           # all points in glyph
304		                 "02 07 08 09 "  # deltaX: [7, 8, 9]
305		                 "02 04 05 06",  # deltaY: [4, 5, 6]
306		                 hexencode(deltas))
307
308	def test_compile_embeddedPeak_nonIntermediate_privateConstants(self):
309		var = TupleVariation(
310			{"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)},
311			[7, 8, 9])
312		tup, deltas, _ = var.compile(
313			axisTags=["wght", "wdth"], sharedCoordIndices={}, sharedPoints=None)
314		# len(deltas)=5; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE
315		# embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[]
316		self.assertEqual("00 05 A0 00 20 00 33 33", hexencode(tup))
317		self.assertEqual("00 "           # all points in glyph
318		                 "02 07 08 09",  # delta: [7, 8, 9]
319		                 hexencode(deltas))
320
321	def test_compile_embeddedPeak_intermediate_privatePoints(self):
322		var = TupleVariation(
323			{"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)},
324			[(7,4), (8,5), (9,6)])
325		tup, deltas, _ = var.compile(
326			axisTags = ["wght", "wdth"],
327			sharedCoordIndices={}, sharedPoints=None)
328		# len(deltas)=9;
329		# flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE
330		# embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)]
331		self.assertEqual("00 09 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A",
332		                 hexencode(tup))
333		self.assertEqual("00 "              # all points in glyph
334		                 "02 07 08 09 "     # deltaX: [7, 8, 9]
335		                 "02 04 05 06",     # deltaY: [4, 5, 6]
336		                 hexencode(deltas))
337
338	def test_compile_embeddedPeak_intermediate_privateConstants(self):
339		var = TupleVariation(
340			{"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)},
341			[7, 8, 9])
342		tup, deltas, _ = var.compile(
343			axisTags = ["wght", "wdth"],
344			sharedCoordIndices={}, sharedPoints=None)
345		# len(deltas)=5;
346		# flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE
347		# embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)]
348		self.assertEqual("00 05 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A",
349		                 hexencode(tup))
350		self.assertEqual("00 "             # all points in glyph
351		                 "02 07 08 09",    # delta: [7, 8, 9]
352		                 hexencode(deltas))
353
354	def test_compileCoord(self):
355		var = TupleVariation({"wght": (-1.0, -1.0, -1.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4)
356		self.assertEqual("C0 00 20 00", hexencode(var.compileCoord(["wght", "wdth"])))
357		self.assertEqual("20 00 C0 00", hexencode(var.compileCoord(["wdth", "wght"])))
358		self.assertEqual("C0 00", hexencode(var.compileCoord(["wght"])))
359
360	def test_compileIntermediateCoord(self):
361		var = TupleVariation({"wght": (-1.0, -1.0, 0.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4)
362		self.assertEqual("C0 00 19 9A 00 00 26 66", hexencode(var.compileIntermediateCoord(["wght", "wdth"])))
363		self.assertEqual("19 9A C0 00 26 66 00 00", hexencode(var.compileIntermediateCoord(["wdth", "wght"])))
364		self.assertEqual(None, var.compileIntermediateCoord(["wght"]))
365		self.assertEqual("19 9A 26 66", hexencode(var.compileIntermediateCoord(["wdth"])))
366
367	def test_decompileCoord(self):
368		decompileCoord = TupleVariation.decompileCoord_
369		data = deHexStr("DE AD C0 00 20 00 DE AD")
370		self.assertEqual(({"wght": -1.0, "wdth": 0.5}, 6), decompileCoord(["wght", "wdth"], data, 2))
371
372	def test_decompileCoord_roundTrip(self):
373		# Make sure we are not affected by https://github.com/fonttools/fonttools/issues/286
374		data = deHexStr("7F B9 80 35")
375		values, _ = TupleVariation.decompileCoord_(["wght", "wdth"], data, 0)
376		axisValues = {axis:(val, val, val) for axis, val in  values.items()}
377		var = TupleVariation(axisValues, [None] * 4)
378		self.assertEqual("7F B9 80 35", hexencode(var.compileCoord(["wght", "wdth"])))
379
380	def test_compilePoints(self):
381		compilePoints = lambda p: TupleVariation.compilePoints(set(p), numPointsInGlyph=999)
382		self.assertEqual("00", hexencode(compilePoints(range(999))))  # all points in glyph
383		self.assertEqual("01 00 07", hexencode(compilePoints([7])))
384		self.assertEqual("01 80 FF FF", hexencode(compilePoints([65535])))
385		self.assertEqual("02 01 09 06", hexencode(compilePoints([9, 15])))
386		self.assertEqual("06 05 07 01 F7 02 01 F2", hexencode(compilePoints([7, 8, 255, 257, 258, 500])))
387		self.assertEqual("03 01 07 01 80 01 EC", hexencode(compilePoints([7, 8, 500])))
388		self.assertEqual("04 01 07 01 81 BE E7 0C 0F", hexencode(compilePoints([7, 8, 0xBEEF, 0xCAFE])))
389		self.maxDiff = None
390		self.assertEqual("81 2C" +  # 300 points (0x12c) in total
391				 " 7F 00" + (127 * " 01") +  # first run, contains 128 points: [0 .. 127]
392				 " 7F" + (128 * " 01") +  # second run, contains 128 points: [128 .. 255]
393				 " 2B" + (44 * " 01"),  # third run, contains 44 points: [256 .. 299]
394				 hexencode(compilePoints(range(300))))
395		self.assertEqual("81 8F" +  # 399 points (0x18f) in total
396				 " 7F 00" + (127 * " 01") +  # first run, contains 128 points: [0 .. 127]
397				 " 7F" + (128 * " 01") +  # second run, contains 128 points: [128 .. 255]
398				 " 7F" + (128 * " 01") +  # third run, contains 128 points: [256 .. 383]
399				 " 0E" + (15 * " 01"),  # fourth run, contains 15 points: [384 .. 398]
400				 hexencode(compilePoints(range(399))))
401
402	def test_decompilePoints(self):
403		numPointsInGlyph = 65536
404		allPoints = list(range(numPointsInGlyph))
405		def decompilePoints(data, offset):
406			points, offset = TupleVariation.decompilePoints_(numPointsInGlyph, deHexStr(data), offset, "gvar")
407			# Conversion to list needed for Python 3.
408			return (list(points), offset)
409		# all points in glyph
410		self.assertEqual((allPoints, 1), decompilePoints("00", 0))
411		# all points in glyph (in overly verbose encoding, not explicitly prohibited by spec)
412		self.assertEqual((allPoints, 2), decompilePoints("80 00", 0))
413		# 2 points; first run: [9, 9+6]
414		self.assertEqual(([9, 15], 4), decompilePoints("02 01 09 06", 0))
415		# 2 points; first run: [0xBEEF, 0xCAFE]. (0x0C0F = 0xCAFE - 0xBEEF)
416		self.assertEqual(([0xBEEF, 0xCAFE], 6), decompilePoints("02 81 BE EF 0C 0F", 0))
417		# 1 point; first run: [7]
418		self.assertEqual(([7], 3), decompilePoints("01 00 07", 0))
419		# 1 point; first run: [7] in overly verbose encoding
420		self.assertEqual(([7], 4), decompilePoints("01 80 00 07", 0))
421		# 1 point; first run: [65535]; requires words to be treated as unsigned numbers
422		self.assertEqual(([65535], 4), decompilePoints("01 80 FF FF", 0))
423		# 4 points; first run: [7, 8]; second run: [255, 257]. 257 is stored in delta-encoded bytes (0xFF + 2).
424		self.assertEqual(([7, 8, 263, 265], 7), decompilePoints("04 01 07 01 01 FF 02", 0))
425		# combination of all encodings, preceded and followed by 4 bytes of unused data
426		data = "DE AD DE AD 04 01 07 01 81 BE E7 0C 0F DE AD DE AD"
427		self.assertEqual(([7, 8, 0xBEEF, 0xCAFE], 13), decompilePoints(data, 4))
428		self.assertSetEqual(set(range(300)), set(decompilePoints(
429		    "81 2C" +  # 300 points (0x12c) in total
430		    " 7F 00" + (127 * " 01") +  # first run, contains 128 points: [0 .. 127]
431		    " 7F" + (128 * " 01") +  # second run, contains 128 points: [128 .. 255]
432		    " AB" + (44 * " 00 01"),  # third run, contains 44 points: [256 .. 299]
433		    0)[0]))
434		self.assertSetEqual(set(range(399)), set(decompilePoints(
435		    "81 8F" +  # 399 points (0x18f) in total
436		    " 7F 00" + (127 * " 01") +  # first run, contains 128 points: [0 .. 127]
437		    " 7F" + (128 * " 01") +  # second run, contains 128 points: [128 .. 255]
438		    " FF" + (128 * " 00 01") + # third run, contains 128 points: [256 .. 383]
439		    " 8E" + (15 * " 00 01"),  # fourth run, contains 15 points: [384 .. 398]
440		    0)[0]))
441
442	def test_decompilePoints_shouldAcceptBadPointNumbers(self):
443		decompilePoints = TupleVariation.decompilePoints_
444		# 2 points; first run: [3, 9].
445		numPointsInGlyph = 8
446		with CapturingLogHandler(log, "WARNING") as captor:
447			decompilePoints(numPointsInGlyph,
448			                deHexStr("02 01 03 06"), 0, "cvar")
449		self.assertIn("point 9 out of range in 'cvar' table",
450		              [r.msg for r in captor.records])
451
452	def test_decompilePoints_roundTrip(self):
453		numPointsInGlyph = 500  # greater than 255, so we also exercise code path for 16-bit encoding
454		compile = lambda points: TupleVariation.compilePoints(points, numPointsInGlyph)
455		decompile = lambda data: set(TupleVariation.decompilePoints_(numPointsInGlyph, data, 0, "gvar")[0])
456		for i in range(50):
457			points = set(random.sample(range(numPointsInGlyph), 30))
458			self.assertSetEqual(points, decompile(compile(points)),
459					    "failed round-trip decompile/compilePoints; points=%s" % points)
460		allPoints = set(range(numPointsInGlyph))
461		self.assertSetEqual(allPoints, decompile(compile(allPoints)))
462
463	def test_compileDeltas_points(self):
464		var = TupleVariation({}, [(0,0), (1, 0), (2, 0), None, (4, 0), (5, 0)])
465		points = {1, 2, 3, 4}
466		# deltaX for points: [1, 2, 4]; deltaY for points: [0, 0, 0]
467		self.assertEqual("02 01 02 04 82", hexencode(var.compileDeltas(points)))
468
469	def test_compileDeltas_constants(self):
470		var = TupleVariation({}, [0, 1, 2, None, 4, 5])
471		cvts = {1, 2, 3, 4}
472		# delta for cvts: [1, 2, 4]
473		self.assertEqual("02 01 02 04", hexencode(var.compileDeltas(cvts)))
474
475	def test_compileDeltaValues(self):
476		compileDeltaValues = lambda values: hexencode(TupleVariation.compileDeltaValues_(values))
477		# zeroes
478		self.assertEqual("80", compileDeltaValues([0]))
479		self.assertEqual("BF", compileDeltaValues([0] * 64))
480		self.assertEqual("BF 80", compileDeltaValues([0] * 65))
481		self.assertEqual("BF A3", compileDeltaValues([0] * 100))
482		self.assertEqual("BF BF BF BF", compileDeltaValues([0] * 256))
483		# bytes
484		self.assertEqual("00 01", compileDeltaValues([1]))
485		self.assertEqual("06 01 02 03 7F 80 FF FE", compileDeltaValues([1, 2, 3, 127, -128, -1, -2]))
486		self.assertEqual("3F" + (64 * " 7F"), compileDeltaValues([127] * 64))
487		self.assertEqual("3F" + (64 * " 7F") + " 00 7F", compileDeltaValues([127] * 65))
488		# words
489		self.assertEqual("40 66 66", compileDeltaValues([0x6666]))
490		self.assertEqual("43 66 66 7F FF FF FF 80 00", compileDeltaValues([0x6666, 32767, -1, -32768]))
491		self.assertEqual("7F" + (64 * " 11 22"), compileDeltaValues([0x1122] * 64))
492		self.assertEqual("7F" + (64 * " 11 22") + " 40 11 22", compileDeltaValues([0x1122] * 65))
493		# bytes, zeroes, bytes: a single zero is more compact when encoded as part of the bytes run
494		self.assertEqual("04 7F 7F 00 7F 7F", compileDeltaValues([127, 127, 0, 127, 127]))
495		self.assertEqual("01 7F 7F 81 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 127, 127]))
496		self.assertEqual("01 7F 7F 82 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 127, 127]))
497		self.assertEqual("01 7F 7F 83 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 0, 127, 127]))
498		# bytes, zeroes
499		self.assertEqual("01 01 00", compileDeltaValues([1, 0]))
500		self.assertEqual("00 01 81", compileDeltaValues([1, 0, 0]))
501		# words, bytes, words: a single byte is more compact when encoded as part of the words run
502		self.assertEqual("42 66 66 00 02 77 77", compileDeltaValues([0x6666, 2, 0x7777]))
503		self.assertEqual("40 66 66 01 02 02 40 77 77", compileDeltaValues([0x6666, 2, 2, 0x7777]))
504		# words, zeroes, words
505		self.assertEqual("40 66 66 80 40 77 77", compileDeltaValues([0x6666, 0, 0x7777]))
506		self.assertEqual("40 66 66 81 40 77 77", compileDeltaValues([0x6666, 0, 0, 0x7777]))
507		self.assertEqual("40 66 66 82 40 77 77", compileDeltaValues([0x6666, 0, 0, 0, 0x7777]))
508		# words, zeroes, bytes
509		self.assertEqual("40 66 66 80 02 01 02 03", compileDeltaValues([0x6666, 0, 1, 2, 3]))
510		self.assertEqual("40 66 66 81 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 1, 2, 3]))
511		self.assertEqual("40 66 66 82 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 0, 1, 2, 3]))
512		# words, zeroes
513		self.assertEqual("40 66 66 80", compileDeltaValues([0x6666, 0]))
514		self.assertEqual("40 66 66 81", compileDeltaValues([0x6666, 0, 0]))
515		# bytes or words from floats
516		self.assertEqual("00 01", compileDeltaValues([1.1]))
517		self.assertEqual("00 02", compileDeltaValues([1.9]))
518		self.assertEqual("40 66 66", compileDeltaValues([0x6666 + 0.1]))
519		self.assertEqual("40 66 66", compileDeltaValues([0x6665 + 0.9]))
520
521	def test_decompileDeltas(self):
522		decompileDeltas = TupleVariation.decompileDeltas_
523		# 83 = zero values (0x80), count = 4 (1 + 0x83 & 0x3F)
524		self.assertEqual(([0, 0, 0, 0], 1), decompileDeltas(4, deHexStr("83"), 0))
525		# 41 01 02 FF FF = signed 16-bit values (0x40), count = 2 (1 + 0x41 & 0x3F)
526		self.assertEqual(([258, -1], 5), decompileDeltas(2, deHexStr("41 01 02 FF FF"), 0))
527		# 01 81 07 = signed 8-bit values, count = 2 (1 + 0x01 & 0x3F)
528		self.assertEqual(([-127, 7], 3), decompileDeltas(2, deHexStr("01 81 07"), 0))
529		# combination of all three encodings, preceded and followed by 4 bytes of unused data
530		data = deHexStr("DE AD BE EF 83 40 01 02 01 81 80 DE AD BE EF")
531		self.assertEqual(([0, 0, 0, 0, 258, -127, -128], 11), decompileDeltas(7, data, 4))
532
533	def test_decompileDeltas_roundTrip(self):
534		numDeltas = 30
535		compile = TupleVariation.compileDeltaValues_
536		decompile = lambda data: TupleVariation.decompileDeltas_(numDeltas, data, 0)[0]
537		for i in range(50):
538			deltas = random.sample(range(-128, 127), 10)
539			deltas.extend(random.sample(range(-32768, 32767), 10))
540			deltas.extend([0] * 10)
541			random.shuffle(deltas)
542			self.assertListEqual(deltas, decompile(compile(deltas)))
543
544	def test_compileSharedTuples(self):
545		# Below, the peak coordinate {"wght": 1.0, "wdth": 0.7} appears
546		# three times; {"wght": 1.0, "wdth": 0.8} appears twice.
547		# Because the start and end of variation ranges is not encoded
548		# into the shared pool, they should get ignored.
549		deltas = [None] * 4
550		variations = [
551			TupleVariation({
552				"wght": (1.0, 1.0, 1.0),
553				"wdth": (0.5, 0.7, 1.0)
554			}, deltas),
555			TupleVariation({
556				"wght": (1.0, 1.0, 1.0),
557				"wdth": (0.2, 0.7, 1.0)
558			}, deltas),
559			TupleVariation({
560				"wght": (1.0, 1.0, 1.0),
561				"wdth": (0.2, 0.8, 1.0)
562			}, deltas),
563			TupleVariation({
564				"wght": (1.0, 1.0, 1.0),
565				"wdth": (0.3, 0.7, 1.0)
566			}, deltas),
567			TupleVariation({
568				"wght": (1.0, 1.0, 1.0),
569				"wdth": (0.3, 0.8, 1.0)
570			}, deltas),
571			TupleVariation({
572				"wght": (1.0, 1.0, 1.0),
573				"wdth": (0.3, 0.9, 1.0)
574            }, deltas)
575		]
576		result = compileSharedTuples(["wght", "wdth"], variations)
577		self.assertEqual([hexencode(c) for c in result],
578		                 ["40 00 2C CD", "40 00 33 33"])
579
580	def test_decompileSharedTuples_Skia(self):
581		sharedTuples = decompileSharedTuples(
582			axisTags=["wght", "wdth"], sharedTupleCount=8,
583			data=SKIA_GVAR_SHARED_TUPLES_DATA, offset=0)
584		self.assertEqual(sharedTuples, SKIA_GVAR_SHARED_TUPLES)
585
586	def test_decompileSharedTuples_empty(self):
587		self.assertEqual(decompileSharedTuples(["wght"], 0, b"", 0), [])
588
589	def test_compileTupleVariationStore_allVariationsRedundant(self):
590		axes = {"wght": (0.3, 0.4, 0.5), "opsz": (0.7, 0.8, 0.9)}
591		variations = [
592			TupleVariation(axes, [None] * 4),
593			TupleVariation(axes, [None] * 4),
594			TupleVariation(axes, [None] * 4)
595		]
596		self.assertEqual(
597			compileTupleVariationStore(variations, pointCount=8,
598			                           axisTags=["wght", "opsz"],
599			                           sharedTupleIndices={}),
600            (0, b"", b""))
601
602	def test_compileTupleVariationStore_noVariations(self):
603		self.assertEqual(
604			compileTupleVariationStore(variations=[], pointCount=8,
605			                           axisTags=["wght", "opsz"],
606			                           sharedTupleIndices={}),
607            (0, b"", b""))
608
609	def test_compileTupleVariationStore_roundTrip_cvar(self):
610		deltas = [1, 2, 3, 4]
611		variations = [
612			TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)},
613			               deltas),
614			TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)},
615			               deltas)
616		]
617		tupleVariationCount, tuples, data = compileTupleVariationStore(
618			variations, pointCount=4, axisTags=["wght", "wdth"],
619			sharedTupleIndices={})
620		self.assertEqual(
621			decompileTupleVariationStore("cvar", ["wght", "wdth"],
622			                             tupleVariationCount, pointCount=4,
623			                             sharedTuples={}, data=(tuples + data),
624			                             pos=0, dataPos=len(tuples)),
625            variations)
626
627	def test_compileTupleVariationStore_roundTrip_gvar(self):
628		deltas = [(1,1), (2,2), (3,3), (4,4)]
629		variations = [
630			TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)},
631			               deltas),
632			TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)},
633			               deltas)
634		]
635		tupleVariationCount, tuples, data = compileTupleVariationStore(
636			variations, pointCount=4, axisTags=["wght", "wdth"],
637			sharedTupleIndices={})
638		self.assertEqual(
639			decompileTupleVariationStore("gvar", ["wght", "wdth"],
640			                             tupleVariationCount, pointCount=4,
641			                             sharedTuples={}, data=(tuples + data),
642			                             pos=0, dataPos=len(tuples)),
643            variations)
644
645	def test_decompileTupleVariationStore_Skia_I(self):
646		tvar = decompileTupleVariationStore(
647			tableTag="gvar", axisTags=["wght", "wdth"],
648			tupleVariationCount=8, pointCount=18,
649			sharedTuples=SKIA_GVAR_SHARED_TUPLES,
650			data=SKIA_GVAR_I_DATA, pos=4, dataPos=36)
651		self.assertEqual(len(tvar), 8)
652		self.assertEqual(tvar[0].axes, {"wght": (0.0, 1.0, 1.0)})
653		self.assertEqual(
654			" ".join(["%d,%d" % c for c in tvar[0].coordinates]),
655			"257,0 -127,0 -128,58 -130,90 -130,62 -130,67 -130,32 -127,0 "
656			"257,0 259,14 260,64 260,21 260,69 258,124 0,0 130,0 0,0 0,0")
657
658	def test_decompileTupleVariationStore_empty(self):
659		self.assertEqual(
660			decompileTupleVariationStore(tableTag="gvar", axisTags=[],
661			                             tupleVariationCount=0, pointCount=5,
662			                             sharedTuples=[],
663			                             data=b"", pos=4, dataPos=4),
664			[])
665
666	def test_getTupleSize(self):
667		getTupleSize = TupleVariation.getTupleSize_
668		numAxes = 3
669		self.assertEqual(4 + numAxes * 2, getTupleSize(0x8042, numAxes))
670		self.assertEqual(4 + numAxes * 4, getTupleSize(0x4077, numAxes))
671		self.assertEqual(4, getTupleSize(0x2077, numAxes))
672		self.assertEqual(4, getTupleSize(11, numAxes))
673
674	def test_inferRegion(self):
675		start, end = inferRegion_({"wght": -0.3, "wdth": 0.7})
676		self.assertEqual(start, {"wght": -0.3, "wdth": 0.0})
677		self.assertEqual(end, {"wght": 0.0, "wdth": 0.7})
678
679	@staticmethod
680	def xml_lines(writer):
681		content = writer.file.getvalue().decode("utf-8")
682		return [line.strip() for line in content.splitlines()][1:]
683
684
685if __name__ == "__main__":
686	import sys
687	sys.exit(unittest.main())
688