12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685 |
- from io import BytesIO
- import sys
- import array
- import struct
- from collections import OrderedDict
- from fontTools.misc import sstruct
- from fontTools.misc.arrayTools import calcIntBounds
- from fontTools.misc.textTools import Tag, bytechr, byteord, bytesjoin, pad
- from fontTools.ttLib import (
- TTFont,
- TTLibError,
- getTableModule,
- getTableClass,
- getSearchRange,
- )
- from fontTools.ttLib.sfnt import (
- SFNTReader,
- SFNTWriter,
- DirectoryEntry,
- WOFFFlavorData,
- sfntDirectoryFormat,
- sfntDirectorySize,
- SFNTDirectoryEntry,
- sfntDirectoryEntrySize,
- calcChecksum,
- )
- from fontTools.ttLib.tables import ttProgram, _g_l_y_f
- import logging
- log = logging.getLogger("fontTools.ttLib.woff2")
- haveBrotli = False
- try:
- try:
- import brotlicffi as brotli
- except ImportError:
- import brotli
- haveBrotli = True
- except ImportError:
- pass
- 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)
- class WOFF2Writer(SFNTWriter):
- flavor = "woff2"
- def __init__(
- self,
- file,
- numTables,
- sfntVersion="\000\001\000\000",
- flavor=None,
- flavorData=None,
- ):
- if not haveBrotli:
- log.error(
- "The WOFF2 encoder requires the Brotli Python extension, available at: "
- "https://github.com/google/brotli"
- )
- raise ImportError("No module named brotli")
- self.file = file
- self.numTables = numTables
- self.sfntVersion = Tag(sfntVersion)
- self.flavorData = WOFF2FlavorData(data=flavorData)
- 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
- and "glyf" in self.flavorData.transformedTables
- and "glyf" in self.tables
- ):
- 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
- #
- # 2023: We rely on this in _transformTables where we expect that
- # "loca" comes after "glyf" table.
- 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
- for tag in ("maxp", "head", "loca", "glyf", "fvar"):
- if tag in self.tables:
- 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
- elif tag == "hmtx":
- tableClass = WOFF2HmtxTable
- 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."""
- transformedTables = self.flavorData.transformedTables
- for tag, entry in self.tables.items():
- data = None
- if tag in transformedTables:
- data = self.transformTable(tag)
- if data is not None:
- entry.transformed = True
- if data is None:
- if tag == "glyf":
- # Currently we always sort table tags so
- # 'loca' comes after 'glyf'.
- transformedTables.discard("loca")
- # pass-through the table data without transformation
- data = entry.data
- entry.transformed = False
- 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, or None if some pre-conditions aren't
- met -- in which case, the non-transformed table data will be used.
- """
- 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)
- elif tag == "hmtx":
- if "glyf" not in self.tables:
- return
- for tag in ("maxp", "head", "hhea", "loca", "glyf", "hmtx"):
- self._decompileTable(tag)
- hmtxTable = self.ttFont["hmtx"]
- data = hmtxTable.transform(self.ttFont) # can be None
- else:
- raise TTLibError("Transform for table '%s' is unknown" % tag)
- 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
- # -- woff2 directory helpers and cruft
- woff2DirectoryFormat = """
- > # big endian
- signature: 4s # "wOF2"
- sfntVersion: 4s
- length: L # total woff2 file size
- numTables: H # number of tables
- reserved: H # set to 0
- totalSfntSize: L # uncompressed size
- totalCompressedSize: L # compressed size
- majorVersion: H # major version of WOFF file
- minorVersion: H # minor version of WOFF file
- metaOffset: L # offset to metadata block
- metaLength: L # length of compressed metadata
- metaOrigLength: L # length of uncompressed metadata
- privOffset: L # offset to private data block
- privLength: L # length of private data block
- """
- woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat)
- woff2KnownTags = (
- "cmap",
- "head",
- "hhea",
- "hmtx",
- "maxp",
- "name",
- "OS/2",
- "post",
- "cvt ",
- "fpgm",
- "glyf",
- "loca",
- "prep",
- "CFF ",
- "VORG",
- "EBDT",
- "EBLC",
- "gasp",
- "hdmx",
- "kern",
- "LTSH",
- "PCLT",
- "VDMX",
- "vhea",
- "vmtx",
- "BASE",
- "GDEF",
- "GPOS",
- "GSUB",
- "EBSC",
- "JSTF",
- "MATH",
- "CBDT",
- "CBLC",
- "COLR",
- "CPAL",
- "SVG ",
- "sbix",
- "acnt",
- "avar",
- "bdat",
- "bloc",
- "bsln",
- "cvar",
- "fdsc",
- "feat",
- "fmtx",
- "fvar",
- "gvar",
- "hsty",
- "just",
- "lcar",
- "mort",
- "morx",
- "opbd",
- "prop",
- "trak",
- "Zapf",
- "Silf",
- "Glat",
- "Gloc",
- "Feat",
- "Sill",
- )
- woff2FlagsFormat = """
- > # big endian
- flags: B # table type and flags
- """
- woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat)
- woff2UnknownTagFormat = """
- > # big endian
- tag: 4s # 4-byte tag (optional)
- """
- woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat)
- woff2UnknownTagIndex = 0x3F
- woff2Base128MaxSize = 5
- woff2DirectoryEntryMaxSize = (
- woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize
- )
- woff2TransformedTableTags = ("glyf", "loca")
- woff2GlyfTableFormat = """
- > # big endian
- version: H # = 0x0000
- optionFlags: H # Bit 0: we have overlapSimpleBitmap[], Bits 1-15: reserved
- numGlyphs: H # Number of glyphs
- indexFormat: H # Offset format for loca table
- nContourStreamSize: L # Size of nContour stream
- nPointsStreamSize: L # Size of nPoints stream
- flagStreamSize: L # Size of flag stream
- glyphStreamSize: L # Size of glyph stream
- compositeStreamSize: L # Size of composite stream
- bboxStreamSize: L # Comnined size of bboxBitmap and bboxStream
- instructionStreamSize: L # Size of instruction stream
- """
- woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat)
- bboxFormat = """
- > # big endian
- xMin: h
- yMin: h
- xMax: h
- yMax: h
- """
- woff2OverlapSimpleBitmapFlag = 0x0001
- def getKnownTagIndex(tag):
- """Return index of 'tag' in woff2KnownTags list. Return 63 if not found."""
- for i in range(len(woff2KnownTags)):
- if tag == woff2KnownTags[i]:
- return i
- return woff2UnknownTagIndex
- class WOFF2DirectoryEntry(DirectoryEntry):
- def fromFile(self, file):
- pos = file.tell()
- data = file.read(woff2DirectoryEntryMaxSize)
- left = self.fromString(data)
- consumed = len(data) - len(left)
- file.seek(pos + consumed)
- def fromString(self, data):
- if len(data) < 1:
- raise TTLibError("can't read table 'flags': not enough data")
- dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self)
- if self.flags & 0x3F == 0x3F:
- # if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value
- if len(data) < woff2UnknownTagSize:
- raise TTLibError("can't read table 'tag': not enough data")
- dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self)
- else:
- # otherwise, tag is derived from a fixed 'Known Tags' table
- self.tag = woff2KnownTags[self.flags & 0x3F]
- self.tag = Tag(self.tag)
- self.origLength, data = unpackBase128(data)
- self.length = self.origLength
- if self.transformed:
- self.length, data = unpackBase128(data)
- if self.tag == "loca" and self.length != 0:
- raise TTLibError("the transformLength of the 'loca' table must be 0")
- # return left over data
- return data
- def toString(self):
- data = bytechr(self.flags)
- if (self.flags & 0x3F) == 0x3F:
- data += struct.pack(">4s", self.tag.tobytes())
- data += packBase128(self.origLength)
- if self.transformed:
- data += packBase128(self.length)
- return data
- @property
- def transformVersion(self):
- """Return bits 6-7 of table entry's flags, which indicate the preprocessing
- transformation version number (between 0 and 3).
- """
- return self.flags >> 6
- @transformVersion.setter
- def transformVersion(self, value):
- assert 0 <= value <= 3
- self.flags |= value << 6
- @property
- def transformed(self):
- """Return True if the table has any transformation, else return False."""
- # For all tables in a font, except for 'glyf' and 'loca', the transformation
- # version 0 indicates the null transform (where the original table data is
- # passed directly to the Brotli compressor). For 'glyf' and 'loca' tables,
- # transformation version 3 indicates the null transform
- if self.tag in {"glyf", "loca"}:
- return self.transformVersion != 3
- else:
- return self.transformVersion != 0
- @transformed.setter
- def transformed(self, booleanValue):
- # here we assume that a non-null transform means version 0 for 'glyf' and
- # 'loca' and 1 for every other table (e.g. hmtx); but that may change as
- # new transformation formats are introduced in the future (if ever).
- if self.tag in {"glyf", "loca"}:
- self.transformVersion = 3 if not booleanValue else 0
- else:
- self.transformVersion = int(booleanValue)
- class WOFF2LocaTable(getTableClass("loca")):
- """Same as parent class. The only difference is that it attempts to preserve
- the 'indexFormat' as encoded in the WOFF2 glyf table.
- """
- def __init__(self, tag=None):
- self.tableTag = Tag(tag or "loca")
- def compile(self, ttFont):
- try:
- max_location = max(self.locations)
- except AttributeError:
- self.set([])
- max_location = 0
- if "glyf" in ttFont and hasattr(ttFont["glyf"], "indexFormat"):
- # copile loca using the indexFormat specified in the WOFF2 glyf table
- indexFormat = ttFont["glyf"].indexFormat
- if indexFormat == 0:
- if max_location >= 0x20000:
- raise TTLibError("indexFormat is 0 but local offsets > 0x20000")
- if not all(l % 2 == 0 for l in self.locations):
- raise TTLibError(
- "indexFormat is 0 but local offsets not multiples of 2"
- )
- locations = array.array("H")
- for i in range(len(self.locations)):
- locations.append(self.locations[i] // 2)
- else:
- locations = array.array("I", self.locations)
- if sys.byteorder != "big":
- locations.byteswap()
- data = locations.tobytes()
- else:
- # use the most compact indexFormat given the current glyph offsets
- data = super(WOFF2LocaTable, self).compile(ttFont)
- return data
- class WOFF2GlyfTable(getTableClass("glyf")):
- """Decoder/Encoder for WOFF2 'glyf' table transform."""
- subStreams = (
- "nContourStream",
- "nPointsStream",
- "flagStream",
- "glyphStream",
- "compositeStream",
- "bboxStream",
- "instructionStream",
- )
- def __init__(self, tag=None):
- self.tableTag = Tag(tag or "glyf")
- def reconstruct(self, data, ttFont):
- """Decompile transformed 'glyf' data."""
- inputDataSize = len(data)
- if inputDataSize < woff2GlyfTableFormatSize:
- raise TTLibError("not enough 'glyf' data")
- dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self)
- offset = woff2GlyfTableFormatSize
- for stream in self.subStreams:
- size = getattr(self, stream + "Size")
- setattr(self, stream, data[:size])
- data = data[size:]
- offset += size
- hasOverlapSimpleBitmap = self.optionFlags & woff2OverlapSimpleBitmapFlag
- self.overlapSimpleBitmap = None
- if hasOverlapSimpleBitmap:
- overlapSimpleBitmapSize = (self.numGlyphs + 7) >> 3
- self.overlapSimpleBitmap = array.array("B", data[:overlapSimpleBitmapSize])
- offset += overlapSimpleBitmapSize
- if offset != inputDataSize:
- raise TTLibError(
- "incorrect size of transformed 'glyf' table: expected %d, received %d bytes"
- % (offset, inputDataSize)
- )
- bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
- bboxBitmap = self.bboxStream[:bboxBitmapSize]
- self.bboxBitmap = array.array("B", bboxBitmap)
- self.bboxStream = self.bboxStream[bboxBitmapSize:]
- self.nContourStream = array.array("h", self.nContourStream)
- if sys.byteorder != "big":
- self.nContourStream.byteswap()
- assert len(self.nContourStream) == self.numGlyphs
- if "head" in ttFont:
- ttFont["head"].indexToLocFormat = self.indexFormat
- try:
- self.glyphOrder = ttFont.getGlyphOrder()
- except:
- self.glyphOrder = None
- if self.glyphOrder is None:
- self.glyphOrder = [".notdef"]
- self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)])
- else:
- if len(self.glyphOrder) != self.numGlyphs:
- raise TTLibError(
- "incorrect glyphOrder: expected %d glyphs, found %d"
- % (len(self.glyphOrder), self.numGlyphs)
- )
- glyphs = self.glyphs = {}
- for glyphID, glyphName in enumerate(self.glyphOrder):
- glyph = self._decodeGlyph(glyphID)
- glyphs[glyphName] = glyph
- def transform(self, ttFont):
- """Return transformed 'glyf' data"""
- self.numGlyphs = len(self.glyphs)
- assert len(self.glyphOrder) == self.numGlyphs
- if "maxp" in ttFont:
- ttFont["maxp"].numGlyphs = self.numGlyphs
- self.indexFormat = ttFont["head"].indexToLocFormat
- for stream in self.subStreams:
- setattr(self, stream, b"")
- bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
- self.bboxBitmap = array.array("B", [0] * bboxBitmapSize)
- self.overlapSimpleBitmap = array.array("B", [0] * ((self.numGlyphs + 7) >> 3))
- for glyphID in range(self.numGlyphs):
- try:
- self._encodeGlyph(glyphID)
- except NotImplementedError:
- return None
- hasOverlapSimpleBitmap = any(self.overlapSimpleBitmap)
- self.bboxStream = self.bboxBitmap.tobytes() + self.bboxStream
- for stream in self.subStreams:
- setattr(self, stream + "Size", len(getattr(self, stream)))
- self.version = 0
- self.optionFlags = 0
- if hasOverlapSimpleBitmap:
- self.optionFlags |= woff2OverlapSimpleBitmapFlag
- data = sstruct.pack(woff2GlyfTableFormat, self)
- data += bytesjoin([getattr(self, s) for s in self.subStreams])
- if hasOverlapSimpleBitmap:
- data += self.overlapSimpleBitmap.tobytes()
- return data
- def _decodeGlyph(self, glyphID):
- glyph = getTableModule("glyf").Glyph()
- glyph.numberOfContours = self.nContourStream[glyphID]
- if glyph.numberOfContours == 0:
- return glyph
- elif glyph.isComposite():
- self._decodeComponents(glyph)
- else:
- self._decodeCoordinates(glyph)
- self._decodeOverlapSimpleFlag(glyph, glyphID)
- self._decodeBBox(glyphID, glyph)
- return glyph
- def _decodeComponents(self, glyph):
- data = self.compositeStream
- glyph.components = []
- more = 1
- haveInstructions = 0
- while more:
- component = getTableModule("glyf").GlyphComponent()
- more, haveInstr, data = component.decompile(data, self)
- haveInstructions = haveInstructions | haveInstr
- glyph.components.append(component)
- self.compositeStream = data
- if haveInstructions:
- self._decodeInstructions(glyph)
- def _decodeCoordinates(self, glyph):
- data = self.nPointsStream
- endPtsOfContours = []
- endPoint = -1
- for i in range(glyph.numberOfContours):
- ptsOfContour, data = unpack255UShort(data)
- endPoint += ptsOfContour
- endPtsOfContours.append(endPoint)
- glyph.endPtsOfContours = endPtsOfContours
- self.nPointsStream = data
- self._decodeTriplets(glyph)
- self._decodeInstructions(glyph)
- def _decodeOverlapSimpleFlag(self, glyph, glyphID):
- if self.overlapSimpleBitmap is None or glyph.numberOfContours <= 0:
- return
- byte = glyphID >> 3
- bit = glyphID & 7
- if self.overlapSimpleBitmap[byte] & (0x80 >> bit):
- glyph.flags[0] |= _g_l_y_f.flagOverlapSimple
- def _decodeInstructions(self, glyph):
- glyphStream = self.glyphStream
- instructionStream = self.instructionStream
- instructionLength, glyphStream = unpack255UShort(glyphStream)
- glyph.program = ttProgram.Program()
- glyph.program.fromBytecode(instructionStream[:instructionLength])
- self.glyphStream = glyphStream
- self.instructionStream = instructionStream[instructionLength:]
- def _decodeBBox(self, glyphID, glyph):
- haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7)))
- if glyph.isComposite() and not haveBBox:
- raise TTLibError("no bbox values for composite glyph %d" % glyphID)
- if haveBBox:
- dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph)
- else:
- glyph.recalcBounds(self)
- def _decodeTriplets(self, glyph):
- def withSign(flag, baseval):
- assert 0 <= baseval and baseval < 65536, "integer overflow"
- return baseval if flag & 1 else -baseval
- nPoints = glyph.endPtsOfContours[-1] + 1
- flagSize = nPoints
- if flagSize > len(self.flagStream):
- raise TTLibError("not enough 'flagStream' data")
- flagsData = self.flagStream[:flagSize]
- self.flagStream = self.flagStream[flagSize:]
- flags = array.array("B", flagsData)
- triplets = array.array("B", self.glyphStream)
- nTriplets = len(triplets)
- assert nPoints <= nTriplets
- x = 0
- y = 0
- glyph.coordinates = getTableModule("glyf").GlyphCoordinates.zeros(nPoints)
- glyph.flags = array.array("B")
- tripletIndex = 0
- for i in range(nPoints):
- flag = flags[i]
- onCurve = not bool(flag >> 7)
- flag &= 0x7F
- if flag < 84:
- nBytes = 1
- elif flag < 120:
- nBytes = 2
- elif flag < 124:
- nBytes = 3
- else:
- nBytes = 4
- assert (tripletIndex + nBytes) <= nTriplets
- if flag < 10:
- dx = 0
- dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex])
- elif flag < 20:
- dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex])
- dy = 0
- elif flag < 84:
- b0 = flag - 20
- b1 = triplets[tripletIndex]
- dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4))
- dy = withSign(flag >> 1, 1 + ((b0 & 0x0C) << 2) + (b1 & 0x0F))
- elif flag < 120:
- b0 = flag - 84
- dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex])
- dy = withSign(
- flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1]
- )
- elif flag < 124:
- b2 = triplets[tripletIndex + 1]
- dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4))
- dy = withSign(
- flag >> 1, ((b2 & 0x0F) << 8) + triplets[tripletIndex + 2]
- )
- else:
- dx = withSign(
- flag, (triplets[tripletIndex] << 8) + triplets[tripletIndex + 1]
- )
- dy = withSign(
- flag >> 1,
- (triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3],
- )
- tripletIndex += nBytes
- x += dx
- y += dy
- glyph.coordinates[i] = (x, y)
- glyph.flags.append(int(onCurve))
- bytesConsumed = tripletIndex
- self.glyphStream = self.glyphStream[bytesConsumed:]
- def _encodeGlyph(self, glyphID):
- glyphName = self.getGlyphName(glyphID)
- glyph = self[glyphName]
- self.nContourStream += struct.pack(">h", glyph.numberOfContours)
- if glyph.numberOfContours == 0:
- return
- elif glyph.isComposite():
- self._encodeComponents(glyph)
- elif glyph.isVarComposite():
- raise NotImplementedError
- else:
- self._encodeCoordinates(glyph)
- self._encodeOverlapSimpleFlag(glyph, glyphID)
- self._encodeBBox(glyphID, glyph)
- def _encodeComponents(self, glyph):
- lastcomponent = len(glyph.components) - 1
- more = 1
- haveInstructions = 0
- for i in range(len(glyph.components)):
- if i == lastcomponent:
- haveInstructions = hasattr(glyph, "program")
- more = 0
- component = glyph.components[i]
- self.compositeStream += component.compile(more, haveInstructions, self)
- if haveInstructions:
- self._encodeInstructions(glyph)
- def _encodeCoordinates(self, glyph):
- lastEndPoint = -1
- if _g_l_y_f.flagCubic in glyph.flags:
- raise NotImplementedError
- for endPoint in glyph.endPtsOfContours:
- ptsOfContour = endPoint - lastEndPoint
- self.nPointsStream += pack255UShort(ptsOfContour)
- lastEndPoint = endPoint
- self._encodeTriplets(glyph)
- self._encodeInstructions(glyph)
- def _encodeOverlapSimpleFlag(self, glyph, glyphID):
- if glyph.numberOfContours <= 0:
- return
- if glyph.flags[0] & _g_l_y_f.flagOverlapSimple:
- byte = glyphID >> 3
- bit = glyphID & 7
- self.overlapSimpleBitmap[byte] |= 0x80 >> bit
- def _encodeInstructions(self, glyph):
- instructions = glyph.program.getBytecode()
- self.glyphStream += pack255UShort(len(instructions))
- self.instructionStream += instructions
- def _encodeBBox(self, glyphID, glyph):
- assert glyph.numberOfContours != 0, "empty glyph has no bbox"
- if not glyph.isComposite():
- # for simple glyphs, compare the encoded bounding box info with the calculated
- # values, and if they match omit the bounding box info
- currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax
- calculatedBBox = calcIntBounds(glyph.coordinates)
- if currentBBox == calculatedBBox:
- return
- self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7)
- self.bboxStream += sstruct.pack(bboxFormat, glyph)
- def _encodeTriplets(self, glyph):
- assert len(glyph.coordinates) == len(glyph.flags)
- coordinates = glyph.coordinates.copy()
- coordinates.absoluteToRelative()
- flags = array.array("B")
- triplets = array.array("B")
- for i in range(len(coordinates)):
- onCurve = glyph.flags[i] & _g_l_y_f.flagOnCurve
- x, y = coordinates[i]
- absX = abs(x)
- absY = abs(y)
- onCurveBit = 0 if onCurve else 128
- xSignBit = 0 if (x < 0) else 1
- ySignBit = 0 if (y < 0) else 1
- xySignBits = xSignBit + 2 * ySignBit
- if x == 0 and absY < 1280:
- flags.append(onCurveBit + ((absY & 0xF00) >> 7) + ySignBit)
- triplets.append(absY & 0xFF)
- elif y == 0 and absX < 1280:
- flags.append(onCurveBit + 10 + ((absX & 0xF00) >> 7) + xSignBit)
- triplets.append(absX & 0xFF)
- elif absX < 65 and absY < 65:
- flags.append(
- onCurveBit
- + 20
- + ((absX - 1) & 0x30)
- + (((absY - 1) & 0x30) >> 2)
- + xySignBits
- )
- triplets.append((((absX - 1) & 0xF) << 4) | ((absY - 1) & 0xF))
- elif absX < 769 and absY < 769:
- flags.append(
- onCurveBit
- + 84
- + 12 * (((absX - 1) & 0x300) >> 8)
- + (((absY - 1) & 0x300) >> 6)
- + xySignBits
- )
- triplets.append((absX - 1) & 0xFF)
- triplets.append((absY - 1) & 0xFF)
- elif absX < 4096 and absY < 4096:
- flags.append(onCurveBit + 120 + xySignBits)
- triplets.append(absX >> 4)
- triplets.append(((absX & 0xF) << 4) | (absY >> 8))
- triplets.append(absY & 0xFF)
- else:
- flags.append(onCurveBit + 124 + xySignBits)
- triplets.append(absX >> 8)
- triplets.append(absX & 0xFF)
- triplets.append(absY >> 8)
- triplets.append(absY & 0xFF)
- self.flagStream += flags.tobytes()
- self.glyphStream += triplets.tobytes()
- class WOFF2HmtxTable(getTableClass("hmtx")):
- def __init__(self, tag=None):
- self.tableTag = Tag(tag or "hmtx")
- def reconstruct(self, data, ttFont):
- (flags,) = struct.unpack(">B", data[:1])
- data = data[1:]
- if flags & 0b11111100 != 0:
- raise TTLibError("Bits 2-7 of '%s' flags are reserved" % self.tableTag)
- # When bit 0 is _not_ set, the lsb[] array is present
- hasLsbArray = flags & 1 == 0
- # When bit 1 is _not_ set, the leftSideBearing[] array is present
- hasLeftSideBearingArray = flags & 2 == 0
- if hasLsbArray and hasLeftSideBearingArray:
- raise TTLibError(
- "either bits 0 or 1 (or both) must set in transformed '%s' flags"
- % self.tableTag
- )
- glyfTable = ttFont["glyf"]
- headerTable = ttFont["hhea"]
- glyphOrder = glyfTable.glyphOrder
- numGlyphs = len(glyphOrder)
- numberOfHMetrics = min(int(headerTable.numberOfHMetrics), numGlyphs)
- assert len(data) >= 2 * numberOfHMetrics
- advanceWidthArray = array.array("H", data[: 2 * numberOfHMetrics])
- if sys.byteorder != "big":
- advanceWidthArray.byteswap()
- data = data[2 * numberOfHMetrics :]
- if hasLsbArray:
- assert len(data) >= 2 * numberOfHMetrics
- lsbArray = array.array("h", data[: 2 * numberOfHMetrics])
- if sys.byteorder != "big":
- lsbArray.byteswap()
- data = data[2 * numberOfHMetrics :]
- else:
- # compute (proportional) glyphs' lsb from their xMin
- lsbArray = array.array("h")
- for i, glyphName in enumerate(glyphOrder):
- if i >= numberOfHMetrics:
- break
- glyph = glyfTable[glyphName]
- xMin = getattr(glyph, "xMin", 0)
- lsbArray.append(xMin)
- numberOfSideBearings = numGlyphs - numberOfHMetrics
- if hasLeftSideBearingArray:
- assert len(data) >= 2 * numberOfSideBearings
- leftSideBearingArray = array.array("h", data[: 2 * numberOfSideBearings])
- if sys.byteorder != "big":
- leftSideBearingArray.byteswap()
- data = data[2 * numberOfSideBearings :]
- else:
- # compute (monospaced) glyphs' leftSideBearing from their xMin
- leftSideBearingArray = array.array("h")
- for i, glyphName in enumerate(glyphOrder):
- if i < numberOfHMetrics:
- continue
- glyph = glyfTable[glyphName]
- xMin = getattr(glyph, "xMin", 0)
- leftSideBearingArray.append(xMin)
- if data:
- raise TTLibError("too much '%s' table data" % self.tableTag)
- self.metrics = {}
- for i in range(numberOfHMetrics):
- glyphName = glyphOrder[i]
- advanceWidth, lsb = advanceWidthArray[i], lsbArray[i]
- self.metrics[glyphName] = (advanceWidth, lsb)
- lastAdvance = advanceWidthArray[-1]
- for i in range(numberOfSideBearings):
- glyphName = glyphOrder[i + numberOfHMetrics]
- self.metrics[glyphName] = (lastAdvance, leftSideBearingArray[i])
- def transform(self, ttFont):
- glyphOrder = ttFont.getGlyphOrder()
- glyf = ttFont["glyf"]
- hhea = ttFont["hhea"]
- numberOfHMetrics = hhea.numberOfHMetrics
- # check if any of the proportional glyphs has left sidebearings that
- # differ from their xMin bounding box values.
- hasLsbArray = False
- for i in range(numberOfHMetrics):
- glyphName = glyphOrder[i]
- lsb = self.metrics[glyphName][1]
- if lsb != getattr(glyf[glyphName], "xMin", 0):
- hasLsbArray = True
- break
- # do the same for the monospaced glyphs (if any) at the end of hmtx table
- hasLeftSideBearingArray = False
- for i in range(numberOfHMetrics, len(glyphOrder)):
- glyphName = glyphOrder[i]
- lsb = self.metrics[glyphName][1]
- if lsb != getattr(glyf[glyphName], "xMin", 0):
- hasLeftSideBearingArray = True
- break
- # if we need to encode both sidebearings arrays, then no transformation is
- # applicable, and we must use the untransformed hmtx data
- if hasLsbArray and hasLeftSideBearingArray:
- return
- # set bit 0 and 1 when the respective arrays are _not_ present
- flags = 0
- if not hasLsbArray:
- flags |= 1 << 0
- if not hasLeftSideBearingArray:
- flags |= 1 << 1
- data = struct.pack(">B", flags)
- advanceWidthArray = array.array(
- "H",
- [
- self.metrics[glyphName][0]
- for i, glyphName in enumerate(glyphOrder)
- if i < numberOfHMetrics
- ],
- )
- if sys.byteorder != "big":
- advanceWidthArray.byteswap()
- data += advanceWidthArray.tobytes()
- if hasLsbArray:
- lsbArray = array.array(
- "h",
- [
- self.metrics[glyphName][1]
- for i, glyphName in enumerate(glyphOrder)
- if i < numberOfHMetrics
- ],
- )
- if sys.byteorder != "big":
- lsbArray.byteswap()
- data += lsbArray.tobytes()
- if hasLeftSideBearingArray:
- leftSideBearingArray = array.array(
- "h",
- [
- self.metrics[glyphOrder[i]][1]
- for i in range(numberOfHMetrics, len(glyphOrder))
- ],
- )
- if sys.byteorder != "big":
- leftSideBearingArray.byteswap()
- data += leftSideBearingArray.tobytes()
- return data
- class WOFF2FlavorData(WOFFFlavorData):
- Flavor = "woff2"
- def __init__(self, reader=None, data=None, transformedTables=None):
- """Data class that holds the WOFF2 header major/minor version, any
- metadata or private data (as bytes strings), and the set of
- table tags that have transformations applied (if reader is not None),
- or will have once the WOFF2 font is compiled.
- Args:
- reader: an SFNTReader (or subclass) object to read flavor data from.
- data: another WOFFFlavorData object to initialise data from.
- transformedTables: set of strings containing table tags to be transformed.
- Raises:
- ImportError if the brotli module is not installed.
- NOTE: The 'reader' argument, on the one hand, and the 'data' and
- 'transformedTables' arguments, on the other hand, are mutually exclusive.
- """
- if not haveBrotli:
- raise ImportError("No module named brotli")
- if reader is not None:
- if data is not None:
- raise TypeError("'reader' and 'data' arguments are mutually exclusive")
- if transformedTables is not None:
- raise TypeError(
- "'reader' and 'transformedTables' arguments are mutually exclusive"
- )
- if transformedTables is not None and (
- "glyf" in transformedTables
- and "loca" not in transformedTables
- or "loca" in transformedTables
- and "glyf" not in transformedTables
- ):
- raise ValueError("'glyf' and 'loca' must be transformed (or not) together")
- super(WOFF2FlavorData, self).__init__(reader=reader)
- if reader:
- transformedTables = [
- tag for tag, entry in reader.tables.items() if entry.transformed
- ]
- elif data:
- self.majorVersion = data.majorVersion
- self.majorVersion = data.minorVersion
- self.metaData = data.metaData
- self.privData = data.privData
- if transformedTables is None and hasattr(data, "transformedTables"):
- transformedTables = data.transformedTables
- if transformedTables is None:
- transformedTables = woff2TransformedTableTags
- self.transformedTables = set(transformedTables)
- def _decompress(self, rawData):
- return brotli.decompress(rawData)
- def unpackBase128(data):
- r"""Read one to five bytes from UIntBase128-encoded input string, and return
- a tuple containing the decoded integer plus any leftover data.
- >>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00")
- True
- >>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295
- True
- >>> unpackBase128(b'\x80\x80\x3f') # doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- TTLibError: UIntBase128 value must not start with leading zeros
- >>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0] # doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- TTLibError: UIntBase128-encoded sequence is longer than 5 bytes
- >>> unpackBase128(b'\x90\x80\x80\x80\x00')[0] # doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- TTLibError: UIntBase128 value exceeds 2**32-1
- """
- if len(data) == 0:
- raise TTLibError("not enough data to unpack UIntBase128")
- result = 0
- if byteord(data[0]) == 0x80:
- # font must be rejected if UIntBase128 value starts with 0x80
- raise TTLibError("UIntBase128 value must not start with leading zeros")
- for i in range(woff2Base128MaxSize):
- if len(data) == 0:
- raise TTLibError("not enough data to unpack UIntBase128")
- code = byteord(data[0])
- data = data[1:]
- # if any of the top seven bits are set then we're about to overflow
- if result & 0xFE000000:
- raise TTLibError("UIntBase128 value exceeds 2**32-1")
- # set current value = old value times 128 bitwise-or (byte bitwise-and 127)
- result = (result << 7) | (code & 0x7F)
- # repeat until the most significant bit of byte is false
- if (code & 0x80) == 0:
- # return result plus left over data
- return result, data
- # make sure not to exceed the size bound
- raise TTLibError("UIntBase128-encoded sequence is longer than 5 bytes")
- def base128Size(n):
- """Return the length in bytes of a UIntBase128-encoded sequence with value n.
- >>> base128Size(0)
- 1
- >>> base128Size(24567)
- 3
- >>> base128Size(2**32-1)
- 5
- """
- assert n >= 0
- size = 1
- while n >= 128:
- size += 1
- n >>= 7
- return size
- def packBase128(n):
- r"""Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of
- bytes using UIntBase128 variable-length encoding. Produce the shortest possible
- encoding.
- >>> packBase128(63) == b"\x3f"
- True
- >>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f'
- True
- """
- if n < 0 or n >= 2**32:
- raise TTLibError("UIntBase128 format requires 0 <= integer <= 2**32-1")
- data = b""
- size = base128Size(n)
- for i in range(size):
- b = (n >> (7 * (size - i - 1))) & 0x7F
- if i < size - 1:
- b |= 0x80
- data += struct.pack("B", b)
- return data
- def unpack255UShort(data):
- """Read one to three bytes from 255UInt16-encoded input string, and return a
- tuple containing the decoded integer plus any leftover data.
- >>> unpack255UShort(bytechr(252))[0]
- 252
- Note that some numbers (e.g. 506) can have multiple encodings:
- >>> unpack255UShort(struct.pack("BB", 254, 0))[0]
- 506
- >>> unpack255UShort(struct.pack("BB", 255, 253))[0]
- 506
- >>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0]
- 506
- """
- code = byteord(data[:1])
- data = data[1:]
- if code == 253:
- # read two more bytes as an unsigned short
- if len(data) < 2:
- raise TTLibError("not enough data to unpack 255UInt16")
- (result,) = struct.unpack(">H", data[:2])
- data = data[2:]
- elif code == 254:
- # read another byte, plus 253 * 2
- if len(data) == 0:
- raise TTLibError("not enough data to unpack 255UInt16")
- result = byteord(data[:1])
- result += 506
- data = data[1:]
- elif code == 255:
- # read another byte, plus 253
- if len(data) == 0:
- raise TTLibError("not enough data to unpack 255UInt16")
- result = byteord(data[:1])
- result += 253
- data = data[1:]
- else:
- # leave as is if lower than 253
- result = code
- # return result plus left over data
- return result, data
- def pack255UShort(value):
- r"""Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring
- using 255UInt16 variable-length encoding.
- >>> pack255UShort(252) == b'\xfc'
- True
- >>> pack255UShort(506) == b'\xfe\x00'
- True
- >>> pack255UShort(762) == b'\xfd\x02\xfa'
- True
- """
- if value < 0 or value > 0xFFFF:
- raise TTLibError("255UInt16 format requires 0 <= integer <= 65535")
- if value < 253:
- return struct.pack(">B", value)
- elif value < 506:
- return struct.pack(">BB", 255, value - 253)
- elif value < 762:
- return struct.pack(">BB", 254, value - 506)
- else:
- return struct.pack(">BH", 253, value)
- def compress(input_file, output_file, transform_tables=None):
- """Compress OpenType font to WOFF2.
- Args:
- input_file: a file path, file or file-like object (open in binary mode)
- containing an OpenType font (either CFF- or TrueType-flavored).
- output_file: a file path, file or file-like object where to save the
- compressed WOFF2 font.
- transform_tables: Optional[Iterable[str]]: a set of table tags for which
- to enable preprocessing transformations. By default, only 'glyf'
- and 'loca' tables are transformed. An empty set means disable all
- transformations.
- """
- log.info("Processing %s => %s" % (input_file, output_file))
- font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
- font.flavor = "woff2"
- if transform_tables is not None:
- font.flavorData = WOFF2FlavorData(
- data=font.flavorData, transformedTables=transform_tables
- )
- font.save(output_file, reorderTables=False)
- def decompress(input_file, output_file):
- """Decompress WOFF2 font to OpenType font.
- Args:
- input_file: a file path, file or file-like object (open in binary mode)
- containing a compressed WOFF2 font.
- output_file: a file path, file or file-like object where to save the
- decompressed OpenType font.
- """
- log.info("Processing %s => %s" % (input_file, output_file))
- font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
- font.flavor = None
- font.flavorData = None
- font.save(output_file, reorderTables=True)
- def main(args=None):
- """Compress and decompress WOFF2 fonts"""
- import argparse
- from fontTools import configLogger
- from fontTools.ttx import makeOutputFileName
- class _HelpAction(argparse._HelpAction):
- def __call__(self, parser, namespace, values, option_string=None):
- subparsers_actions = [
- action
- for action in parser._actions
- if isinstance(action, argparse._SubParsersAction)
- ]
- for subparsers_action in subparsers_actions:
- for choice, subparser in subparsers_action.choices.items():
- print(subparser.format_help())
- parser.exit()
- class _NoGlyfTransformAction(argparse.Action):
- def __call__(self, parser, namespace, values, option_string=None):
- namespace.transform_tables.difference_update({"glyf", "loca"})
- class _HmtxTransformAction(argparse.Action):
- def __call__(self, parser, namespace, values, option_string=None):
- namespace.transform_tables.add("hmtx")
- parser = argparse.ArgumentParser(
- prog="fonttools ttLib.woff2", description=main.__doc__, add_help=False
- )
- parser.add_argument(
- "-h", "--help", action=_HelpAction, help="show this help message and exit"
- )
- parser_group = parser.add_subparsers(title="sub-commands")
- parser_compress = parser_group.add_parser(
- "compress", description="Compress a TTF or OTF font to WOFF2"
- )
- parser_decompress = parser_group.add_parser(
- "decompress", description="Decompress a WOFF2 font to OTF"
- )
- for subparser in (parser_compress, parser_decompress):
- group = subparser.add_mutually_exclusive_group(required=False)
- group.add_argument(
- "-v",
- "--verbose",
- action="store_true",
- help="print more messages to console",
- )
- group.add_argument(
- "-q",
- "--quiet",
- action="store_true",
- help="do not print messages to console",
- )
- parser_compress.add_argument(
- "input_file",
- metavar="INPUT",
- help="the input OpenType font (.ttf or .otf)",
- )
- parser_decompress.add_argument(
- "input_file",
- metavar="INPUT",
- help="the input WOFF2 font",
- )
- parser_compress.add_argument(
- "-o",
- "--output-file",
- metavar="OUTPUT",
- help="the output WOFF2 font",
- )
- parser_decompress.add_argument(
- "-o",
- "--output-file",
- metavar="OUTPUT",
- help="the output OpenType font",
- )
- transform_group = parser_compress.add_argument_group()
- transform_group.add_argument(
- "--no-glyf-transform",
- dest="transform_tables",
- nargs=0,
- action=_NoGlyfTransformAction,
- help="Do not transform glyf (and loca) tables",
- )
- transform_group.add_argument(
- "--hmtx-transform",
- dest="transform_tables",
- nargs=0,
- action=_HmtxTransformAction,
- help="Enable optional transformation for 'hmtx' table",
- )
- parser_compress.set_defaults(
- subcommand=compress,
- transform_tables={"glyf", "loca"},
- )
- parser_decompress.set_defaults(subcommand=decompress)
- options = vars(parser.parse_args(args))
- subcommand = options.pop("subcommand", None)
- if not subcommand:
- parser.print_help()
- return
- quiet = options.pop("quiet")
- verbose = options.pop("verbose")
- configLogger(
- level=("ERROR" if quiet else "DEBUG" if verbose else "INFO"),
- )
- if not options["output_file"]:
- if subcommand is compress:
- extension = ".woff2"
- elif subcommand is decompress:
- # choose .ttf/.otf file extension depending on sfntVersion
- with open(options["input_file"], "rb") as f:
- f.seek(4) # skip 'wOF2' signature
- sfntVersion = f.read(4)
- assert len(sfntVersion) == 4, "not enough data"
- extension = ".otf" if sfntVersion == b"OTTO" else ".ttf"
- else:
- raise AssertionError(subcommand)
- options["output_file"] = makeOutputFileName(
- options["input_file"], outputDir=None, extension=extension
- )
- try:
- subcommand(**options)
- except TTLibError as e:
- parser.error(e)
- if __name__ == "__main__":
- sys.exit(main())
|