• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from __future__ import print_function, division, absolute_import, unicode_literals
2from fontTools.misc.py23 import *
3from fontTools import ttLib
4from fontTools.ttLib.woff2 import (
5	WOFF2Reader, woff2DirectorySize, woff2DirectoryFormat,
6	woff2FlagsSize, woff2UnknownTagSize, woff2Base128MaxSize, WOFF2DirectoryEntry,
7	getKnownTagIndex, packBase128, base128Size, woff2UnknownTagIndex,
8	WOFF2FlavorData, woff2TransformedTableTags, WOFF2GlyfTable, WOFF2LocaTable,
9	WOFF2Writer, unpackBase128, unpack255UShort, pack255UShort)
10import unittest
11from fontTools.misc import sstruct
12import struct
13import os
14import random
15import copy
16from collections import OrderedDict
17
18haveBrotli = False
19try:
20	import brotli
21	haveBrotli = True
22except ImportError:
23	pass
24
25
26# Python 3 renamed 'assertRaisesRegexp' to 'assertRaisesRegex', and fires
27# deprecation warnings if a program uses the old name.
28if not hasattr(unittest.TestCase, 'assertRaisesRegex'):
29	unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
30
31
32current_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
33data_dir = os.path.join(current_dir, 'data')
34TTX = os.path.join(data_dir, 'TestTTF-Regular.ttx')
35OTX = os.path.join(data_dir, 'TestOTF-Regular.otx')
36METADATA = os.path.join(data_dir, 'test_woff2_metadata.xml')
37
38TT_WOFF2 = BytesIO()
39CFF_WOFF2 = BytesIO()
40
41
42def setUpModule():
43	if not haveBrotli:
44		raise unittest.SkipTest("No module named brotli")
45	assert os.path.exists(TTX)
46	assert os.path.exists(OTX)
47	# import TT-flavoured test font and save it as WOFF2
48	ttf = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
49	ttf.importXML(TTX)
50	ttf.flavor = "woff2"
51	ttf.save(TT_WOFF2, reorderTables=None)
52	# import CFF-flavoured test font and save it as WOFF2
53	otf = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
54	otf.importXML(OTX)
55	otf.flavor = "woff2"
56	otf.save(CFF_WOFF2, reorderTables=None)
57
58
59class WOFF2ReaderTest(unittest.TestCase):
60
61	@classmethod
62	def setUpClass(cls):
63		cls.file = BytesIO(CFF_WOFF2.getvalue())
64		cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
65		cls.font.importXML(OTX)
66
67	def setUp(self):
68		self.file.seek(0)
69
70	def test_bad_signature(self):
71		with self.assertRaisesRegex(ttLib.TTLibError, 'bad signature'):
72			WOFF2Reader(BytesIO(b"wOFF"))
73
74	def test_not_enough_data_header(self):
75		incomplete_header = self.file.read(woff2DirectorySize - 1)
76		with self.assertRaisesRegex(ttLib.TTLibError, 'not enough data'):
77			WOFF2Reader(BytesIO(incomplete_header))
78
79	def test_incorrect_compressed_size(self):
80		data = self.file.read(woff2DirectorySize)
81		header = sstruct.unpack(woff2DirectoryFormat, data)
82		header['totalCompressedSize'] = 0
83		data = sstruct.pack(woff2DirectoryFormat, header)
84		with self.assertRaises((brotli.error, ttLib.TTLibError)):
85			WOFF2Reader(BytesIO(data + self.file.read()))
86
87	def test_incorrect_uncompressed_size(self):
88		decompress_backup = brotli.decompress
89		brotli.decompress = lambda data: b""  # return empty byte string
90		with self.assertRaisesRegex(ttLib.TTLibError, 'unexpected size for decompressed'):
91			WOFF2Reader(self.file)
92		brotli.decompress = decompress_backup
93
94	def test_incorrect_file_size(self):
95		data = self.file.read(woff2DirectorySize)
96		header = sstruct.unpack(woff2DirectoryFormat, data)
97		header['length'] -= 1
98		data = sstruct.pack(woff2DirectoryFormat, header)
99		with self.assertRaisesRegex(
100				ttLib.TTLibError, "doesn't match the actual file size"):
101			WOFF2Reader(BytesIO(data + self.file.read()))
102
103	def test_num_tables(self):
104		tags = [t for t in self.font.keys() if t not in ('GlyphOrder', 'DSIG')]
105		data = self.file.read(woff2DirectorySize)
106		header = sstruct.unpack(woff2DirectoryFormat, data)
107		self.assertEqual(header['numTables'], len(tags))
108
109	def test_table_tags(self):
110		tags = set([t for t in self.font.keys() if t not in ('GlyphOrder', 'DSIG')])
111		reader = WOFF2Reader(self.file)
112		self.assertEqual(set(reader.keys()), tags)
113
114	def test_get_normal_tables(self):
115		woff2Reader = WOFF2Reader(self.file)
116		specialTags = woff2TransformedTableTags + ('head', 'GlyphOrder', 'DSIG')
117		for tag in [t for t in self.font.keys() if t not in specialTags]:
118			origData = self.font.getTableData(tag)
119			decompressedData = woff2Reader[tag]
120			self.assertEqual(origData, decompressedData)
121
122	def test_reconstruct_unknown(self):
123		reader = WOFF2Reader(self.file)
124		with self.assertRaisesRegex(ttLib.TTLibError, 'transform for table .* unknown'):
125			reader.reconstructTable('ZZZZ')
126
127
128class WOFF2ReaderTTFTest(WOFF2ReaderTest):
129	""" Tests specific to TT-flavored fonts. """
130
131	@classmethod
132	def setUpClass(cls):
133		cls.file = BytesIO(TT_WOFF2.getvalue())
134		cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
135		cls.font.importXML(TTX)
136
137	def setUp(self):
138		self.file.seek(0)
139
140	def test_reconstruct_glyf(self):
141		woff2Reader = WOFF2Reader(self.file)
142		reconstructedData = woff2Reader['glyf']
143		self.assertEqual(self.font.getTableData('glyf'), reconstructedData)
144
145	def test_reconstruct_loca(self):
146		woff2Reader = WOFF2Reader(self.file)
147		reconstructedData = woff2Reader['loca']
148		self.assertEqual(self.font.getTableData('loca'), reconstructedData)
149		self.assertTrue(hasattr(woff2Reader.tables['glyf'], 'data'))
150
151	def test_reconstruct_loca_not_match_orig_size(self):
152		reader = WOFF2Reader(self.file)
153		reader.tables['loca'].origLength -= 1
154		with self.assertRaisesRegex(
155				ttLib.TTLibError, "'loca' table doesn't match original size"):
156			reader.reconstructTable('loca')
157
158
159def normalise_table(font, tag, padding=4):
160	""" Return normalised table data. Keep 'font' instance unmodified. """
161	assert tag in ('glyf', 'loca', 'head')
162	assert tag in font
163	if tag == 'head':
164		origHeadFlags = font['head'].flags
165		font['head'].flags |= (1 << 11)
166		tableData = font['head'].compile(font)
167	if font.sfntVersion in ("\x00\x01\x00\x00", "true"):
168		assert {'glyf', 'loca', 'head'}.issubset(font.keys())
169		origIndexFormat = font['head'].indexToLocFormat
170		if hasattr(font['loca'], 'locations'):
171			origLocations = font['loca'].locations[:]
172		else:
173			origLocations = []
174		glyfTable = ttLib.newTable('glyf')
175		glyfTable.decompile(font.getTableData('glyf'), font)
176		glyfTable.padding = padding
177		if tag == 'glyf':
178			tableData = glyfTable.compile(font)
179		elif tag == 'loca':
180			glyfTable.compile(font)
181			tableData = font['loca'].compile(font)
182		if tag == 'head':
183			glyfTable.compile(font)
184			font['loca'].compile(font)
185			tableData = font['head'].compile(font)
186		font['head'].indexToLocFormat = origIndexFormat
187		font['loca'].set(origLocations)
188	if tag == 'head':
189		font['head'].flags = origHeadFlags
190	return tableData
191
192
193def normalise_font(font, padding=4):
194	""" Return normalised font data. Keep 'font' instance unmodified. """
195	# drop DSIG but keep a copy
196	DSIG_copy = copy.deepcopy(font['DSIG'])
197	del font['DSIG']
198	# ovverride TTFont attributes
199	origFlavor = font.flavor
200	origRecalcBBoxes = font.recalcBBoxes
201	origRecalcTimestamp = font.recalcTimestamp
202	origLazy = font.lazy
203	font.flavor = None
204	font.recalcBBoxes = False
205	font.recalcTimestamp = False
206	font.lazy = True
207	# save font to temporary stream
208	infile = BytesIO()
209	font.save(infile)
210	infile.seek(0)
211	# reorder tables alphabetically
212	outfile = BytesIO()
213	reader = ttLib.sfnt.SFNTReader(infile)
214	writer = ttLib.sfnt.SFNTWriter(
215		outfile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData)
216	for tag in sorted(reader.keys()):
217		if tag in woff2TransformedTableTags + ('head',):
218			writer[tag] = normalise_table(font, tag, padding)
219		else:
220			writer[tag] = reader[tag]
221	writer.close()
222	# restore font attributes
223	font['DSIG'] = DSIG_copy
224	font.flavor = origFlavor
225	font.recalcBBoxes = origRecalcBBoxes
226	font.recalcTimestamp = origRecalcTimestamp
227	font.lazy = origLazy
228	return outfile.getvalue()
229
230
231class WOFF2DirectoryEntryTest(unittest.TestCase):
232
233	def setUp(self):
234		self.entry = WOFF2DirectoryEntry()
235
236	def test_not_enough_data_table_flags(self):
237		with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'flags'"):
238			self.entry.fromString(b"")
239
240	def test_not_enough_data_table_tag(self):
241		incompleteData = bytearray([0x3F, 0, 0, 0])
242		with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'tag'"):
243			self.entry.fromString(bytes(incompleteData))
244
245	def test_table_reserved_flags(self):
246		with self.assertRaisesRegex(ttLib.TTLibError, "bits 6-7 are reserved"):
247			self.entry.fromString(bytechr(0xC0))
248
249	def test_loca_zero_transformLength(self):
250		data = bytechr(getKnownTagIndex('loca'))  # flags
251		data += packBase128(random.randint(1, 100))  # origLength
252		data += packBase128(1)  # non-zero transformLength
253		with self.assertRaisesRegex(
254				ttLib.TTLibError, "transformLength of the 'loca' table must be 0"):
255			self.entry.fromString(data)
256
257	def test_fromFile(self):
258		unknownTag = Tag('ZZZZ')
259		data = bytechr(getKnownTagIndex(unknownTag))
260		data += unknownTag.tobytes()
261		data += packBase128(random.randint(1, 100))
262		expectedPos = len(data)
263		f = BytesIO(data + b'\0'*100)
264		self.entry.fromFile(f)
265		self.assertEqual(f.tell(), expectedPos)
266
267	def test_transformed_toString(self):
268		self.entry.tag = Tag('glyf')
269		self.entry.flags = getKnownTagIndex(self.entry.tag)
270		self.entry.origLength = random.randint(101, 200)
271		self.entry.length = random.randint(1, 100)
272		expectedSize = (woff2FlagsSize + base128Size(self.entry.origLength) +
273			base128Size(self.entry.length))
274		data = self.entry.toString()
275		self.assertEqual(len(data), expectedSize)
276
277	def test_known_toString(self):
278		self.entry.tag = Tag('head')
279		self.entry.flags = getKnownTagIndex(self.entry.tag)
280		self.entry.origLength = 54
281		expectedSize = (woff2FlagsSize + base128Size(self.entry.origLength))
282		data = self.entry.toString()
283		self.assertEqual(len(data), expectedSize)
284
285	def test_unknown_toString(self):
286		self.entry.tag = Tag('ZZZZ')
287		self.entry.flags = woff2UnknownTagIndex
288		self.entry.origLength = random.randint(1, 100)
289		expectedSize = (woff2FlagsSize + woff2UnknownTagSize +
290			base128Size(self.entry.origLength))
291		data = self.entry.toString()
292		self.assertEqual(len(data), expectedSize)
293
294
295class DummyReader(WOFF2Reader):
296
297	def __init__(self, file, checkChecksums=1, fontNumber=-1):
298		self.file = file
299		for attr in ('majorVersion', 'minorVersion', 'metaOffset', 'metaLength',
300				'metaOrigLength', 'privLength', 'privOffset'):
301			setattr(self, attr, 0)
302
303
304class WOFF2FlavorDataTest(unittest.TestCase):
305
306	@classmethod
307	def setUpClass(cls):
308		assert os.path.exists(METADATA)
309		with open(METADATA, 'rb') as f:
310			cls.xml_metadata = f.read()
311		cls.compressed_metadata = brotli.compress(cls.xml_metadata, mode=brotli.MODE_TEXT)
312		# make random byte strings; font data must be 4-byte aligned
313		cls.fontdata = bytes(bytearray(random.sample(range(0, 256), 80)))
314		cls.privData = bytes(bytearray(random.sample(range(0, 256), 20)))
315
316	def setUp(self):
317		self.file = BytesIO(self.fontdata)
318		self.file.seek(0, 2)
319
320	def test_get_metaData_no_privData(self):
321		self.file.write(self.compressed_metadata)
322		reader = DummyReader(self.file)
323		reader.metaOffset = len(self.fontdata)
324		reader.metaLength = len(self.compressed_metadata)
325		reader.metaOrigLength = len(self.xml_metadata)
326		flavorData = WOFF2FlavorData(reader)
327		self.assertEqual(self.xml_metadata, flavorData.metaData)
328
329	def test_get_privData_no_metaData(self):
330		self.file.write(self.privData)
331		reader = DummyReader(self.file)
332		reader.privOffset = len(self.fontdata)
333		reader.privLength = len(self.privData)
334		flavorData = WOFF2FlavorData(reader)
335		self.assertEqual(self.privData, flavorData.privData)
336
337	def test_get_metaData_and_privData(self):
338		self.file.write(self.compressed_metadata + self.privData)
339		reader = DummyReader(self.file)
340		reader.metaOffset = len(self.fontdata)
341		reader.metaLength = len(self.compressed_metadata)
342		reader.metaOrigLength = len(self.xml_metadata)
343		reader.privOffset = reader.metaOffset + reader.metaLength
344		reader.privLength = len(self.privData)
345		flavorData = WOFF2FlavorData(reader)
346		self.assertEqual(self.xml_metadata, flavorData.metaData)
347		self.assertEqual(self.privData, flavorData.privData)
348
349	def test_get_major_minorVersion(self):
350		reader = DummyReader(self.file)
351		reader.majorVersion = reader.minorVersion = 1
352		flavorData = WOFF2FlavorData(reader)
353		self.assertEqual(flavorData.majorVersion, 1)
354		self.assertEqual(flavorData.minorVersion, 1)
355
356
357class WOFF2WriterTest(unittest.TestCase):
358
359	@classmethod
360	def setUpClass(cls):
361		cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False, flavor="woff2")
362		cls.font.importXML(OTX)
363		cls.tags = [t for t in cls.font.keys() if t != 'GlyphOrder']
364		cls.numTables = len(cls.tags)
365		cls.file = BytesIO(CFF_WOFF2.getvalue())
366		cls.file.seek(0, 2)
367		cls.length = (cls.file.tell() + 3) & ~3
368		cls.setUpFlavorData()
369
370	@classmethod
371	def setUpFlavorData(cls):
372		assert os.path.exists(METADATA)
373		with open(METADATA, 'rb') as f:
374			cls.xml_metadata = f.read()
375		cls.compressed_metadata = brotli.compress(cls.xml_metadata, mode=brotli.MODE_TEXT)
376		cls.privData = bytes(bytearray(random.sample(range(0, 256), 20)))
377
378	def setUp(self):
379		self.file.seek(0)
380		self.writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion)
381
382	def test_DSIG_dropped(self):
383		self.writer['DSIG'] = b"\0"
384		self.assertEqual(len(self.writer.tables), 0)
385		self.assertEqual(self.writer.numTables, self.numTables-1)
386
387	def test_no_rewrite_table(self):
388		self.writer['ZZZZ'] = b"\0"
389		with self.assertRaisesRegex(ttLib.TTLibError, "cannot rewrite"):
390			self.writer['ZZZZ'] = b"\0"
391
392	def test_num_tables(self):
393		self.writer['ABCD'] = b"\0"
394		with self.assertRaisesRegex(ttLib.TTLibError, "wrong number of tables"):
395			self.writer.close()
396
397	def test_required_tables(self):
398		font = ttLib.TTFont(flavor="woff2")
399		with self.assertRaisesRegex(ttLib.TTLibError, "missing required table"):
400			font.save(BytesIO())
401
402	def test_head_transform_flag(self):
403		headData = self.font.getTableData('head')
404		origFlags = byteord(headData[16])
405		woff2font = ttLib.TTFont(self.file)
406		newHeadData = woff2font.getTableData('head')
407		modifiedFlags = byteord(newHeadData[16])
408		self.assertNotEqual(origFlags, modifiedFlags)
409		restoredFlags = modifiedFlags & ~0x08  # turn off bit 11
410		self.assertEqual(origFlags, restoredFlags)
411
412	def test_tables_sorted_alphabetically(self):
413		expected = sorted([t for t in self.tags if t != 'DSIG'])
414		woff2font = ttLib.TTFont(self.file)
415		self.assertEqual(expected, list(woff2font.reader.keys()))
416
417	def test_checksums(self):
418		normFile = BytesIO(normalise_font(self.font, padding=4))
419		normFile.seek(0)
420		normFont = ttLib.TTFont(normFile, checkChecksums=2)
421		w2font = ttLib.TTFont(self.file)
422		# force reconstructing glyf table using 4-byte padding
423		w2font.reader.padding = 4
424		for tag in [t for t in self.tags if t != 'DSIG']:
425			w2data = w2font.reader[tag]
426			normData = normFont.reader[tag]
427			if tag == "head":
428				w2data = w2data[:8] + b'\0\0\0\0' + w2data[12:]
429				normData = normData[:8] + b'\0\0\0\0' + normData[12:]
430			w2CheckSum = ttLib.sfnt.calcChecksum(w2data)
431			normCheckSum = ttLib.sfnt.calcChecksum(normData)
432			self.assertEqual(w2CheckSum, normCheckSum)
433		normCheckSumAdjustment = normFont['head'].checkSumAdjustment
434		self.assertEqual(normCheckSumAdjustment, w2font['head'].checkSumAdjustment)
435
436	def test_calcSFNTChecksumsLengthsAndOffsets(self):
437		normFont = ttLib.TTFont(BytesIO(normalise_font(self.font, padding=4)))
438		for tag in self.tags:
439			self.writer[tag] = self.font.getTableData(tag)
440		self.writer._normaliseGlyfAndLoca(padding=4)
441		self.writer._setHeadTransformFlag()
442		self.writer.tables = OrderedDict(sorted(self.writer.tables.items()))
443		self.writer._calcSFNTChecksumsLengthsAndOffsets()
444		for tag, entry in normFont.reader.tables.items():
445			self.assertEqual(entry.offset, self.writer.tables[tag].origOffset)
446			self.assertEqual(entry.length, self.writer.tables[tag].origLength)
447			self.assertEqual(entry.checkSum, self.writer.tables[tag].checkSum)
448
449	def test_bad_sfntVersion(self):
450		for i in range(self.numTables):
451			self.writer[bytechr(65 + i)*4] = b"\0"
452		self.writer.sfntVersion = 'ZZZZ'
453		with self.assertRaisesRegex(ttLib.TTLibError, "bad sfntVersion"):
454			self.writer.close()
455
456	def test_calcTotalSize_no_flavorData(self):
457		expected = self.length
458		self.writer.file = BytesIO()
459		for tag in self.tags:
460			self.writer[tag] = self.font.getTableData(tag)
461		self.writer.close()
462		self.assertEqual(expected, self.writer.length)
463		self.assertEqual(expected, self.writer.file.tell())
464
465	def test_calcTotalSize_with_metaData(self):
466		expected = self.length + len(self.compressed_metadata)
467		flavorData = self.writer.flavorData = WOFF2FlavorData()
468		flavorData.metaData = self.xml_metadata
469		self.writer.file = BytesIO()
470		for tag in self.tags:
471			self.writer[tag] = self.font.getTableData(tag)
472		self.writer.close()
473		self.assertEqual(expected, self.writer.length)
474		self.assertEqual(expected, self.writer.file.tell())
475
476	def test_calcTotalSize_with_privData(self):
477		expected = self.length + len(self.privData)
478		flavorData = self.writer.flavorData = WOFF2FlavorData()
479		flavorData.privData = self.privData
480		self.writer.file = BytesIO()
481		for tag in self.tags:
482			self.writer[tag] = self.font.getTableData(tag)
483		self.writer.close()
484		self.assertEqual(expected, self.writer.length)
485		self.assertEqual(expected, self.writer.file.tell())
486
487	def test_calcTotalSize_with_metaData_and_privData(self):
488		metaDataLength = (len(self.compressed_metadata) + 3) & ~3
489		expected = self.length + metaDataLength + len(self.privData)
490		flavorData = self.writer.flavorData = WOFF2FlavorData()
491		flavorData.metaData = self.xml_metadata
492		flavorData.privData = self.privData
493		self.writer.file = BytesIO()
494		for tag in self.tags:
495			self.writer[tag] = self.font.getTableData(tag)
496		self.writer.close()
497		self.assertEqual(expected, self.writer.length)
498		self.assertEqual(expected, self.writer.file.tell())
499
500	def test_getVersion(self):
501		# no version
502		self.assertEqual((0, 0), self.writer._getVersion())
503		# version from head.fontRevision
504		fontRevision = self.font['head'].fontRevision
505		versionTuple = tuple(int(i) for i in str(fontRevision).split("."))
506		entry = self.writer.tables['head'] = ttLib.newTable('head')
507		entry.data = self.font.getTableData('head')
508		self.assertEqual(versionTuple, self.writer._getVersion())
509		# version from writer.flavorData
510		flavorData = self.writer.flavorData = WOFF2FlavorData()
511		flavorData.majorVersion, flavorData.minorVersion = (10, 11)
512		self.assertEqual((10, 11), self.writer._getVersion())
513
514
515class WOFF2WriterTTFTest(WOFF2WriterTest):
516
517	@classmethod
518	def setUpClass(cls):
519		cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False, flavor="woff2")
520		cls.font.importXML(TTX)
521		cls.tags = [t for t in cls.font.keys() if t != 'GlyphOrder']
522		cls.numTables = len(cls.tags)
523		cls.file = BytesIO(TT_WOFF2.getvalue())
524		cls.file.seek(0, 2)
525		cls.length = (cls.file.tell() + 3) & ~3
526		cls.setUpFlavorData()
527
528	def test_normaliseGlyfAndLoca(self):
529		normTables = {}
530		for tag in ('head', 'loca', 'glyf'):
531			normTables[tag] = normalise_table(self.font, tag, padding=4)
532		for tag in self.tags:
533			tableData = self.font.getTableData(tag)
534			self.writer[tag] = tableData
535			if tag in normTables:
536				self.assertNotEqual(tableData, normTables[tag])
537		self.writer._normaliseGlyfAndLoca(padding=4)
538		self.writer._setHeadTransformFlag()
539		for tag in normTables:
540			self.assertEqual(self.writer.tables[tag].data, normTables[tag])
541
542
543class WOFF2LocaTableTest(unittest.TestCase):
544
545	def setUp(self):
546		self.font = font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
547		font['head'] = ttLib.newTable('head')
548		font['loca'] = WOFF2LocaTable()
549		font['glyf'] = WOFF2GlyfTable()
550
551	def test_compile_short_loca(self):
552		locaTable = self.font['loca']
553		locaTable.set(list(range(0, 0x20000, 2)))
554		self.font['glyf'].indexFormat = 0
555		locaData = locaTable.compile(self.font)
556		self.assertEqual(len(locaData), 0x20000)
557
558	def test_compile_short_loca_overflow(self):
559		locaTable = self.font['loca']
560		locaTable.set(list(range(0x20000 + 1)))
561		self.font['glyf'].indexFormat = 0
562		with self.assertRaisesRegex(
563				ttLib.TTLibError, "indexFormat is 0 but local offsets > 0x20000"):
564			locaTable.compile(self.font)
565
566	def test_compile_short_loca_not_multiples_of_2(self):
567		locaTable = self.font['loca']
568		locaTable.set([1, 3, 5, 7])
569		self.font['glyf'].indexFormat = 0
570		with self.assertRaisesRegex(ttLib.TTLibError, "offsets not multiples of 2"):
571			locaTable.compile(self.font)
572
573	def test_compile_long_loca(self):
574		locaTable = self.font['loca']
575		locaTable.set(list(range(0x20001)))
576		self.font['glyf'].indexFormat = 1
577		locaData = locaTable.compile(self.font)
578		self.assertEqual(len(locaData), 0x20001 * 4)
579
580	def test_compile_set_indexToLocFormat_0(self):
581		locaTable = self.font['loca']
582		# offsets are all multiples of 2 and max length is < 0x10000
583		locaTable.set(list(range(0, 0x20000, 2)))
584		locaTable.compile(self.font)
585		newIndexFormat = self.font['head'].indexToLocFormat
586		self.assertEqual(0, newIndexFormat)
587
588	def test_compile_set_indexToLocFormat_1(self):
589		locaTable = self.font['loca']
590		# offsets are not multiples of 2
591		locaTable.set(list(range(10)))
592		locaTable.compile(self.font)
593		newIndexFormat = self.font['head'].indexToLocFormat
594		self.assertEqual(1, newIndexFormat)
595		# max length is >= 0x10000
596		locaTable.set(list(range(0, 0x20000 + 1, 2)))
597		locaTable.compile(self.font)
598		newIndexFormat = self.font['head'].indexToLocFormat
599		self.assertEqual(1, newIndexFormat)
600
601
602class WOFF2GlyfTableTest(unittest.TestCase):
603
604	@classmethod
605	def setUpClass(cls):
606		font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
607		font.importXML(TTX)
608		cls.tables = {}
609		cls.transformedTags = ('maxp', 'head', 'loca', 'glyf')
610		for tag in reversed(cls.transformedTags):  # compile in inverse order
611			cls.tables[tag] = font.getTableData(tag)
612		infile = BytesIO(TT_WOFF2.getvalue())
613		reader = WOFF2Reader(infile)
614		cls.transformedGlyfData = reader.tables['glyf'].loadData(
615			reader.transformBuffer)
616		cls.glyphOrder = ['.notdef'] + ["glyph%.5d" % i for i in range(1, font['maxp'].numGlyphs)]
617
618	def setUp(self):
619		self.font = font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False)
620		font.setGlyphOrder(self.glyphOrder)
621		font['head'] = ttLib.newTable('head')
622		font['maxp'] = ttLib.newTable('maxp')
623		font['loca'] = WOFF2LocaTable()
624		font['glyf'] = WOFF2GlyfTable()
625		for tag in self.transformedTags:
626			font[tag].decompile(self.tables[tag], font)
627
628	def test_reconstruct_glyf_padded_4(self):
629		glyfTable = WOFF2GlyfTable()
630		glyfTable.reconstruct(self.transformedGlyfData, self.font)
631		glyfTable.padding = 4
632		data = glyfTable.compile(self.font)
633		normGlyfData = normalise_table(self.font, 'glyf', glyfTable.padding)
634		self.assertEqual(normGlyfData, data)
635
636	def test_reconstruct_glyf_padded_2(self):
637		glyfTable = WOFF2GlyfTable()
638		glyfTable.reconstruct(self.transformedGlyfData, self.font)
639		glyfTable.padding = 2
640		data = glyfTable.compile(self.font)
641		normGlyfData = normalise_table(self.font, 'glyf', glyfTable.padding)
642		self.assertEqual(normGlyfData, data)
643
644	def test_reconstruct_glyf_unpadded(self):
645		glyfTable = WOFF2GlyfTable()
646		glyfTable.reconstruct(self.transformedGlyfData, self.font)
647		data = glyfTable.compile(self.font)
648		self.assertEqual(self.tables['glyf'], data)
649
650	def test_reconstruct_glyf_incorrect_glyphOrder(self):
651		glyfTable = WOFF2GlyfTable()
652		badGlyphOrder = self.font.getGlyphOrder()[:-1]
653		self.font.setGlyphOrder(badGlyphOrder)
654		with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"):
655			glyfTable.reconstruct(self.transformedGlyfData, self.font)
656
657	def test_reconstruct_glyf_missing_glyphOrder(self):
658		glyfTable = WOFF2GlyfTable()
659		del self.font.glyphOrder
660		numGlyphs = self.font['maxp'].numGlyphs
661		del self.font['maxp']
662		glyfTable.reconstruct(self.transformedGlyfData, self.font)
663		expected = [".notdef"]
664		expected.extend(["glyph%.5d" % i for i in range(1, numGlyphs)])
665		self.assertEqual(expected, glyfTable.glyphOrder)
666
667	def test_reconstruct_loca_padded_4(self):
668		locaTable = self.font['loca'] = WOFF2LocaTable()
669		glyfTable = self.font['glyf'] = WOFF2GlyfTable()
670		glyfTable.reconstruct(self.transformedGlyfData, self.font)
671		glyfTable.padding = 4
672		glyfTable.compile(self.font)
673		data = locaTable.compile(self.font)
674		normLocaData = normalise_table(self.font, 'loca', glyfTable.padding)
675		self.assertEqual(normLocaData, data)
676
677	def test_reconstruct_loca_padded_2(self):
678		locaTable = self.font['loca'] = WOFF2LocaTable()
679		glyfTable = self.font['glyf'] = WOFF2GlyfTable()
680		glyfTable.reconstruct(self.transformedGlyfData, self.font)
681		glyfTable.padding = 2
682		glyfTable.compile(self.font)
683		data = locaTable.compile(self.font)
684		normLocaData = normalise_table(self.font, 'loca', glyfTable.padding)
685		self.assertEqual(normLocaData, data)
686
687	def test_reconstruct_loca_unpadded(self):
688		locaTable = self.font['loca'] = WOFF2LocaTable()
689		glyfTable = self.font['glyf'] = WOFF2GlyfTable()
690		glyfTable.reconstruct(self.transformedGlyfData, self.font)
691		glyfTable.compile(self.font)
692		data = locaTable.compile(self.font)
693		self.assertEqual(self.tables['loca'], data)
694
695	def test_reconstruct_glyf_header_not_enough_data(self):
696		with self.assertRaisesRegex(ttLib.TTLibError, "not enough 'glyf' data"):
697			WOFF2GlyfTable().reconstruct(b"", self.font)
698
699	def test_reconstruct_glyf_table_incorrect_size(self):
700		msg = "incorrect size of transformed 'glyf'"
701		with self.assertRaisesRegex(ttLib.TTLibError, msg):
702			WOFF2GlyfTable().reconstruct(self.transformedGlyfData + b"\x00", self.font)
703		with self.assertRaisesRegex(ttLib.TTLibError, msg):
704			WOFF2GlyfTable().reconstruct(self.transformedGlyfData[:-1], self.font)
705
706	def test_transform_glyf(self):
707		glyfTable = self.font['glyf']
708		data = glyfTable.transform(self.font)
709		self.assertEqual(self.transformedGlyfData, data)
710
711	def test_transform_glyf_incorrect_glyphOrder(self):
712		glyfTable = self.font['glyf']
713		badGlyphOrder = self.font.getGlyphOrder()[:-1]
714		del glyfTable.glyphOrder
715		self.font.setGlyphOrder(badGlyphOrder)
716		with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"):
717			glyfTable.transform(self.font)
718		glyfTable.glyphOrder = badGlyphOrder
719		with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"):
720			glyfTable.transform(self.font)
721
722	def test_transform_glyf_missing_glyphOrder(self):
723		glyfTable = self.font['glyf']
724		del glyfTable.glyphOrder
725		del self.font.glyphOrder
726		numGlyphs = self.font['maxp'].numGlyphs
727		del self.font['maxp']
728		glyfTable.transform(self.font)
729		expected = [".notdef"]
730		expected.extend(["glyph%.5d" % i for i in range(1, numGlyphs)])
731		self.assertEqual(expected, glyfTable.glyphOrder)
732
733	def test_roundtrip_glyf_reconstruct_and_transform(self):
734		glyfTable = WOFF2GlyfTable()
735		glyfTable.reconstruct(self.transformedGlyfData, self.font)
736		data = glyfTable.transform(self.font)
737		self.assertEqual(self.transformedGlyfData, data)
738
739	def test_roundtrip_glyf_transform_and_reconstruct(self):
740		glyfTable = self.font['glyf']
741		transformedData = glyfTable.transform(self.font)
742		newGlyfTable = WOFF2GlyfTable()
743		newGlyfTable.reconstruct(transformedData, self.font)
744		newGlyfTable.padding = 4
745		reconstructedData = newGlyfTable.compile(self.font)
746		normGlyfData = normalise_table(self.font, 'glyf', newGlyfTable.padding)
747		self.assertEqual(normGlyfData, reconstructedData)
748
749
750class Base128Test(unittest.TestCase):
751
752	def test_unpackBase128(self):
753		self.assertEqual(unpackBase128(b'\x3f\x00\x00'), (63, b"\x00\x00"))
754		self.assertEqual(unpackBase128(b'\x8f\xff\xff\xff\x7f')[0], 4294967295)
755
756		self.assertRaisesRegex(
757			ttLib.TTLibError,
758			"UIntBase128 value must not start with leading zeros",
759			unpackBase128, b'\x80\x80\x3f')
760
761		self.assertRaisesRegex(
762			ttLib.TTLibError,
763			"UIntBase128-encoded sequence is longer than 5 bytes",
764			unpackBase128, b'\x8f\xff\xff\xff\xff\x7f')
765
766		self.assertRaisesRegex(
767			ttLib.TTLibError,
768			r"UIntBase128 value exceeds 2\*\*32-1",
769			unpackBase128, b'\x90\x80\x80\x80\x00')
770
771		self.assertRaisesRegex(
772			ttLib.TTLibError,
773			"not enough data to unpack UIntBase128",
774			unpackBase128, b'')
775
776	def test_base128Size(self):
777		self.assertEqual(base128Size(0), 1)
778		self.assertEqual(base128Size(24567), 3)
779		self.assertEqual(base128Size(2**32-1), 5)
780
781	def test_packBase128(self):
782		self.assertEqual(packBase128(63), b"\x3f")
783		self.assertEqual(packBase128(2**32-1), b'\x8f\xff\xff\xff\x7f')
784		self.assertRaisesRegex(
785			ttLib.TTLibError,
786			r"UIntBase128 format requires 0 <= integer <= 2\*\*32-1",
787			packBase128, 2**32+1)
788		self.assertRaisesRegex(
789			ttLib.TTLibError,
790			r"UIntBase128 format requires 0 <= integer <= 2\*\*32-1",
791			packBase128, -1)
792
793
794class UShort255Test(unittest.TestCase):
795
796	def test_unpack255UShort(self):
797		self.assertEqual(unpack255UShort(bytechr(252))[0], 252)
798		# some numbers (e.g. 506) can have multiple encodings
799		self.assertEqual(
800			unpack255UShort(struct.pack(b"BB", 254, 0))[0], 506)
801		self.assertEqual(
802			unpack255UShort(struct.pack(b"BB", 255, 253))[0], 506)
803		self.assertEqual(
804			unpack255UShort(struct.pack(b"BBB", 253, 1, 250))[0], 506)
805
806		self.assertRaisesRegex(
807			ttLib.TTLibError,
808			"not enough data to unpack 255UInt16",
809			unpack255UShort, struct.pack(b"BB", 253, 0))
810
811		self.assertRaisesRegex(
812			ttLib.TTLibError,
813			"not enough data to unpack 255UInt16",
814			unpack255UShort, struct.pack(b"B", 254))
815
816		self.assertRaisesRegex(
817			ttLib.TTLibError,
818			"not enough data to unpack 255UInt16",
819			unpack255UShort, struct.pack(b"B", 255))
820
821	def test_pack255UShort(self):
822		self.assertEqual(pack255UShort(252), b'\xfc')
823		self.assertEqual(pack255UShort(505), b'\xff\xfc')
824		self.assertEqual(pack255UShort(506), b'\xfe\x00')
825		self.assertEqual(pack255UShort(762), b'\xfd\x02\xfa')
826
827		self.assertRaisesRegex(
828			ttLib.TTLibError,
829			"255UInt16 format requires 0 <= integer <= 65535",
830			pack255UShort, -1)
831
832		self.assertRaisesRegex(
833			ttLib.TTLibError,
834			"255UInt16 format requires 0 <= integer <= 65535",
835			pack255UShort, 0xFFFF+1)
836
837
838if __name__ == "__main__":
839	import sys
840	sys.exit(unittest.main())
841