class WOFF2Writer(SFNTWriter): flavor = "woff2" def __init__(self, file, numTables, sfntVersion="\000\001\000\000", flavor=None, flavorData=None): if not haveBrotli: print( 'The WOFF2 encoder requires the Brotli Python extension, available at:\n' 'https://github.com/google/brotli', file=sys.stderr) raise ImportError("No module named brotli") self.file = file self.numTables = numTables self.sfntVersion = Tag(sfntVersion) self.flavorData = flavorData or WOFF2FlavorData() self.directoryFormat = woff2DirectoryFormat self.directorySize = woff2DirectorySize self.DirectoryEntry = WOFF2DirectoryEntry self.signature = Tag("wOF2") self.nextTableOffset = 0 self.transformBuffer = BytesIO() self.tables = OrderedDict() # make empty TTFont to store data while normalising and transforming tables self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) def __setitem__(self, tag, data): """Associate new entry named 'tag' with raw table data.""" if tag in self.tables: raise TTLibError("cannot rewrite '%s' table" % tag) if tag == 'DSIG': # always drop DSIG table, since the encoding process can invalidate it self.numTables -= 1 return entry = self.DirectoryEntry() entry.tag = Tag(tag) entry.flags = getKnownTagIndex(entry.tag) # WOFF2 table data are written to disk only on close(), after all tags # have been specified entry.data = data self.tables[tag] = entry def close(self): """ All tags must have been specified. Now write the table data and directory. """ if len(self.tables) != self.numTables: raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(self.tables))) if self.sfntVersion in ("\x00\x01\x00\x00", "true"): isTrueType = True elif self.sfntVersion == "OTTO": isTrueType = False else: raise TTLibError( "Not a TrueType or OpenType font (bad sfntVersion)") # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned. # However, the reference WOFF2 implementation still fails to reconstruct # 'unpadded' glyf tables, therefore we need to 'normalise' them. # See: # https://github.com/khaledhosny/ots/issues/60 # https://github.com/google/woff2/issues/15 if isTrueType: self._normaliseGlyfAndLoca(padding=4) self._setHeadTransformFlag() # To pass the legacy OpenType Sanitiser currently included in browsers, # we must sort the table directory and data alphabetically by tag. # See: # https://github.com/google/woff2/pull/3 # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html # TODO(user): remove to match spec once browsers are on newer OTS self.tables = OrderedDict(sorted(self.tables.items())) self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() fontData = self._transformTables() compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT) self.totalCompressedSize = len(compressedFont) self.length = self._calcTotalSize() self.majorVersion, self.minorVersion = self._getVersion() self.reserved = 0 directory = self._packTableDirectory() self.file.seek(0) self.file.write(pad(directory + compressedFont, size=4)) self._writeFlavorData() def _normaliseGlyfAndLoca(self, padding=4): """ Recompile glyf and loca tables, aligning glyph offsets to multiples of 'padding' size. Update the head table's 'indexToLocFormat' accordingly while compiling loca. """ if self.sfntVersion == "OTTO": return # make up glyph names required to decompile glyf table self._decompileTable('maxp') numGlyphs = self.ttFont['maxp'].numGlyphs glyphOrder = ['.notdef' ] + ["glyph%.5d" % i for i in range(1, numGlyphs)] self.ttFont.setGlyphOrder(glyphOrder) for tag in ('head', 'loca', 'glyf'): self._decompileTable(tag) self.ttFont['glyf'].padding = padding for tag in ('glyf', 'loca'): self._compileTable(tag) def _setHeadTransformFlag(self): """ Set bit 11 of 'head' table flags to indicate that the font has undergone a lossless modifying transform. Re-compile head table data.""" self._decompileTable('head') self.ttFont['head'].flags |= (1 << 11) self._compileTable('head') def _decompileTable(self, tag): """ Fetch table data, decompile it, and store it inside self.ttFont. """ tag = Tag(tag) if tag not in self.tables: raise TTLibError("missing required table: %s" % tag) if self.ttFont.isLoaded(tag): return data = self.tables[tag].data if tag == 'loca': tableClass = WOFF2LocaTable elif tag == 'glyf': tableClass = WOFF2GlyfTable else: tableClass = getTableClass(tag) table = tableClass(tag) self.ttFont.tables[tag] = table table.decompile(data, self.ttFont) def _compileTable(self, tag): """ Compile table and store it in its 'data' attribute. """ self.tables[tag].data = self.ttFont[tag].compile(self.ttFont) def _calcSFNTChecksumsLengthsAndOffsets(self): """ Compute the 'original' SFNT checksums, lengths and offsets for checksum adjustment calculation. Return the total size of the uncompressed font. """ offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables) for tag, entry in self.tables.items(): data = entry.data entry.origOffset = offset entry.origLength = len(data) if tag == 'head': entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) else: entry.checkSum = calcChecksum(data) offset += (entry.origLength + 3) & ~3 return offset def _transformTables(self): """Return transformed font data.""" for tag, entry in self.tables.items(): if tag in woff2TransformedTableTags: data = self.transformTable(tag) else: data = entry.data entry.offset = self.nextTableOffset entry.saveData(self.transformBuffer, data) self.nextTableOffset += entry.length self.writeMasterChecksum() fontData = self.transformBuffer.getvalue() return fontData def transformTable(self, tag): """Return transformed table data.""" if tag not in woff2TransformedTableTags: raise TTLibError("Transform for table '%s' is unknown" % tag) if tag == "loca": data = b"" elif tag == "glyf": for tag in ('maxp', 'head', 'loca', 'glyf'): self._decompileTable(tag) glyfTable = self.ttFont['glyf'] data = glyfTable.transform(self.ttFont) else: raise NotImplementedError return data def _calcMasterChecksum(self): """Calculate checkSumAdjustment.""" tags = list(self.tables.keys()) checksums = [] for i in range(len(tags)): checksums.append(self.tables[tags[i]].checkSum) # Create a SFNT directory for checksum calculation purposes self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( self.numTables, 16) directory = sstruct.pack(sfntDirectoryFormat, self) tables = sorted(self.tables.items()) for tag, entry in tables: sfntEntry = SFNTDirectoryEntry() sfntEntry.tag = entry.tag sfntEntry.checkSum = entry.checkSum sfntEntry.offset = entry.origOffset sfntEntry.length = entry.origLength directory = directory + sfntEntry.toString() directory_end = sfntDirectorySize + len( self.tables) * sfntDirectoryEntrySize assert directory_end == len(directory) checksums.append(calcChecksum(directory)) checksum = sum(checksums) & 0xffffffff # BiboAfba! checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff return checksumadjustment def writeMasterChecksum(self): """Write checkSumAdjustment to the transformBuffer.""" checksumadjustment = self._calcMasterChecksum() self.transformBuffer.seek(self.tables['head'].offset + 8) self.transformBuffer.write(struct.pack(">L", checksumadjustment)) def _calcTotalSize(self): """Calculate total size of WOFF2 font, including any meta- and/or private data.""" offset = self.directorySize for entry in self.tables.values(): offset += len(entry.toString()) offset += self.totalCompressedSize offset = (offset + 3) & ~3 offset = self._calcFlavorDataOffsetsAndSize(offset) return offset def _calcFlavorDataOffsetsAndSize(self, start): """Calculate offsets and lengths for any meta- and/or private data.""" offset = start data = self.flavorData if data.metaData: self.metaOrigLength = len(data.metaData) self.metaOffset = offset self.compressedMetaData = brotli.compress(data.metaData, mode=brotli.MODE_TEXT) self.metaLength = len(self.compressedMetaData) offset += self.metaLength else: self.metaOffset = self.metaLength = self.metaOrigLength = 0 self.compressedMetaData = b"" if data.privData: # make sure private data is padded to 4-byte boundary offset = (offset + 3) & ~3 self.privOffset = offset self.privLength = len(data.privData) offset += self.privLength else: self.privOffset = self.privLength = 0 return offset def _getVersion(self): """Return the WOFF2 font's (majorVersion, minorVersion) tuple.""" data = self.flavorData if data.majorVersion is not None and data.minorVersion is not None: return data.majorVersion, data.minorVersion else: # if None, return 'fontRevision' from 'head' table if 'head' in self.tables: return struct.unpack(">HH", self.tables['head'].data[4:8]) else: return 0, 0 def _packTableDirectory(self): """Return WOFF2 table directory data.""" directory = sstruct.pack(self.directoryFormat, self) for entry in self.tables.values(): directory = directory + entry.toString() return directory def _writeFlavorData(self): """Write metadata and/or private data using appropiate padding.""" compressedMetaData = self.compressedMetaData privData = self.flavorData.privData if compressedMetaData and privData: compressedMetaData = pad(compressedMetaData, size=4) if compressedMetaData: self.file.seek(self.metaOffset) assert self.file.tell() == self.metaOffset self.file.write(compressedMetaData) if privData: self.file.seek(self.privOffset) assert self.file.tell() == self.privOffset self.file.write(privData) def reordersTables(self): return True
class WOFF2Writer(SFNTWriter): flavor = "woff2" def __init__(self, file, numTables, sfntVersion="\000\001\000\000", flavor=None, flavorData=None): if not haveBrotli: print('The WOFF2 encoder requires the Brotli Python extension, available at:\n' 'https://github.com/google/brotli', file=sys.stderr) raise ImportError("No module named brotli") self.file = file self.numTables = numTables self.sfntVersion = Tag(sfntVersion) self.flavorData = flavorData or WOFF2FlavorData() self.directoryFormat = woff2DirectoryFormat self.directorySize = woff2DirectorySize self.DirectoryEntry = WOFF2DirectoryEntry self.signature = Tag("wOF2") self.nextTableOffset = 0 self.transformBuffer = BytesIO() self.tables = OrderedDict() # make empty TTFont to store data while normalising and transforming tables self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) def __setitem__(self, tag, data): """Associate new entry named 'tag' with raw table data.""" if tag in self.tables: raise TTLibError("cannot rewrite '%s' table" % tag) if tag == 'DSIG': # always drop DSIG table, since the encoding process can invalidate it self.numTables -= 1 return entry = self.DirectoryEntry() entry.tag = Tag(tag) entry.flags = getKnownTagIndex(entry.tag) # WOFF2 table data are written to disk only on close(), after all tags # have been specified entry.data = data self.tables[tag] = entry def close(self): """ All tags must have been specified. Now write the table data and directory. """ if len(self.tables) != self.numTables: raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(self.tables))) if self.sfntVersion in ("\x00\x01\x00\x00", "true"): isTrueType = True elif self.sfntVersion == "OTTO": isTrueType = False else: raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned. # However, the reference WOFF2 implementation still fails to reconstruct # 'unpadded' glyf tables, therefore we need to 'normalise' them. # See: # https://github.com/khaledhosny/ots/issues/60 # https://github.com/google/woff2/issues/15 if isTrueType: self._normaliseGlyfAndLoca(padding=4) self._setHeadTransformFlag() # To pass the legacy OpenType Sanitiser currently included in browsers, # we must sort the table directory and data alphabetically by tag. # See: # https://github.com/google/woff2/pull/3 # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html # TODO(user): remove to match spec once browsers are on newer OTS self.tables = OrderedDict(sorted(self.tables.items())) self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() fontData = self._transformTables() compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT) self.totalCompressedSize = len(compressedFont) self.length = self._calcTotalSize() self.majorVersion, self.minorVersion = self._getVersion() self.reserved = 0 directory = self._packTableDirectory() self.file.seek(0) self.file.write(pad(directory + compressedFont, size=4)) self._writeFlavorData() def _normaliseGlyfAndLoca(self, padding=4): """ Recompile glyf and loca tables, aligning glyph offsets to multiples of 'padding' size. Update the head table's 'indexToLocFormat' accordingly while compiling loca. """ if self.sfntVersion == "OTTO": return # make up glyph names required to decompile glyf table self._decompileTable('maxp') numGlyphs = self.ttFont['maxp'].numGlyphs glyphOrder = ['.notdef'] + ["glyph%.5d" % i for i in range(1, numGlyphs)] self.ttFont.setGlyphOrder(glyphOrder) for tag in ('head', 'loca', 'glyf'): self._decompileTable(tag) self.ttFont['glyf'].padding = padding for tag in ('glyf', 'loca'): self._compileTable(tag) def _setHeadTransformFlag(self): """ Set bit 11 of 'head' table flags to indicate that the font has undergone a lossless modifying transform. Re-compile head table data.""" self._decompileTable('head') self.ttFont['head'].flags |= (1 << 11) self._compileTable('head') def _decompileTable(self, tag): """ Fetch table data, decompile it, and store it inside self.ttFont. """ tag = Tag(tag) if tag not in self.tables: raise TTLibError("missing required table: %s" % tag) if self.ttFont.isLoaded(tag): return data = self.tables[tag].data if tag == 'loca': tableClass = WOFF2LocaTable elif tag == 'glyf': tableClass = WOFF2GlyfTable else: tableClass = getTableClass(tag) table = tableClass(tag) self.ttFont.tables[tag] = table table.decompile(data, self.ttFont) def _compileTable(self, tag): """ Compile table and store it in its 'data' attribute. """ self.tables[tag].data = self.ttFont[tag].compile(self.ttFont) def _calcSFNTChecksumsLengthsAndOffsets(self): """ Compute the 'original' SFNT checksums, lengths and offsets for checksum adjustment calculation. Return the total size of the uncompressed font. """ offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables) for tag, entry in self.tables.items(): data = entry.data entry.origOffset = offset entry.origLength = len(data) if tag == 'head': entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) else: entry.checkSum = calcChecksum(data) offset += (entry.origLength + 3) & ~3 return offset def _transformTables(self): """Return transformed font data.""" for tag, entry in self.tables.items(): if tag in woff2TransformedTableTags: data = self.transformTable(tag) else: data = entry.data entry.offset = self.nextTableOffset entry.saveData(self.transformBuffer, data) self.nextTableOffset += entry.length self.writeMasterChecksum() fontData = self.transformBuffer.getvalue() return fontData def transformTable(self, tag): """Return transformed table data.""" if tag not in woff2TransformedTableTags: raise TTLibError("Transform for table '%s' is unknown" % tag) if tag == "loca": data = b"" elif tag == "glyf": for tag in ('maxp', 'head', 'loca', 'glyf'): self._decompileTable(tag) glyfTable = self.ttFont['glyf'] data = glyfTable.transform(self.ttFont) else: raise NotImplementedError return data def _calcMasterChecksum(self): """Calculate checkSumAdjustment.""" tags = list(self.tables.keys()) checksums = [] for i in range(len(tags)): checksums.append(self.tables[tags[i]].checkSum) # Create a SFNT directory for checksum calculation purposes self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16) directory = sstruct.pack(sfntDirectoryFormat, self) tables = sorted(self.tables.items()) for tag, entry in tables: sfntEntry = SFNTDirectoryEntry() sfntEntry.tag = entry.tag sfntEntry.checkSum = entry.checkSum sfntEntry.offset = entry.origOffset sfntEntry.length = entry.origLength directory = directory + sfntEntry.toString() directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize assert directory_end == len(directory) checksums.append(calcChecksum(directory)) checksum = sum(checksums) & 0xffffffff # BiboAfba! checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff return checksumadjustment def writeMasterChecksum(self): """Write checkSumAdjustment to the transformBuffer.""" checksumadjustment = self._calcMasterChecksum() self.transformBuffer.seek(self.tables['head'].offset + 8) self.transformBuffer.write(struct.pack(">L", checksumadjustment)) def _calcTotalSize(self): """Calculate total size of WOFF2 font, including any meta- and/or private data.""" offset = self.directorySize for entry in self.tables.values(): offset += len(entry.toString()) offset += self.totalCompressedSize offset = (offset + 3) & ~3 offset = self._calcFlavorDataOffsetsAndSize(offset) return offset def _calcFlavorDataOffsetsAndSize(self, start): """Calculate offsets and lengths for any meta- and/or private data.""" offset = start data = self.flavorData if data.metaData: self.metaOrigLength = len(data.metaData) self.metaOffset = offset self.compressedMetaData = brotli.compress( data.metaData, mode=brotli.MODE_TEXT) self.metaLength = len(self.compressedMetaData) offset += self.metaLength else: self.metaOffset = self.metaLength = self.metaOrigLength = 0 self.compressedMetaData = b"" if data.privData: # make sure private data is padded to 4-byte boundary offset = (offset + 3) & ~3 self.privOffset = offset self.privLength = len(data.privData) offset += self.privLength else: self.privOffset = self.privLength = 0 return offset def _getVersion(self): """Return the WOFF2 font's (majorVersion, minorVersion) tuple.""" data = self.flavorData if data.majorVersion is not None and data.minorVersion is not None: return data.majorVersion, data.minorVersion else: # if None, return 'fontRevision' from 'head' table if 'head' in self.tables: return struct.unpack(">HH", self.tables['head'].data[4:8]) else: return 0, 0 def _packTableDirectory(self): """Return WOFF2 table directory data.""" directory = sstruct.pack(self.directoryFormat, self) for entry in self.tables.values(): directory = directory + entry.toString() return directory def _writeFlavorData(self): """Write metadata and/or private data using appropiate padding.""" compressedMetaData = self.compressedMetaData privData = self.flavorData.privData if compressedMetaData and privData: compressedMetaData = pad(compressedMetaData, size=4) if compressedMetaData: self.file.seek(self.metaOffset) assert self.file.tell() == self.metaOffset self.file.write(compressedMetaData) if privData: self.file.seek(self.privOffset) assert self.file.tell() == self.privOffset self.file.write(privData) def reordersTables(self): return True
class WOFF2Reader(SFNTReader): flavor = "woff2" def __init__(self, file, checkChecksums=0, fontNumber=-1): if not haveBrotli: log.error( 'The WOFF2 decoder requires the Brotli Python extension, available at: ' 'https://github.com/google/brotli') raise ImportError("No module named brotli") self.file = file signature = Tag(self.file.read(4)) if signature != b"wOF2": raise TTLibError("Not a WOFF2 font (bad signature)") self.file.seek(0) self.DirectoryEntry = WOFF2DirectoryEntry data = self.file.read(woff2DirectorySize) if len(data) != woff2DirectorySize: raise TTLibError('Not a WOFF2 font (not enough data)') sstruct.unpack(woff2DirectoryFormat, data, self) self.tables = OrderedDict() offset = 0 for i in range(self.numTables): entry = self.DirectoryEntry() entry.fromFile(self.file) tag = Tag(entry.tag) self.tables[tag] = entry entry.offset = offset offset += entry.length totalUncompressedSize = offset compressedData = self.file.read(self.totalCompressedSize) decompressedData = brotli.decompress(compressedData) if len(decompressedData) != totalUncompressedSize: raise TTLibError( 'unexpected size for decompressed font data: expected %d, found %d' % (totalUncompressedSize, len(decompressedData))) self.transformBuffer = BytesIO(decompressedData) self.file.seek(0, 2) if self.length != self.file.tell(): raise TTLibError("reported 'length' doesn't match the actual file size") self.flavorData = WOFF2FlavorData(self) # make empty TTFont to store data while reconstructing tables self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) def __getitem__(self, tag): """Fetch the raw table data. Reconstruct transformed tables.""" entry = self.tables[Tag(tag)] if not hasattr(entry, 'data'): if entry.transformed: entry.data = self.reconstructTable(tag) else: entry.data = entry.loadData(self.transformBuffer) return entry.data def reconstructTable(self, tag): """Reconstruct table named 'tag' from transformed data.""" entry = self.tables[Tag(tag)] rawData = entry.loadData(self.transformBuffer) if tag == 'glyf': # no need to pad glyph data when reconstructing padding = self.padding if hasattr(self, 'padding') else None data = self._reconstructGlyf(rawData, padding) elif tag == 'loca': data = self._reconstructLoca() elif tag == 'hmtx': data = self._reconstructHmtx(rawData) else: raise TTLibError("transform for table '%s' is unknown" % tag) return data def _reconstructGlyf(self, data, padding=None): """ Return recostructed glyf table data, and set the corresponding loca's locations. Optionally pad glyph offsets to the specified number of bytes. """ self.ttFont['loca'] = WOFF2LocaTable() glyfTable = self.ttFont['glyf'] = WOFF2GlyfTable() glyfTable.reconstruct(data, self.ttFont) if padding: glyfTable.padding = padding data = glyfTable.compile(self.ttFont) return data def _reconstructLoca(self): """ Return reconstructed loca table data. """ if 'loca' not in self.ttFont: # make sure glyf is reconstructed first self.tables['glyf'].data = self.reconstructTable('glyf') locaTable = self.ttFont['loca'] data = locaTable.compile(self.ttFont) if len(data) != self.tables['loca'].origLength: raise TTLibError( "reconstructed 'loca' table doesn't match original size: " "expected %d, found %d" % (self.tables['loca'].origLength, len(data))) return data def _reconstructHmtx(self, data): """ Return reconstructed hmtx table data. """ # Before reconstructing 'hmtx' table we need to parse other tables: # 'glyf' is required for reconstructing the sidebearings from the glyphs' # bounding box; 'hhea' is needed for the numberOfHMetrics field. if "glyf" in self.flavorData.transformedTables: # transformed 'glyf' table is self-contained, thus 'loca' not needed tableDependencies = ("maxp", "hhea", "glyf") else: # decompiling untransformed 'glyf' requires 'loca', which requires 'head' tableDependencies = ("maxp", "head", "hhea", "loca", "glyf") for tag in tableDependencies: self._decompileTable(tag) hmtxTable = self.ttFont["hmtx"] = WOFF2HmtxTable() hmtxTable.reconstruct(data, self.ttFont) data = hmtxTable.compile(self.ttFont) return data def _decompileTable(self, tag): """Decompile table data and store it inside self.ttFont.""" data = self[tag] if self.ttFont.isLoaded(tag): return self.ttFont[tag] tableClass = getTableClass(tag) table = tableClass(tag) self.ttFont.tables[tag] = table table.decompile(data, self.ttFont)
def test_ensureDecompiled(lazy): # test that no matter the lazy value, ensureDecompiled decompiles all tables font = TTFont() font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx")) # test font has no OTL so we add some, as an example of otData-driven tables addOpenTypeFeaturesFromString( font, """ feature calt { sub period' period' period' space by ellipsis; } calt; feature dist { pos period period -30; } dist; """ ) # also add an additional cmap subtable that will be lazily-loaded cm = CmapSubtable.newSubtable(14) cm.platformID = 0 cm.platEncID = 5 cm.language = 0 cm.cmap = {} cm.uvsDict = {0xFE00: [(0x002e, None)]} font["cmap"].tables.append(cm) # save and reload, potentially lazily buf = io.BytesIO() font.save(buf) buf.seek(0) font = TTFont(buf, lazy=lazy) # check no table is loaded until/unless requested, no matter the laziness for tag in font.keys(): assert not font.isLoaded(tag) if lazy is not False: # additional cmap doesn't get decompiled automatically unless lazy=False; # can't use hasattr or else cmap's maginc __getattr__ kicks in... cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14) assert cm.data is not None assert "uvsDict" not in cm.__dict__ # glyf glyphs are not expanded unless lazy=False assert font["glyf"].glyphs["period"].data is not None assert not hasattr(font["glyf"].glyphs["period"], "coordinates") if lazy is True: # OTL tables hold a 'reader' to lazily load when lazy=True assert "reader" in font["GSUB"].table.LookupList.__dict__ assert "reader" in font["GPOS"].table.LookupList.__dict__ font.ensureDecompiled() # all tables are decompiled now for tag in font.keys(): assert font.isLoaded(tag) # including the additional cmap cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14) assert cm.data is None assert "uvsDict" in cm.__dict__ # expanded glyf glyphs lost the 'data' attribute assert not hasattr(font["glyf"].glyphs["period"], "data") assert hasattr(font["glyf"].glyphs["period"], "coordinates") # and OTL tables have read their 'reader' assert "reader" not in font["GSUB"].table.LookupList.__dict__ assert "Lookup" in font["GSUB"].table.LookupList.__dict__ assert "reader" not in font["GPOS"].table.LookupList.__dict__ assert "Lookup" in font["GPOS"].table.LookupList.__dict__