Пример #1
0
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
Пример #2
0
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
Пример #3
0
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)
Пример #4
0
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__