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