from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import * from fontTools import ttLib from fontTools.ttLib.woff2 import ( WOFF2Reader, woff2DirectorySize, woff2DirectoryFormat, woff2FlagsSize, woff2UnknownTagSize, woff2Base128MaxSize, WOFF2DirectoryEntry, getKnownTagIndex, packBase128, base128Size, woff2UnknownTagIndex, WOFF2FlavorData, woff2TransformedTableTags, WOFF2GlyfTable, WOFF2LocaTable, WOFF2Writer) import unittest from fontTools.misc import sstruct import os import random import copy from collections import OrderedDict haveBrotli = False try: import brotli haveBrotli = True except ImportError: pass # Python 3 renamed 'assertRaisesRegexp' to 'assertRaisesRegex', and fires # deprecation warnings if a program uses the old name. if not hasattr(unittest.TestCase, 'assertRaisesRegex'): unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp current_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) data_dir = os.path.join(current_dir, 'data') TTX = os.path.join(data_dir, 'TestTTF-Regular.ttx') OTX = os.path.join(data_dir, 'TestOTF-Regular.otx') METADATA = os.path.join(data_dir, 'test_woff2_metadata.xml') TT_WOFF2 = BytesIO() CFF_WOFF2 = BytesIO() def setUpModule(): if not haveBrotli: raise unittest.SkipTest("No module named brotli") assert os.path.exists(TTX) assert os.path.exists(OTX) # import TT-flavoured test font and save it as WOFF2 ttf = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) ttf.importXML(TTX) ttf.flavor = "woff2" ttf.save(TT_WOFF2, reorderTables=None) # import CFF-flavoured test font and save it as WOFF2 otf = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) otf.importXML(OTX) otf.flavor = "woff2" otf.save(CFF_WOFF2, reorderTables=None) class WOFF2ReaderTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.file = BytesIO(CFF_WOFF2.getvalue()) cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) cls.font.importXML(OTX) def setUp(self): self.file.seek(0) def test_bad_signature(self): with self.assertRaisesRegex(ttLib.TTLibError, 'bad signature'): WOFF2Reader(BytesIO(b"wOFF")) def test_not_enough_data_header(self): incomplete_header = self.file.read(woff2DirectorySize - 1) with self.assertRaisesRegex(ttLib.TTLibError, 'not enough data'): WOFF2Reader(BytesIO(incomplete_header)) def test_incorrect_compressed_size(self): data = self.file.read(woff2DirectorySize) header = sstruct.unpack(woff2DirectoryFormat, data) header['totalCompressedSize'] = 0 data = sstruct.pack(woff2DirectoryFormat, header) with self.assertRaises((brotli.error, ttLib.TTLibError)): WOFF2Reader(BytesIO(data + self.file.read())) def test_incorrect_uncompressed_size(self): decompress_backup = brotli.decompress brotli.decompress = lambda data: b"" # return empty byte string with self.assertRaisesRegex(ttLib.TTLibError, 'unexpected size for decompressed'): WOFF2Reader(self.file) brotli.decompress = decompress_backup def test_incorrect_file_size(self): data = self.file.read(woff2DirectorySize) header = sstruct.unpack(woff2DirectoryFormat, data) header['length'] -= 1 data = sstruct.pack(woff2DirectoryFormat, header) with self.assertRaisesRegex( ttLib.TTLibError, "doesn't match the actual file size"): WOFF2Reader(BytesIO(data + self.file.read())) def test_num_tables(self): tags = [t for t in self.font.keys() if t not in ('GlyphOrder', 'DSIG')] data = self.file.read(woff2DirectorySize) header = sstruct.unpack(woff2DirectoryFormat, data) self.assertEqual(header['numTables'], len(tags)) def test_table_tags(self): tags = set([t for t in self.font.keys() if t not in ('GlyphOrder', 'DSIG')]) reader = WOFF2Reader(self.file) self.assertEqual(set(reader.keys()), tags) def test_get_normal_tables(self): woff2Reader = WOFF2Reader(self.file) specialTags = woff2TransformedTableTags + ('head', 'GlyphOrder', 'DSIG') for tag in [t for t in self.font.keys() if t not in specialTags]: origData = self.font.getTableData(tag) decompressedData = woff2Reader[tag] self.assertEqual(origData, decompressedData) def test_reconstruct_unknown(self): reader = WOFF2Reader(self.file) with self.assertRaisesRegex(ttLib.TTLibError, 'transform for table .* unknown'): reader.reconstructTable('ZZZZ') class WOFF2ReaderTTFTest(WOFF2ReaderTest): """ Tests specific to TT-flavored fonts. """ @classmethod def setUpClass(cls): cls.file = BytesIO(TT_WOFF2.getvalue()) cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) cls.font.importXML(TTX) def setUp(self): self.file.seek(0) def test_reconstruct_glyf(self): woff2Reader = WOFF2Reader(self.file) reconstructedData = woff2Reader['glyf'] self.assertEqual(self.font.getTableData('glyf'), reconstructedData) def test_reconstruct_loca(self): woff2Reader = WOFF2Reader(self.file) reconstructedData = woff2Reader['loca'] self.assertEqual(self.font.getTableData('loca'), reconstructedData) self.assertTrue(hasattr(woff2Reader.tables['glyf'], 'data')) def test_reconstruct_loca_not_match_orig_size(self): reader = WOFF2Reader(self.file) reader.tables['loca'].origLength -= 1 with self.assertRaisesRegex( ttLib.TTLibError, "'loca' table doesn't match original size"): reader.reconstructTable('loca') def normalise_table(font, tag, padding=4): """ Return normalised table data. Keep 'font' instance unmodified. """ assert tag in ('glyf', 'loca', 'head') assert tag in font if tag == 'head': origHeadFlags = font['head'].flags font['head'].flags |= (1 << 11) tableData = font['head'].compile(font) if font.sfntVersion in ("\x00\x01\x00\x00", "true"): assert {'glyf', 'loca', 'head'}.issubset(font.keys()) origIndexFormat = font['head'].indexToLocFormat if hasattr(font['loca'], 'locations'): origLocations = font['loca'].locations[:] else: origLocations = [] glyfTable = ttLib.newTable('glyf') glyfTable.decompile(font.getTableData('glyf'), font) glyfTable.padding = padding if tag == 'glyf': tableData = glyfTable.compile(font) elif tag == 'loca': glyfTable.compile(font) tableData = font['loca'].compile(font) if tag == 'head': glyfTable.compile(font) font['loca'].compile(font) tableData = font['head'].compile(font) font['head'].indexToLocFormat = origIndexFormat font['loca'].set(origLocations) if tag == 'head': font['head'].flags = origHeadFlags return tableData def normalise_font(font, padding=4): """ Return normalised font data. Keep 'font' instance unmodified. """ # drop DSIG but keep a copy DSIG_copy = copy.deepcopy(font['DSIG']) del font['DSIG'] # ovverride TTFont attributes origFlavor = font.flavor origRecalcBBoxes = font.recalcBBoxes origRecalcTimestamp = font.recalcTimestamp origLazy = font.lazy font.flavor = None font.recalcBBoxes = False font.recalcTimestamp = False font.lazy = True # save font to temporary stream infile = BytesIO() font.save(infile) infile.seek(0) # reorder tables alphabetically outfile = BytesIO() reader = ttLib.sfnt.SFNTReader(infile) writer = ttLib.sfnt.SFNTWriter( outfile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) for tag in sorted(reader.keys()): if tag in woff2TransformedTableTags + ('head',): writer[tag] = normalise_table(font, tag, padding) else: writer[tag] = reader[tag] writer.close() # restore font attributes font['DSIG'] = DSIG_copy font.flavor = origFlavor font.recalcBBoxes = origRecalcBBoxes font.recalcTimestamp = origRecalcTimestamp font.lazy = origLazy return outfile.getvalue() class WOFF2DirectoryEntryTest(unittest.TestCase): def setUp(self): self.entry = WOFF2DirectoryEntry() def test_not_enough_data_table_flags(self): with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'flags'"): self.entry.fromString(b"") def test_not_enough_data_table_tag(self): incompleteData = bytearray([0x3F, 0, 0, 0]) with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'tag'"): self.entry.fromString(bytes(incompleteData)) def test_table_reserved_flags(self): with self.assertRaisesRegex(ttLib.TTLibError, "bits 6-7 are reserved"): self.entry.fromString(bytechr(0xC0)) def test_loca_zero_transformLength(self): data = bytechr(getKnownTagIndex('loca')) # flags data += packBase128(random.randint(1, 100)) # origLength data += packBase128(1) # non-zero transformLength with self.assertRaisesRegex( ttLib.TTLibError, "transformLength of the 'loca' table must be 0"): self.entry.fromString(data) def test_fromFile(self): unknownTag = Tag('ZZZZ') data = bytechr(getKnownTagIndex(unknownTag)) data += unknownTag.tobytes() data += packBase128(random.randint(1, 100)) expectedPos = len(data) f = BytesIO(data + b'\0'*100) self.entry.fromFile(f) self.assertEqual(f.tell(), expectedPos) def test_transformed_toString(self): self.entry.tag = Tag('glyf') self.entry.flags = getKnownTagIndex(self.entry.tag) self.entry.origLength = random.randint(101, 200) self.entry.length = random.randint(1, 100) expectedSize = (woff2FlagsSize + base128Size(self.entry.origLength) + base128Size(self.entry.length)) data = self.entry.toString() self.assertEqual(len(data), expectedSize) def test_known_toString(self): self.entry.tag = Tag('head') self.entry.flags = getKnownTagIndex(self.entry.tag) self.entry.origLength = 54 expectedSize = (woff2FlagsSize + base128Size(self.entry.origLength)) data = self.entry.toString() self.assertEqual(len(data), expectedSize) def test_unknown_toString(self): self.entry.tag = Tag('ZZZZ') self.entry.flags = woff2UnknownTagIndex self.entry.origLength = random.randint(1, 100) expectedSize = (woff2FlagsSize + woff2UnknownTagSize + base128Size(self.entry.origLength)) data = self.entry.toString() self.assertEqual(len(data), expectedSize) class DummyReader(WOFF2Reader): def __init__(self, file, checkChecksums=1, fontNumber=-1): self.file = file for attr in ('majorVersion', 'minorVersion', 'metaOffset', 'metaLength', 'metaOrigLength', 'privLength', 'privOffset'): setattr(self, attr, 0) class WOFF2FlavorDataTest(unittest.TestCase): @classmethod def setUpClass(cls): assert os.path.exists(METADATA) with open(METADATA, 'rb') as f: cls.xml_metadata = f.read() cls.compressed_metadata = brotli.compress(cls.xml_metadata, mode=brotli.MODE_TEXT) # make random byte strings; font data must be 4-byte aligned cls.fontdata = bytes(bytearray(random.sample(range(0, 256), 80))) cls.privData = bytes(bytearray(random.sample(range(0, 256), 20))) def setUp(self): self.file = BytesIO(self.fontdata) self.file.seek(0, 2) def test_get_metaData_no_privData(self): self.file.write(self.compressed_metadata) reader = DummyReader(self.file) reader.metaOffset = len(self.fontdata) reader.metaLength = len(self.compressed_metadata) reader.metaOrigLength = len(self.xml_metadata) flavorData = WOFF2FlavorData(reader) self.assertEqual(self.xml_metadata, flavorData.metaData) def test_get_privData_no_metaData(self): self.file.write(self.privData) reader = DummyReader(self.file) reader.privOffset = len(self.fontdata) reader.privLength = len(self.privData) flavorData = WOFF2FlavorData(reader) self.assertEqual(self.privData, flavorData.privData) def test_get_metaData_and_privData(self): self.file.write(self.compressed_metadata + self.privData) reader = DummyReader(self.file) reader.metaOffset = len(self.fontdata) reader.metaLength = len(self.compressed_metadata) reader.metaOrigLength = len(self.xml_metadata) reader.privOffset = reader.metaOffset + reader.metaLength reader.privLength = len(self.privData) flavorData = WOFF2FlavorData(reader) self.assertEqual(self.xml_metadata, flavorData.metaData) self.assertEqual(self.privData, flavorData.privData) def test_get_major_minorVersion(self): reader = DummyReader(self.file) reader.majorVersion = reader.minorVersion = 1 flavorData = WOFF2FlavorData(reader) self.assertEqual(flavorData.majorVersion, 1) self.assertEqual(flavorData.minorVersion, 1) class WOFF2WriterTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False, flavor="woff2") cls.font.importXML(OTX) cls.tags = [t for t in cls.font.keys() if t != 'GlyphOrder'] cls.numTables = len(cls.tags) cls.file = BytesIO(CFF_WOFF2.getvalue()) cls.file.seek(0, 2) cls.length = (cls.file.tell() + 3) & ~3 cls.setUpFlavorData() @classmethod def setUpFlavorData(cls): assert os.path.exists(METADATA) with open(METADATA, 'rb') as f: cls.xml_metadata = f.read() cls.compressed_metadata = brotli.compress(cls.xml_metadata, mode=brotli.MODE_TEXT) cls.privData = bytes(bytearray(random.sample(range(0, 256), 20))) def setUp(self): self.file.seek(0) self.writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion) def test_DSIG_dropped(self): self.writer['DSIG'] = b"\0" self.assertEqual(len(self.writer.tables), 0) self.assertEqual(self.writer.numTables, self.numTables-1) def test_no_rewrite_table(self): self.writer['ZZZZ'] = b"\0" with self.assertRaisesRegex(ttLib.TTLibError, "cannot rewrite"): self.writer['ZZZZ'] = b"\0" def test_num_tables(self): self.writer['ABCD'] = b"\0" with self.assertRaisesRegex(ttLib.TTLibError, "wrong number of tables"): self.writer.close() def test_required_tables(self): font = ttLib.TTFont(flavor="woff2") with self.assertRaisesRegex(ttLib.TTLibError, "missing required table"): font.save(BytesIO()) def test_head_transform_flag(self): headData = self.font.getTableData('head') origFlags = byteord(headData[16]) woff2font = ttLib.TTFont(self.file) newHeadData = woff2font.getTableData('head') modifiedFlags = byteord(newHeadData[16]) self.assertNotEqual(origFlags, modifiedFlags) restoredFlags = modifiedFlags & ~0x08 # turn off bit 11 self.assertEqual(origFlags, restoredFlags) def test_tables_sorted_alphabetically(self): expected = sorted([t for t in self.tags if t != 'DSIG']) woff2font = ttLib.TTFont(self.file) self.assertEqual(expected, list(woff2font.reader.keys())) def test_checksums(self): normFile = BytesIO(normalise_font(self.font, padding=4)) normFile.seek(0) normFont = ttLib.TTFont(normFile, checkChecksums=2) w2font = ttLib.TTFont(self.file) # force reconstructing glyf table using 4-byte padding w2font.reader.padding = 4 for tag in [t for t in self.tags if t != 'DSIG']: w2data = w2font.reader[tag] normData = normFont.reader[tag] if tag == "head": w2data = w2data[:8] + b'\0\0\0\0' + w2data[12:] normData = normData[:8] + b'\0\0\0\0' + normData[12:] w2CheckSum = ttLib.sfnt.calcChecksum(w2data) normCheckSum = ttLib.sfnt.calcChecksum(normData) self.assertEqual(w2CheckSum, normCheckSum) normCheckSumAdjustment = normFont['head'].checkSumAdjustment self.assertEqual(normCheckSumAdjustment, w2font['head'].checkSumAdjustment) def test_calcSFNTChecksumsLengthsAndOffsets(self): normFont = ttLib.TTFont(BytesIO(normalise_font(self.font, padding=4))) for tag in self.tags: self.writer[tag] = self.font.getTableData(tag) self.writer._normaliseGlyfAndLoca(padding=4) self.writer._setHeadTransformFlag() self.writer.tables = OrderedDict(sorted(self.writer.tables.items())) self.writer._calcSFNTChecksumsLengthsAndOffsets() for tag, entry in normFont.reader.tables.items(): self.assertEqual(entry.offset, self.writer.tables[tag].origOffset) self.assertEqual(entry.length, self.writer.tables[tag].origLength) self.assertEqual(entry.checkSum, self.writer.tables[tag].checkSum) def test_bad_sfntVersion(self): for i in range(self.numTables): self.writer[bytechr(65 + i)*4] = b"\0" self.writer.sfntVersion = 'ZZZZ' with self.assertRaisesRegex(ttLib.TTLibError, "bad sfntVersion"): self.writer.close() def test_calcTotalSize_no_flavorData(self): expected = self.length self.writer.file = BytesIO() for tag in self.tags: self.writer[tag] = self.font.getTableData(tag) self.writer.close() self.assertEqual(expected, self.writer.length) self.assertEqual(expected, self.writer.file.tell()) def test_calcTotalSize_with_metaData(self): expected = self.length + len(self.compressed_metadata) flavorData = self.writer.flavorData = WOFF2FlavorData() flavorData.metaData = self.xml_metadata self.writer.file = BytesIO() for tag in self.tags: self.writer[tag] = self.font.getTableData(tag) self.writer.close() self.assertEqual(expected, self.writer.length) self.assertEqual(expected, self.writer.file.tell()) def test_calcTotalSize_with_privData(self): expected = self.length + len(self.privData) flavorData = self.writer.flavorData = WOFF2FlavorData() flavorData.privData = self.privData self.writer.file = BytesIO() for tag in self.tags: self.writer[tag] = self.font.getTableData(tag) self.writer.close() self.assertEqual(expected, self.writer.length) self.assertEqual(expected, self.writer.file.tell()) def test_calcTotalSize_with_metaData_and_privData(self): metaDataLength = (len(self.compressed_metadata) + 3) & ~3 expected = self.length + metaDataLength + len(self.privData) flavorData = self.writer.flavorData = WOFF2FlavorData() flavorData.metaData = self.xml_metadata flavorData.privData = self.privData self.writer.file = BytesIO() for tag in self.tags: self.writer[tag] = self.font.getTableData(tag) self.writer.close() self.assertEqual(expected, self.writer.length) self.assertEqual(expected, self.writer.file.tell()) def test_getVersion(self): # no version self.assertEqual((0, 0), self.writer._getVersion()) # version from head.fontRevision fontRevision = self.font['head'].fontRevision versionTuple = tuple(int(i) for i in str(fontRevision).split(".")) entry = self.writer.tables['head'] = ttLib.newTable('head') entry.data = self.font.getTableData('head') self.assertEqual(versionTuple, self.writer._getVersion()) # version from writer.flavorData flavorData = self.writer.flavorData = WOFF2FlavorData() flavorData.majorVersion, flavorData.minorVersion = (10, 11) self.assertEqual((10, 11), self.writer._getVersion()) class WOFF2WriterTTFTest(WOFF2WriterTest): @classmethod def setUpClass(cls): cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False, flavor="woff2") cls.font.importXML(TTX) cls.tags = [t for t in cls.font.keys() if t != 'GlyphOrder'] cls.numTables = len(cls.tags) cls.file = BytesIO(TT_WOFF2.getvalue()) cls.file.seek(0, 2) cls.length = (cls.file.tell() + 3) & ~3 cls.setUpFlavorData() def test_normaliseGlyfAndLoca(self): normTables = {} for tag in ('head', 'loca', 'glyf'): normTables[tag] = normalise_table(self.font, tag, padding=4) for tag in self.tags: tableData = self.font.getTableData(tag) self.writer[tag] = tableData if tag in normTables: self.assertNotEqual(tableData, normTables[tag]) self.writer._normaliseGlyfAndLoca(padding=4) self.writer._setHeadTransformFlag() for tag in normTables: self.assertEqual(self.writer.tables[tag].data, normTables[tag]) class WOFF2LocaTableTest(unittest.TestCase): def setUp(self): self.font = font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) font['head'] = ttLib.newTable('head') font['loca'] = WOFF2LocaTable() font['glyf'] = WOFF2GlyfTable() def test_compile_short_loca(self): locaTable = self.font['loca'] locaTable.set(list(range(0, 0x20000, 2))) self.font['glyf'].indexFormat = 0 locaData = locaTable.compile(self.font) self.assertEqual(len(locaData), 0x20000) def test_compile_short_loca_overflow(self): locaTable = self.font['loca'] locaTable.set(list(range(0x20000 + 1))) self.font['glyf'].indexFormat = 0 with self.assertRaisesRegex( ttLib.TTLibError, "indexFormat is 0 but local offsets > 0x20000"): locaTable.compile(self.font) def test_compile_short_loca_not_multiples_of_2(self): locaTable = self.font['loca'] locaTable.set([1, 3, 5, 7]) self.font['glyf'].indexFormat = 0 with self.assertRaisesRegex(ttLib.TTLibError, "offsets not multiples of 2"): locaTable.compile(self.font) def test_compile_long_loca(self): locaTable = self.font['loca'] locaTable.set(list(range(0x20001))) self.font['glyf'].indexFormat = 1 locaData = locaTable.compile(self.font) self.assertEqual(len(locaData), 0x20001 * 4) def test_compile_set_indexToLocFormat_0(self): locaTable = self.font['loca'] # offsets are all multiples of 2 and max length is < 0x10000 locaTable.set(list(range(0, 0x20000, 2))) locaTable.compile(self.font) newIndexFormat = self.font['head'].indexToLocFormat self.assertEqual(0, newIndexFormat) def test_compile_set_indexToLocFormat_1(self): locaTable = self.font['loca'] # offsets are not multiples of 2 locaTable.set(list(range(10))) locaTable.compile(self.font) newIndexFormat = self.font['head'].indexToLocFormat self.assertEqual(1, newIndexFormat) # max length is >= 0x10000 locaTable.set(list(range(0, 0x20000 + 1, 2))) locaTable.compile(self.font) newIndexFormat = self.font['head'].indexToLocFormat self.assertEqual(1, newIndexFormat) class WOFF2GlyfTableTest(unittest.TestCase): @classmethod def setUpClass(cls): font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) font.importXML(TTX) cls.tables = {} cls.transformedTags = ('maxp', 'head', 'loca', 'glyf') for tag in reversed(cls.transformedTags): # compile in inverse order cls.tables[tag] = font.getTableData(tag) infile = BytesIO(TT_WOFF2.getvalue()) reader = WOFF2Reader(infile) cls.transformedGlyfData = reader.tables['glyf'].loadData( reader.transformBuffer) cls.glyphOrder = ['.notdef'] + ["glyph%.5d" % i for i in range(1, font['maxp'].numGlyphs)] def setUp(self): self.font = font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) font.setGlyphOrder(self.glyphOrder) font['head'] = ttLib.newTable('head') font['maxp'] = ttLib.newTable('maxp') font['loca'] = WOFF2LocaTable() font['glyf'] = WOFF2GlyfTable() for tag in self.transformedTags: font[tag].decompile(self.tables[tag], font) def test_reconstruct_glyf_padded_4(self): glyfTable = WOFF2GlyfTable() glyfTable.reconstruct(self.transformedGlyfData, self.font) glyfTable.padding = 4 data = glyfTable.compile(self.font) normGlyfData = normalise_table(self.font, 'glyf', glyfTable.padding) self.assertEqual(normGlyfData, data) def test_reconstruct_glyf_padded_2(self): glyfTable = WOFF2GlyfTable() glyfTable.reconstruct(self.transformedGlyfData, self.font) glyfTable.padding = 2 data = glyfTable.compile(self.font) normGlyfData = normalise_table(self.font, 'glyf', glyfTable.padding) self.assertEqual(normGlyfData, data) def test_reconstruct_glyf_unpadded(self): glyfTable = WOFF2GlyfTable() glyfTable.reconstruct(self.transformedGlyfData, self.font) data = glyfTable.compile(self.font) self.assertEqual(self.tables['glyf'], data) def test_reconstruct_glyf_incorrect_glyphOrder(self): glyfTable = WOFF2GlyfTable() badGlyphOrder = self.font.getGlyphOrder()[:-1] self.font.setGlyphOrder(badGlyphOrder) with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"): glyfTable.reconstruct(self.transformedGlyfData, self.font) def test_reconstruct_glyf_missing_glyphOrder(self): glyfTable = WOFF2GlyfTable() del self.font.glyphOrder numGlyphs = self.font['maxp'].numGlyphs del self.font['maxp'] glyfTable.reconstruct(self.transformedGlyfData, self.font) expected = [".notdef"] expected.extend(["glyph%.5d" % i for i in range(1, numGlyphs)]) self.assertEqual(expected, glyfTable.glyphOrder) def test_reconstruct_loca_padded_4(self): locaTable = self.font['loca'] = WOFF2LocaTable() glyfTable = self.font['glyf'] = WOFF2GlyfTable() glyfTable.reconstruct(self.transformedGlyfData, self.font) glyfTable.padding = 4 glyfTable.compile(self.font) data = locaTable.compile(self.font) normLocaData = normalise_table(self.font, 'loca', glyfTable.padding) self.assertEqual(normLocaData, data) def test_reconstruct_loca_padded_2(self): locaTable = self.font['loca'] = WOFF2LocaTable() glyfTable = self.font['glyf'] = WOFF2GlyfTable() glyfTable.reconstruct(self.transformedGlyfData, self.font) glyfTable.padding = 2 glyfTable.compile(self.font) data = locaTable.compile(self.font) normLocaData = normalise_table(self.font, 'loca', glyfTable.padding) self.assertEqual(normLocaData, data) def test_reconstruct_loca_unpadded(self): locaTable = self.font['loca'] = WOFF2LocaTable() glyfTable = self.font['glyf'] = WOFF2GlyfTable() glyfTable.reconstruct(self.transformedGlyfData, self.font) glyfTable.compile(self.font) data = locaTable.compile(self.font) self.assertEqual(self.tables['loca'], data) def test_reconstruct_glyf_header_not_enough_data(self): with self.assertRaisesRegex(ttLib.TTLibError, "not enough 'glyf' data"): WOFF2GlyfTable().reconstruct(b"", self.font) def test_reconstruct_glyf_table_incorrect_size(self): msg = "incorrect size of transformed 'glyf'" with self.assertRaisesRegex(ttLib.TTLibError, msg): WOFF2GlyfTable().reconstruct(self.transformedGlyfData + b"\x00", self.font) with self.assertRaisesRegex(ttLib.TTLibError, msg): WOFF2GlyfTable().reconstruct(self.transformedGlyfData[:-1], self.font) def test_transform_glyf(self): glyfTable = self.font['glyf'] data = glyfTable.transform(self.font) self.assertEqual(self.transformedGlyfData, data) def test_transform_glyf_incorrect_glyphOrder(self): glyfTable = self.font['glyf'] badGlyphOrder = self.font.getGlyphOrder()[:-1] del glyfTable.glyphOrder self.font.setGlyphOrder(badGlyphOrder) with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"): glyfTable.transform(self.font) glyfTable.glyphOrder = badGlyphOrder with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"): glyfTable.transform(self.font) def test_transform_glyf_missing_glyphOrder(self): glyfTable = self.font['glyf'] del glyfTable.glyphOrder del self.font.glyphOrder numGlyphs = self.font['maxp'].numGlyphs del self.font['maxp'] glyfTable.transform(self.font) expected = [".notdef"] expected.extend(["glyph%.5d" % i for i in range(1, numGlyphs)]) self.assertEqual(expected, glyfTable.glyphOrder) def test_roundtrip_glyf_reconstruct_and_transform(self): glyfTable = WOFF2GlyfTable() glyfTable.reconstruct(self.transformedGlyfData, self.font) data = glyfTable.transform(self.font) self.assertEqual(self.transformedGlyfData, data) def test_roundtrip_glyf_transform_and_reconstruct(self): glyfTable = self.font['glyf'] transformedData = glyfTable.transform(self.font) newGlyfTable = WOFF2GlyfTable() newGlyfTable.reconstruct(transformedData, self.font) newGlyfTable.padding = 4 reconstructedData = newGlyfTable.compile(self.font) normGlyfData = normalise_table(self.font, 'glyf', newGlyfTable.padding) self.assertEqual(normGlyfData, reconstructedData) if __name__ == "__main__": import sys sys.exit(unittest.main())