Home | History | Annotate | Download | only in tables
      1 from __future__ import \
      2 	print_function, division, absolute_import, unicode_literals
      3 from fontTools.misc.py23 import *
      4 from fontTools.misc.loggingTools import CapturingLogHandler
      5 from fontTools.misc.testTools import parseXML
      6 from fontTools.misc.textTools import deHexStr, hexStr
      7 from fontTools.misc.xmlWriter import XMLWriter
      8 from fontTools.ttLib.tables.TupleVariation import \
      9 	log, TupleVariation, compileSharedTuples, decompileSharedTuples, \
     10 	compileTupleVariationStore, decompileTupleVariationStore, inferRegion_
     11 import random
     12 import unittest
     13 
     14 
     15 def hexencode(s):
     16 	h = hexStr(s).upper()
     17 	return ' '.join([h[i:i+2] for i in range(0, len(h), 2)])
     18 
     19 
     20 AXES = {
     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
     30 SKIA_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 
     34 SKIA_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
     51 SKIA_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 
     71 class 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 
    685 if __name__ == "__main__":
    686 	import sys
    687 	sys.exit(unittest.main())
    688