• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from io import BytesIO
2import sys
3import array
4import struct
5from collections import OrderedDict
6from fontTools.misc import sstruct
7from fontTools.misc.arrayTools import calcIntBounds
8from fontTools.misc.textTools import Tag, bytechr, byteord, bytesjoin, pad
9from fontTools.ttLib import (TTFont, TTLibError, getTableModule, getTableClass,
10	getSearchRange)
11from fontTools.ttLib.sfnt import (SFNTReader, SFNTWriter, DirectoryEntry,
12	WOFFFlavorData, sfntDirectoryFormat, sfntDirectorySize, SFNTDirectoryEntry,
13	sfntDirectoryEntrySize, calcChecksum)
14from fontTools.ttLib.tables import ttProgram, _g_l_y_f
15import logging
16
17
18log = logging.getLogger("fontTools.ttLib.woff2")
19
20haveBrotli = False
21try:
22	try:
23		import brotlicffi as brotli
24	except ImportError:
25		import brotli
26	haveBrotli = True
27except ImportError:
28	pass
29
30
31class WOFF2Reader(SFNTReader):
32
33	flavor = "woff2"
34
35	def __init__(self, file, checkChecksums=0, fontNumber=-1):
36		if not haveBrotli:
37			log.error(
38				'The WOFF2 decoder requires the Brotli Python extension, available at: '
39				'https://github.com/google/brotli')
40			raise ImportError("No module named brotli")
41
42		self.file = file
43
44		signature = Tag(self.file.read(4))
45		if signature != b"wOF2":
46			raise TTLibError("Not a WOFF2 font (bad signature)")
47
48		self.file.seek(0)
49		self.DirectoryEntry = WOFF2DirectoryEntry
50		data = self.file.read(woff2DirectorySize)
51		if len(data) != woff2DirectorySize:
52			raise TTLibError('Not a WOFF2 font (not enough data)')
53		sstruct.unpack(woff2DirectoryFormat, data, self)
54
55		self.tables = OrderedDict()
56		offset = 0
57		for i in range(self.numTables):
58			entry = self.DirectoryEntry()
59			entry.fromFile(self.file)
60			tag = Tag(entry.tag)
61			self.tables[tag] = entry
62			entry.offset = offset
63			offset += entry.length
64
65		totalUncompressedSize = offset
66		compressedData = self.file.read(self.totalCompressedSize)
67		decompressedData = brotli.decompress(compressedData)
68		if len(decompressedData) != totalUncompressedSize:
69			raise TTLibError(
70				'unexpected size for decompressed font data: expected %d, found %d'
71				% (totalUncompressedSize, len(decompressedData)))
72		self.transformBuffer = BytesIO(decompressedData)
73
74		self.file.seek(0, 2)
75		if self.length != self.file.tell():
76			raise TTLibError("reported 'length' doesn't match the actual file size")
77
78		self.flavorData = WOFF2FlavorData(self)
79
80		# make empty TTFont to store data while reconstructing tables
81		self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
82
83	def __getitem__(self, tag):
84		"""Fetch the raw table data. Reconstruct transformed tables."""
85		entry = self.tables[Tag(tag)]
86		if not hasattr(entry, 'data'):
87			if entry.transformed:
88				entry.data = self.reconstructTable(tag)
89			else:
90				entry.data = entry.loadData(self.transformBuffer)
91		return entry.data
92
93	def reconstructTable(self, tag):
94		"""Reconstruct table named 'tag' from transformed data."""
95		entry = self.tables[Tag(tag)]
96		rawData = entry.loadData(self.transformBuffer)
97		if tag == 'glyf':
98			# no need to pad glyph data when reconstructing
99			padding = self.padding if hasattr(self, 'padding') else None
100			data = self._reconstructGlyf(rawData, padding)
101		elif tag == 'loca':
102			data = self._reconstructLoca()
103		elif tag == 'hmtx':
104			data = self._reconstructHmtx(rawData)
105		else:
106			raise TTLibError("transform for table '%s' is unknown" % tag)
107		return data
108
109	def _reconstructGlyf(self, data, padding=None):
110		""" Return recostructed glyf table data, and set the corresponding loca's
111		locations. Optionally pad glyph offsets to the specified number of bytes.
112		"""
113		self.ttFont['loca'] = WOFF2LocaTable()
114		glyfTable = self.ttFont['glyf'] = WOFF2GlyfTable()
115		glyfTable.reconstruct(data, self.ttFont)
116		if padding:
117			glyfTable.padding = padding
118		data = glyfTable.compile(self.ttFont)
119		return data
120
121	def _reconstructLoca(self):
122		""" Return reconstructed loca table data. """
123		if 'loca' not in self.ttFont:
124			# make sure glyf is reconstructed first
125			self.tables['glyf'].data = self.reconstructTable('glyf')
126		locaTable = self.ttFont['loca']
127		data = locaTable.compile(self.ttFont)
128		if len(data) != self.tables['loca'].origLength:
129			raise TTLibError(
130				"reconstructed 'loca' table doesn't match original size: "
131				"expected %d, found %d"
132				% (self.tables['loca'].origLength, len(data)))
133		return data
134
135	def _reconstructHmtx(self, data):
136		""" Return reconstructed hmtx table data. """
137		# Before reconstructing 'hmtx' table we need to parse other tables:
138		# 'glyf' is required for reconstructing the sidebearings from the glyphs'
139		# bounding box; 'hhea' is needed for the numberOfHMetrics field.
140		if "glyf" in self.flavorData.transformedTables:
141			# transformed 'glyf' table is self-contained, thus 'loca' not needed
142			tableDependencies = ("maxp", "hhea", "glyf")
143		else:
144			# decompiling untransformed 'glyf' requires 'loca', which requires 'head'
145			tableDependencies = ("maxp", "head", "hhea", "loca", "glyf")
146		for tag in tableDependencies:
147			self._decompileTable(tag)
148		hmtxTable = self.ttFont["hmtx"] = WOFF2HmtxTable()
149		hmtxTable.reconstruct(data, self.ttFont)
150		data = hmtxTable.compile(self.ttFont)
151		return data
152
153	def _decompileTable(self, tag):
154		"""Decompile table data and store it inside self.ttFont."""
155		data = self[tag]
156		if self.ttFont.isLoaded(tag):
157			return self.ttFont[tag]
158		tableClass = getTableClass(tag)
159		table = tableClass(tag)
160		self.ttFont.tables[tag] = table
161		table.decompile(data, self.ttFont)
162
163
164class WOFF2Writer(SFNTWriter):
165
166	flavor = "woff2"
167
168	def __init__(self, file, numTables, sfntVersion="\000\001\000\000",
169		         flavor=None, flavorData=None):
170		if not haveBrotli:
171			log.error(
172				'The WOFF2 encoder requires the Brotli Python extension, available at: '
173				'https://github.com/google/brotli')
174			raise ImportError("No module named brotli")
175
176		self.file = file
177		self.numTables = numTables
178		self.sfntVersion = Tag(sfntVersion)
179		self.flavorData = WOFF2FlavorData(data=flavorData)
180
181		self.directoryFormat = woff2DirectoryFormat
182		self.directorySize = woff2DirectorySize
183		self.DirectoryEntry = WOFF2DirectoryEntry
184
185		self.signature = Tag("wOF2")
186
187		self.nextTableOffset = 0
188		self.transformBuffer = BytesIO()
189
190		self.tables = OrderedDict()
191
192		# make empty TTFont to store data while normalising and transforming tables
193		self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
194
195	def __setitem__(self, tag, data):
196		"""Associate new entry named 'tag' with raw table data."""
197		if tag in self.tables:
198			raise TTLibError("cannot rewrite '%s' table" % tag)
199		if tag == 'DSIG':
200			# always drop DSIG table, since the encoding process can invalidate it
201			self.numTables -= 1
202			return
203
204		entry = self.DirectoryEntry()
205		entry.tag = Tag(tag)
206		entry.flags = getKnownTagIndex(entry.tag)
207		# WOFF2 table data are written to disk only on close(), after all tags
208		# have been specified
209		entry.data = data
210
211		self.tables[tag] = entry
212
213	def close(self):
214		""" All tags must have been specified. Now write the table data and directory.
215		"""
216		if len(self.tables) != self.numTables:
217			raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(self.tables)))
218
219		if self.sfntVersion in ("\x00\x01\x00\x00", "true"):
220			isTrueType = True
221		elif self.sfntVersion == "OTTO":
222			isTrueType = False
223		else:
224			raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
225
226		# The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned.
227		# However, the reference WOFF2 implementation still fails to reconstruct
228		# 'unpadded' glyf tables, therefore we need to 'normalise' them.
229		# See:
230		# https://github.com/khaledhosny/ots/issues/60
231		# https://github.com/google/woff2/issues/15
232		if (
233			isTrueType
234			and "glyf" in self.flavorData.transformedTables
235			and "glyf" in self.tables
236		):
237			self._normaliseGlyfAndLoca(padding=4)
238		self._setHeadTransformFlag()
239
240		# To pass the legacy OpenType Sanitiser currently included in browsers,
241		# we must sort the table directory and data alphabetically by tag.
242		# See:
243		# https://github.com/google/woff2/pull/3
244		# https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html
245		# TODO(user): remove to match spec once browsers are on newer OTS
246		self.tables = OrderedDict(sorted(self.tables.items()))
247
248		self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets()
249
250		fontData = self._transformTables()
251		compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT)
252
253		self.totalCompressedSize = len(compressedFont)
254		self.length = self._calcTotalSize()
255		self.majorVersion, self.minorVersion = self._getVersion()
256		self.reserved = 0
257
258		directory = self._packTableDirectory()
259		self.file.seek(0)
260		self.file.write(pad(directory + compressedFont, size=4))
261		self._writeFlavorData()
262
263	def _normaliseGlyfAndLoca(self, padding=4):
264		""" Recompile glyf and loca tables, aligning glyph offsets to multiples of
265		'padding' size. Update the head table's 'indexToLocFormat' accordingly while
266		compiling loca.
267		"""
268		if self.sfntVersion == "OTTO":
269			return
270
271		for tag in ('maxp', 'head', 'loca', 'glyf'):
272			self._decompileTable(tag)
273		self.ttFont['glyf'].padding = padding
274		for tag in ('glyf', 'loca'):
275			self._compileTable(tag)
276
277	def _setHeadTransformFlag(self):
278		""" Set bit 11 of 'head' table flags to indicate that the font has undergone
279		a lossless modifying transform. Re-compile head table data."""
280		self._decompileTable('head')
281		self.ttFont['head'].flags |= (1 << 11)
282		self._compileTable('head')
283
284	def _decompileTable(self, tag):
285		""" Fetch table data, decompile it, and store it inside self.ttFont. """
286		tag = Tag(tag)
287		if tag not in self.tables:
288			raise TTLibError("missing required table: %s" % tag)
289		if self.ttFont.isLoaded(tag):
290			return
291		data = self.tables[tag].data
292		if tag == 'loca':
293			tableClass = WOFF2LocaTable
294		elif tag == 'glyf':
295			tableClass = WOFF2GlyfTable
296		elif tag == 'hmtx':
297			tableClass = WOFF2HmtxTable
298		else:
299			tableClass = getTableClass(tag)
300		table = tableClass(tag)
301		self.ttFont.tables[tag] = table
302		table.decompile(data, self.ttFont)
303
304	def _compileTable(self, tag):
305		""" Compile table and store it in its 'data' attribute. """
306		self.tables[tag].data = self.ttFont[tag].compile(self.ttFont)
307
308	def _calcSFNTChecksumsLengthsAndOffsets(self):
309		""" Compute the 'original' SFNT checksums, lengths and offsets for checksum
310		adjustment calculation. Return the total size of the uncompressed font.
311		"""
312		offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables)
313		for tag, entry in self.tables.items():
314			data = entry.data
315			entry.origOffset = offset
316			entry.origLength = len(data)
317			if tag == 'head':
318				entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:])
319			else:
320				entry.checkSum = calcChecksum(data)
321			offset += (entry.origLength + 3) & ~3
322		return offset
323
324	def _transformTables(self):
325		"""Return transformed font data."""
326		transformedTables = self.flavorData.transformedTables
327		for tag, entry in self.tables.items():
328			data = None
329			if tag in transformedTables:
330				data = self.transformTable(tag)
331				if data is not None:
332					entry.transformed = True
333			if data is None:
334				# pass-through the table data without transformation
335				data = entry.data
336				entry.transformed = False
337			entry.offset = self.nextTableOffset
338			entry.saveData(self.transformBuffer, data)
339			self.nextTableOffset += entry.length
340		self.writeMasterChecksum()
341		fontData = self.transformBuffer.getvalue()
342		return fontData
343
344	def transformTable(self, tag):
345		"""Return transformed table data, or None if some pre-conditions aren't
346		met -- in which case, the non-transformed table data will be used.
347		"""
348		if tag == "loca":
349			data = b""
350		elif tag == "glyf":
351			for tag in ('maxp', 'head', 'loca', 'glyf'):
352				self._decompileTable(tag)
353			glyfTable = self.ttFont['glyf']
354			data = glyfTable.transform(self.ttFont)
355		elif tag == "hmtx":
356			if "glyf" not in self.tables:
357				return
358			for tag in ("maxp", "head", "hhea", "loca", "glyf", "hmtx"):
359				self._decompileTable(tag)
360			hmtxTable = self.ttFont["hmtx"]
361			data = hmtxTable.transform(self.ttFont)  # can be None
362		else:
363			raise TTLibError("Transform for table '%s' is unknown" % tag)
364		return data
365
366	def _calcMasterChecksum(self):
367		"""Calculate checkSumAdjustment."""
368		tags = list(self.tables.keys())
369		checksums = []
370		for i in range(len(tags)):
371			checksums.append(self.tables[tags[i]].checkSum)
372
373		# Create a SFNT directory for checksum calculation purposes
374		self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16)
375		directory = sstruct.pack(sfntDirectoryFormat, self)
376		tables = sorted(self.tables.items())
377		for tag, entry in tables:
378			sfntEntry = SFNTDirectoryEntry()
379			sfntEntry.tag = entry.tag
380			sfntEntry.checkSum = entry.checkSum
381			sfntEntry.offset = entry.origOffset
382			sfntEntry.length = entry.origLength
383			directory = directory + sfntEntry.toString()
384
385		directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
386		assert directory_end == len(directory)
387
388		checksums.append(calcChecksum(directory))
389		checksum = sum(checksums) & 0xffffffff
390		# BiboAfba!
391		checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff
392		return checksumadjustment
393
394	def writeMasterChecksum(self):
395		"""Write checkSumAdjustment to the transformBuffer."""
396		checksumadjustment = self._calcMasterChecksum()
397		self.transformBuffer.seek(self.tables['head'].offset + 8)
398		self.transformBuffer.write(struct.pack(">L", checksumadjustment))
399
400	def _calcTotalSize(self):
401		"""Calculate total size of WOFF2 font, including any meta- and/or private data."""
402		offset = self.directorySize
403		for entry in self.tables.values():
404			offset += len(entry.toString())
405		offset += self.totalCompressedSize
406		offset = (offset + 3) & ~3
407		offset = self._calcFlavorDataOffsetsAndSize(offset)
408		return offset
409
410	def _calcFlavorDataOffsetsAndSize(self, start):
411		"""Calculate offsets and lengths for any meta- and/or private data."""
412		offset = start
413		data = self.flavorData
414		if data.metaData:
415			self.metaOrigLength = len(data.metaData)
416			self.metaOffset = offset
417			self.compressedMetaData = brotli.compress(
418				data.metaData, mode=brotli.MODE_TEXT)
419			self.metaLength = len(self.compressedMetaData)
420			offset += self.metaLength
421		else:
422			self.metaOffset = self.metaLength = self.metaOrigLength = 0
423			self.compressedMetaData = b""
424		if data.privData:
425			# make sure private data is padded to 4-byte boundary
426			offset = (offset + 3) & ~3
427			self.privOffset = offset
428			self.privLength = len(data.privData)
429			offset += self.privLength
430		else:
431			self.privOffset = self.privLength = 0
432		return offset
433
434	def _getVersion(self):
435		"""Return the WOFF2 font's (majorVersion, minorVersion) tuple."""
436		data = self.flavorData
437		if data.majorVersion is not None and data.minorVersion is not None:
438			return data.majorVersion, data.minorVersion
439		else:
440			# if None, return 'fontRevision' from 'head' table
441			if 'head' in self.tables:
442				return struct.unpack(">HH", self.tables['head'].data[4:8])
443			else:
444				return 0, 0
445
446	def _packTableDirectory(self):
447		"""Return WOFF2 table directory data."""
448		directory = sstruct.pack(self.directoryFormat, self)
449		for entry in self.tables.values():
450			directory = directory + entry.toString()
451		return directory
452
453	def _writeFlavorData(self):
454		"""Write metadata and/or private data using appropiate padding."""
455		compressedMetaData = self.compressedMetaData
456		privData = self.flavorData.privData
457		if compressedMetaData and privData:
458			compressedMetaData = pad(compressedMetaData, size=4)
459		if compressedMetaData:
460			self.file.seek(self.metaOffset)
461			assert self.file.tell() == self.metaOffset
462			self.file.write(compressedMetaData)
463		if privData:
464			self.file.seek(self.privOffset)
465			assert self.file.tell() == self.privOffset
466			self.file.write(privData)
467
468	def reordersTables(self):
469		return True
470
471
472# -- woff2 directory helpers and cruft
473
474woff2DirectoryFormat = """
475		> # big endian
476		signature:           4s   # "wOF2"
477		sfntVersion:         4s
478		length:              L    # total woff2 file size
479		numTables:           H    # number of tables
480		reserved:            H    # set to 0
481		totalSfntSize:       L    # uncompressed size
482		totalCompressedSize: L    # compressed size
483		majorVersion:        H    # major version of WOFF file
484		minorVersion:        H    # minor version of WOFF file
485		metaOffset:          L    # offset to metadata block
486		metaLength:          L    # length of compressed metadata
487		metaOrigLength:      L    # length of uncompressed metadata
488		privOffset:          L    # offset to private data block
489		privLength:          L    # length of private data block
490"""
491
492woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat)
493
494woff2KnownTags = (
495	"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post", "cvt ",
496	"fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT", "EBLC", "gasp",
497	"hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea", "vmtx", "BASE", "GDEF",
498	"GPOS", "GSUB", "EBSC", "JSTF", "MATH", "CBDT", "CBLC", "COLR", "CPAL",
499	"SVG ", "sbix", "acnt", "avar", "bdat", "bloc", "bsln", "cvar", "fdsc",
500	"feat", "fmtx", "fvar", "gvar", "hsty", "just", "lcar", "mort", "morx",
501	"opbd", "prop", "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill")
502
503woff2FlagsFormat = """
504		> # big endian
505		flags: B  # table type and flags
506"""
507
508woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat)
509
510woff2UnknownTagFormat = """
511		> # big endian
512		tag: 4s  # 4-byte tag (optional)
513"""
514
515woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat)
516
517woff2UnknownTagIndex = 0x3F
518
519woff2Base128MaxSize = 5
520woff2DirectoryEntryMaxSize = woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize
521
522woff2TransformedTableTags = ('glyf', 'loca')
523
524woff2GlyfTableFormat = """
525		> # big endian
526		version:                  L  # = 0x00000000
527		numGlyphs:                H  # Number of glyphs
528		indexFormat:              H  # Offset format for loca table
529		nContourStreamSize:       L  # Size of nContour stream
530		nPointsStreamSize:        L  # Size of nPoints stream
531		flagStreamSize:           L  # Size of flag stream
532		glyphStreamSize:          L  # Size of glyph stream
533		compositeStreamSize:      L  # Size of composite stream
534		bboxStreamSize:           L  # Comnined size of bboxBitmap and bboxStream
535		instructionStreamSize:    L  # Size of instruction stream
536"""
537
538woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat)
539
540bboxFormat = """
541		>	# big endian
542		xMin:				h
543		yMin:				h
544		xMax:				h
545		yMax:				h
546"""
547
548
549def getKnownTagIndex(tag):
550	"""Return index of 'tag' in woff2KnownTags list. Return 63 if not found."""
551	for i in range(len(woff2KnownTags)):
552		if tag == woff2KnownTags[i]:
553			return i
554	return woff2UnknownTagIndex
555
556
557class WOFF2DirectoryEntry(DirectoryEntry):
558
559	def fromFile(self, file):
560		pos = file.tell()
561		data = file.read(woff2DirectoryEntryMaxSize)
562		left = self.fromString(data)
563		consumed = len(data) - len(left)
564		file.seek(pos + consumed)
565
566	def fromString(self, data):
567		if len(data) < 1:
568			raise TTLibError("can't read table 'flags': not enough data")
569		dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self)
570		if self.flags & 0x3F == 0x3F:
571			# if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value
572			if len(data) < woff2UnknownTagSize:
573				raise TTLibError("can't read table 'tag': not enough data")
574			dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self)
575		else:
576			# otherwise, tag is derived from a fixed 'Known Tags' table
577			self.tag = woff2KnownTags[self.flags & 0x3F]
578		self.tag = Tag(self.tag)
579		self.origLength, data = unpackBase128(data)
580		self.length = self.origLength
581		if self.transformed:
582			self.length, data = unpackBase128(data)
583			if self.tag == 'loca' and self.length != 0:
584				raise TTLibError(
585					"the transformLength of the 'loca' table must be 0")
586		# return left over data
587		return data
588
589	def toString(self):
590		data = bytechr(self.flags)
591		if (self.flags & 0x3F) == 0x3F:
592			data += struct.pack('>4s', self.tag.tobytes())
593		data += packBase128(self.origLength)
594		if self.transformed:
595			data += packBase128(self.length)
596		return data
597
598	@property
599	def transformVersion(self):
600		"""Return bits 6-7 of table entry's flags, which indicate the preprocessing
601		transformation version number (between 0 and 3).
602		"""
603		return self.flags >> 6
604
605	@transformVersion.setter
606	def transformVersion(self, value):
607		assert 0 <= value <= 3
608		self.flags |= value << 6
609
610	@property
611	def transformed(self):
612		"""Return True if the table has any transformation, else return False."""
613		# For all tables in a font, except for 'glyf' and 'loca', the transformation
614		# version 0 indicates the null transform (where the original table data is
615		# passed directly to the Brotli compressor). For 'glyf' and 'loca' tables,
616		# transformation version 3 indicates the null transform
617		if self.tag in {"glyf", "loca"}:
618			return self.transformVersion != 3
619		else:
620			return self.transformVersion != 0
621
622	@transformed.setter
623	def transformed(self, booleanValue):
624		# here we assume that a non-null transform means version 0 for 'glyf' and
625		# 'loca' and 1 for every other table (e.g. hmtx); but that may change as
626		# new transformation formats are introduced in the future (if ever).
627		if self.tag in {"glyf", "loca"}:
628			self.transformVersion = 3 if not booleanValue else 0
629		else:
630			self.transformVersion = int(booleanValue)
631
632
633class WOFF2LocaTable(getTableClass('loca')):
634	"""Same as parent class. The only difference is that it attempts to preserve
635	the 'indexFormat' as encoded in the WOFF2 glyf table.
636	"""
637
638	def __init__(self, tag=None):
639		self.tableTag = Tag(tag or 'loca')
640
641	def compile(self, ttFont):
642		try:
643			max_location = max(self.locations)
644		except AttributeError:
645			self.set([])
646			max_location = 0
647		if 'glyf' in ttFont and hasattr(ttFont['glyf'], 'indexFormat'):
648			# copile loca using the indexFormat specified in the WOFF2 glyf table
649			indexFormat = ttFont['glyf'].indexFormat
650			if indexFormat == 0:
651				if max_location >= 0x20000:
652					raise TTLibError("indexFormat is 0 but local offsets > 0x20000")
653				if not all(l % 2 == 0 for l in self.locations):
654					raise TTLibError("indexFormat is 0 but local offsets not multiples of 2")
655				locations = array.array("H")
656				for i in range(len(self.locations)):
657					locations.append(self.locations[i] // 2)
658			else:
659				locations = array.array("I", self.locations)
660			if sys.byteorder != "big": locations.byteswap()
661			data = locations.tobytes()
662		else:
663			# use the most compact indexFormat given the current glyph offsets
664			data = super(WOFF2LocaTable, self).compile(ttFont)
665		return data
666
667
668class WOFF2GlyfTable(getTableClass('glyf')):
669	"""Decoder/Encoder for WOFF2 'glyf' table transform."""
670
671	subStreams = (
672		'nContourStream', 'nPointsStream', 'flagStream', 'glyphStream',
673		'compositeStream', 'bboxStream', 'instructionStream')
674
675	def __init__(self, tag=None):
676		self.tableTag = Tag(tag or 'glyf')
677
678	def reconstruct(self, data, ttFont):
679		""" Decompile transformed 'glyf' data. """
680		inputDataSize = len(data)
681
682		if inputDataSize < woff2GlyfTableFormatSize:
683			raise TTLibError("not enough 'glyf' data")
684		dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self)
685		offset = woff2GlyfTableFormatSize
686
687		for stream in self.subStreams:
688			size = getattr(self, stream + 'Size')
689			setattr(self, stream, data[:size])
690			data = data[size:]
691			offset += size
692
693		if offset != inputDataSize:
694			raise TTLibError(
695				"incorrect size of transformed 'glyf' table: expected %d, received %d bytes"
696				% (offset, inputDataSize))
697
698		bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
699		bboxBitmap = self.bboxStream[:bboxBitmapSize]
700		self.bboxBitmap = array.array('B', bboxBitmap)
701		self.bboxStream = self.bboxStream[bboxBitmapSize:]
702
703		self.nContourStream = array.array("h", self.nContourStream)
704		if sys.byteorder != "big": self.nContourStream.byteswap()
705		assert len(self.nContourStream) == self.numGlyphs
706
707		if 'head' in ttFont:
708			ttFont['head'].indexToLocFormat = self.indexFormat
709		try:
710			self.glyphOrder = ttFont.getGlyphOrder()
711		except:
712			self.glyphOrder = None
713		if self.glyphOrder is None:
714			self.glyphOrder = [".notdef"]
715			self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)])
716		else:
717			if len(self.glyphOrder) != self.numGlyphs:
718				raise TTLibError(
719					"incorrect glyphOrder: expected %d glyphs, found %d" %
720					(len(self.glyphOrder), self.numGlyphs))
721
722		glyphs = self.glyphs = {}
723		for glyphID, glyphName in enumerate(self.glyphOrder):
724			glyph = self._decodeGlyph(glyphID)
725			glyphs[glyphName] = glyph
726
727	def transform(self, ttFont):
728		""" Return transformed 'glyf' data """
729		self.numGlyphs = len(self.glyphs)
730		assert len(self.glyphOrder) == self.numGlyphs
731		if 'maxp' in ttFont:
732			ttFont['maxp'].numGlyphs = self.numGlyphs
733		self.indexFormat = ttFont['head'].indexToLocFormat
734
735		for stream in self.subStreams:
736			setattr(self, stream, b"")
737		bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
738		self.bboxBitmap = array.array('B', [0]*bboxBitmapSize)
739
740		for glyphID in range(self.numGlyphs):
741			self._encodeGlyph(glyphID)
742
743		self.bboxStream = self.bboxBitmap.tobytes() + self.bboxStream
744		for stream in self.subStreams:
745			setattr(self, stream + 'Size', len(getattr(self, stream)))
746		self.version = 0
747		data = sstruct.pack(woff2GlyfTableFormat, self)
748		data += bytesjoin([getattr(self, s) for s in self.subStreams])
749		return data
750
751	def _decodeGlyph(self, glyphID):
752		glyph = getTableModule('glyf').Glyph()
753		glyph.numberOfContours = self.nContourStream[glyphID]
754		if glyph.numberOfContours == 0:
755			return glyph
756		elif glyph.isComposite():
757			self._decodeComponents(glyph)
758		else:
759			self._decodeCoordinates(glyph)
760		self._decodeBBox(glyphID, glyph)
761		return glyph
762
763	def _decodeComponents(self, glyph):
764		data = self.compositeStream
765		glyph.components = []
766		more = 1
767		haveInstructions = 0
768		while more:
769			component = getTableModule('glyf').GlyphComponent()
770			more, haveInstr, data = component.decompile(data, self)
771			haveInstructions = haveInstructions | haveInstr
772			glyph.components.append(component)
773		self.compositeStream = data
774		if haveInstructions:
775			self._decodeInstructions(glyph)
776
777	def _decodeCoordinates(self, glyph):
778		data = self.nPointsStream
779		endPtsOfContours = []
780		endPoint = -1
781		for i in range(glyph.numberOfContours):
782			ptsOfContour, data = unpack255UShort(data)
783			endPoint += ptsOfContour
784			endPtsOfContours.append(endPoint)
785		glyph.endPtsOfContours = endPtsOfContours
786		self.nPointsStream = data
787		self._decodeTriplets(glyph)
788		self._decodeInstructions(glyph)
789
790	def _decodeInstructions(self, glyph):
791		glyphStream = self.glyphStream
792		instructionStream = self.instructionStream
793		instructionLength, glyphStream = unpack255UShort(glyphStream)
794		glyph.program = ttProgram.Program()
795		glyph.program.fromBytecode(instructionStream[:instructionLength])
796		self.glyphStream = glyphStream
797		self.instructionStream = instructionStream[instructionLength:]
798
799	def _decodeBBox(self, glyphID, glyph):
800		haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7)))
801		if glyph.isComposite() and not haveBBox:
802			raise TTLibError('no bbox values for composite glyph %d' % glyphID)
803		if haveBBox:
804			dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph)
805		else:
806			glyph.recalcBounds(self)
807
808	def _decodeTriplets(self, glyph):
809
810		def withSign(flag, baseval):
811			assert 0 <= baseval and baseval < 65536, 'integer overflow'
812			return baseval if flag & 1 else -baseval
813
814		nPoints = glyph.endPtsOfContours[-1] + 1
815		flagSize = nPoints
816		if flagSize > len(self.flagStream):
817			raise TTLibError("not enough 'flagStream' data")
818		flagsData = self.flagStream[:flagSize]
819		self.flagStream = self.flagStream[flagSize:]
820		flags = array.array('B', flagsData)
821
822		triplets = array.array('B', self.glyphStream)
823		nTriplets = len(triplets)
824		assert nPoints <= nTriplets
825
826		x = 0
827		y = 0
828		glyph.coordinates = getTableModule('glyf').GlyphCoordinates.zeros(nPoints)
829		glyph.flags = array.array("B")
830		tripletIndex = 0
831		for i in range(nPoints):
832			flag = flags[i]
833			onCurve = not bool(flag >> 7)
834			flag &= 0x7f
835			if flag < 84:
836				nBytes = 1
837			elif flag < 120:
838				nBytes = 2
839			elif flag < 124:
840				nBytes = 3
841			else:
842				nBytes = 4
843			assert ((tripletIndex + nBytes) <= nTriplets)
844			if flag < 10:
845				dx = 0
846				dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex])
847			elif flag < 20:
848				dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex])
849				dy = 0
850			elif flag < 84:
851				b0 = flag - 20
852				b1 = triplets[tripletIndex]
853				dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4))
854				dy = withSign(flag >> 1, 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f))
855			elif flag < 120:
856				b0 = flag - 84
857				dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex])
858				dy = withSign(flag >> 1,
859					1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1])
860			elif flag < 124:
861				b2 = triplets[tripletIndex + 1]
862				dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4))
863				dy = withSign(flag >> 1,
864					((b2 & 0x0f) << 8) + triplets[tripletIndex + 2])
865			else:
866				dx = withSign(flag,
867					(triplets[tripletIndex] << 8) + triplets[tripletIndex + 1])
868				dy = withSign(flag >> 1,
869					(triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3])
870			tripletIndex += nBytes
871			x += dx
872			y += dy
873			glyph.coordinates[i] = (x, y)
874			glyph.flags.append(int(onCurve))
875		bytesConsumed = tripletIndex
876		self.glyphStream = self.glyphStream[bytesConsumed:]
877
878	def _encodeGlyph(self, glyphID):
879		glyphName = self.getGlyphName(glyphID)
880		glyph = self[glyphName]
881		self.nContourStream += struct.pack(">h", glyph.numberOfContours)
882		if glyph.numberOfContours == 0:
883			return
884		elif glyph.isComposite():
885			self._encodeComponents(glyph)
886		else:
887			self._encodeCoordinates(glyph)
888		self._encodeBBox(glyphID, glyph)
889
890	def _encodeComponents(self, glyph):
891		lastcomponent = len(glyph.components) - 1
892		more = 1
893		haveInstructions = 0
894		for i in range(len(glyph.components)):
895			if i == lastcomponent:
896				haveInstructions = hasattr(glyph, "program")
897				more = 0
898			component = glyph.components[i]
899			self.compositeStream += component.compile(more, haveInstructions, self)
900		if haveInstructions:
901			self._encodeInstructions(glyph)
902
903	def _encodeCoordinates(self, glyph):
904		lastEndPoint = -1
905		for endPoint in glyph.endPtsOfContours:
906			ptsOfContour = endPoint - lastEndPoint
907			self.nPointsStream += pack255UShort(ptsOfContour)
908			lastEndPoint = endPoint
909		self._encodeTriplets(glyph)
910		self._encodeInstructions(glyph)
911
912	def _encodeInstructions(self, glyph):
913		instructions = glyph.program.getBytecode()
914		self.glyphStream += pack255UShort(len(instructions))
915		self.instructionStream += instructions
916
917	def _encodeBBox(self, glyphID, glyph):
918		assert glyph.numberOfContours != 0, "empty glyph has no bbox"
919		if not glyph.isComposite():
920			# for simple glyphs, compare the encoded bounding box info with the calculated
921			# values, and if they match omit the bounding box info
922			currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax
923			calculatedBBox = calcIntBounds(glyph.coordinates)
924			if currentBBox == calculatedBBox:
925				return
926		self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7)
927		self.bboxStream += sstruct.pack(bboxFormat, glyph)
928
929	def _encodeTriplets(self, glyph):
930		assert len(glyph.coordinates) == len(glyph.flags)
931		coordinates = glyph.coordinates.copy()
932		coordinates.absoluteToRelative()
933
934		flags = array.array('B')
935		triplets = array.array('B')
936		for i in range(len(coordinates)):
937			onCurve = glyph.flags[i] & _g_l_y_f.flagOnCurve
938			x, y = coordinates[i]
939			absX = abs(x)
940			absY = abs(y)
941			onCurveBit = 0 if onCurve else 128
942			xSignBit = 0 if (x < 0) else 1
943			ySignBit = 0 if (y < 0) else 1
944			xySignBits = xSignBit + 2 * ySignBit
945
946			if x == 0 and absY < 1280:
947				flags.append(onCurveBit + ((absY & 0xf00) >> 7) + ySignBit)
948				triplets.append(absY & 0xff)
949			elif y == 0 and absX < 1280:
950				flags.append(onCurveBit + 10 + ((absX & 0xf00) >> 7) + xSignBit)
951				triplets.append(absX & 0xff)
952			elif absX < 65 and absY < 65:
953				flags.append(onCurveBit + 20 + ((absX - 1) & 0x30) + (((absY - 1) & 0x30) >> 2) + xySignBits)
954				triplets.append((((absX - 1) & 0xf) << 4) | ((absY - 1) & 0xf))
955			elif absX < 769 and absY < 769:
956				flags.append(onCurveBit + 84 + 12 * (((absX - 1) & 0x300) >> 8) + (((absY - 1) & 0x300) >> 6) + xySignBits)
957				triplets.append((absX - 1) & 0xff)
958				triplets.append((absY - 1) & 0xff)
959			elif absX < 4096 and absY < 4096:
960				flags.append(onCurveBit + 120 + xySignBits)
961				triplets.append(absX >> 4)
962				triplets.append(((absX & 0xf) << 4) | (absY >> 8))
963				triplets.append(absY & 0xff)
964			else:
965				flags.append(onCurveBit + 124 + xySignBits)
966				triplets.append(absX >> 8)
967				triplets.append(absX & 0xff)
968				triplets.append(absY >> 8)
969				triplets.append(absY & 0xff)
970
971		self.flagStream += flags.tobytes()
972		self.glyphStream += triplets.tobytes()
973
974
975class WOFF2HmtxTable(getTableClass("hmtx")):
976
977	def __init__(self, tag=None):
978		self.tableTag = Tag(tag or 'hmtx')
979
980	def reconstruct(self, data, ttFont):
981		flags, = struct.unpack(">B", data[:1])
982		data = data[1:]
983		if flags & 0b11111100 != 0:
984			raise TTLibError("Bits 2-7 of '%s' flags are reserved" % self.tableTag)
985
986		# When bit 0 is _not_ set, the lsb[] array is present
987		hasLsbArray = flags & 1 == 0
988		# When bit 1 is _not_ set, the leftSideBearing[] array is present
989		hasLeftSideBearingArray = flags & 2 == 0
990		if hasLsbArray and hasLeftSideBearingArray:
991			raise TTLibError(
992				"either bits 0 or 1 (or both) must set in transformed '%s' flags"
993				% self.tableTag
994			)
995
996		glyfTable = ttFont["glyf"]
997		headerTable = ttFont["hhea"]
998		glyphOrder = glyfTable.glyphOrder
999		numGlyphs = len(glyphOrder)
1000		numberOfHMetrics = min(int(headerTable.numberOfHMetrics), numGlyphs)
1001
1002		assert len(data) >= 2 * numberOfHMetrics
1003		advanceWidthArray = array.array("H", data[:2 * numberOfHMetrics])
1004		if sys.byteorder != "big":
1005			advanceWidthArray.byteswap()
1006		data = data[2 * numberOfHMetrics:]
1007
1008		if hasLsbArray:
1009			assert len(data) >= 2 * numberOfHMetrics
1010			lsbArray = array.array("h", data[:2 * numberOfHMetrics])
1011			if sys.byteorder != "big":
1012				lsbArray.byteswap()
1013			data = data[2 * numberOfHMetrics:]
1014		else:
1015			# compute (proportional) glyphs' lsb from their xMin
1016			lsbArray = array.array("h")
1017			for i, glyphName in enumerate(glyphOrder):
1018				if i >= numberOfHMetrics:
1019					break
1020				glyph = glyfTable[glyphName]
1021				xMin = getattr(glyph, "xMin", 0)
1022				lsbArray.append(xMin)
1023
1024		numberOfSideBearings = numGlyphs - numberOfHMetrics
1025		if hasLeftSideBearingArray:
1026			assert len(data) >= 2 * numberOfSideBearings
1027			leftSideBearingArray = array.array("h", data[:2 * numberOfSideBearings])
1028			if sys.byteorder != "big":
1029				leftSideBearingArray.byteswap()
1030			data = data[2 * numberOfSideBearings:]
1031		else:
1032			# compute (monospaced) glyphs' leftSideBearing from their xMin
1033			leftSideBearingArray = array.array("h")
1034			for i, glyphName in enumerate(glyphOrder):
1035				if i < numberOfHMetrics:
1036					continue
1037				glyph = glyfTable[glyphName]
1038				xMin = getattr(glyph, "xMin", 0)
1039				leftSideBearingArray.append(xMin)
1040
1041		if data:
1042			raise TTLibError("too much '%s' table data" % self.tableTag)
1043
1044		self.metrics = {}
1045		for i in range(numberOfHMetrics):
1046			glyphName = glyphOrder[i]
1047			advanceWidth, lsb = advanceWidthArray[i], lsbArray[i]
1048			self.metrics[glyphName] = (advanceWidth, lsb)
1049		lastAdvance = advanceWidthArray[-1]
1050		for i in range(numberOfSideBearings):
1051			glyphName = glyphOrder[i + numberOfHMetrics]
1052			self.metrics[glyphName] = (lastAdvance, leftSideBearingArray[i])
1053
1054	def transform(self, ttFont):
1055		glyphOrder = ttFont.getGlyphOrder()
1056		glyf = ttFont["glyf"]
1057		hhea = ttFont["hhea"]
1058		numberOfHMetrics = hhea.numberOfHMetrics
1059
1060		# check if any of the proportional glyphs has left sidebearings that
1061		# differ from their xMin bounding box values.
1062		hasLsbArray = False
1063		for i in range(numberOfHMetrics):
1064			glyphName = glyphOrder[i]
1065			lsb = self.metrics[glyphName][1]
1066			if lsb != getattr(glyf[glyphName], "xMin", 0):
1067				hasLsbArray = True
1068				break
1069
1070		# do the same for the monospaced glyphs (if any) at the end of hmtx table
1071		hasLeftSideBearingArray = False
1072		for i in range(numberOfHMetrics, len(glyphOrder)):
1073			glyphName = glyphOrder[i]
1074			lsb = self.metrics[glyphName][1]
1075			if lsb != getattr(glyf[glyphName], "xMin", 0):
1076				hasLeftSideBearingArray = True
1077				break
1078
1079		# if we need to encode both sidebearings arrays, then no transformation is
1080		# applicable, and we must use the untransformed hmtx data
1081		if hasLsbArray and hasLeftSideBearingArray:
1082			return
1083
1084		# set bit 0 and 1 when the respective arrays are _not_ present
1085		flags = 0
1086		if not hasLsbArray:
1087			flags |= 1 << 0
1088		if not hasLeftSideBearingArray:
1089			flags |= 1 << 1
1090
1091		data = struct.pack(">B", flags)
1092
1093		advanceWidthArray = array.array(
1094			"H",
1095			[
1096				self.metrics[glyphName][0]
1097				for i, glyphName in enumerate(glyphOrder)
1098				if i < numberOfHMetrics
1099			]
1100		)
1101		if sys.byteorder != "big":
1102			advanceWidthArray.byteswap()
1103		data += advanceWidthArray.tobytes()
1104
1105		if hasLsbArray:
1106			lsbArray = array.array(
1107				"h",
1108				[
1109					self.metrics[glyphName][1]
1110					for i, glyphName in enumerate(glyphOrder)
1111					if i < numberOfHMetrics
1112				]
1113			)
1114			if sys.byteorder != "big":
1115				lsbArray.byteswap()
1116			data += lsbArray.tobytes()
1117
1118		if hasLeftSideBearingArray:
1119			leftSideBearingArray = array.array(
1120				"h",
1121				[
1122					self.metrics[glyphOrder[i]][1]
1123					for i in range(numberOfHMetrics, len(glyphOrder))
1124				]
1125			)
1126			if sys.byteorder != "big":
1127				leftSideBearingArray.byteswap()
1128			data += leftSideBearingArray.tobytes()
1129
1130		return data
1131
1132
1133class WOFF2FlavorData(WOFFFlavorData):
1134
1135	Flavor = 'woff2'
1136
1137	def __init__(self, reader=None, data=None, transformedTables=None):
1138		"""Data class that holds the WOFF2 header major/minor version, any
1139		metadata or private data (as bytes strings), and the set of
1140		table tags that have transformations applied (if reader is not None),
1141		or will have once the WOFF2 font is compiled.
1142
1143		Args:
1144			reader: an SFNTReader (or subclass) object to read flavor data from.
1145			data: another WOFFFlavorData object to initialise data from.
1146			transformedTables: set of strings containing table tags to be transformed.
1147
1148		Raises:
1149			ImportError if the brotli module is not installed.
1150
1151		NOTE: The 'reader' argument, on the one hand, and the 'data' and
1152		'transformedTables' arguments, on the other hand, are mutually exclusive.
1153		"""
1154		if not haveBrotli:
1155			raise ImportError("No module named brotli")
1156
1157		if reader is not None:
1158			if data is not None:
1159				raise TypeError(
1160					"'reader' and 'data' arguments are mutually exclusive"
1161				)
1162			if transformedTables is not None:
1163				raise TypeError(
1164					"'reader' and 'transformedTables' arguments are mutually exclusive"
1165				)
1166
1167		if transformedTables is not None and (
1168				"glyf" in transformedTables and "loca" not in transformedTables
1169				or "loca" in transformedTables and "glyf" not in transformedTables
1170			):
1171				raise ValueError(
1172					"'glyf' and 'loca' must be transformed (or not) together"
1173				)
1174		super(WOFF2FlavorData, self).__init__(reader=reader)
1175		if reader:
1176			transformedTables = [
1177				tag
1178				for tag, entry in reader.tables.items()
1179				if entry.transformed
1180			]
1181		elif data:
1182			self.majorVersion = data.majorVersion
1183			self.majorVersion = data.minorVersion
1184			self.metaData = data.metaData
1185			self.privData = data.privData
1186			if transformedTables is None and hasattr(data, "transformedTables"):
1187				 transformedTables = data.transformedTables
1188
1189		if transformedTables is None:
1190			transformedTables = woff2TransformedTableTags
1191
1192		self.transformedTables = set(transformedTables)
1193
1194	def _decompress(self, rawData):
1195		return brotli.decompress(rawData)
1196
1197
1198def unpackBase128(data):
1199	r""" Read one to five bytes from UIntBase128-encoded input string, and return
1200	a tuple containing the decoded integer plus any leftover data.
1201
1202	>>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00")
1203	True
1204	>>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295
1205	True
1206	>>> unpackBase128(b'\x80\x80\x3f')  # doctest: +IGNORE_EXCEPTION_DETAIL
1207	Traceback (most recent call last):
1208	  File "<stdin>", line 1, in ?
1209	TTLibError: UIntBase128 value must not start with leading zeros
1210	>>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0]  # doctest: +IGNORE_EXCEPTION_DETAIL
1211	Traceback (most recent call last):
1212	  File "<stdin>", line 1, in ?
1213	TTLibError: UIntBase128-encoded sequence is longer than 5 bytes
1214	>>> unpackBase128(b'\x90\x80\x80\x80\x00')[0]  # doctest: +IGNORE_EXCEPTION_DETAIL
1215	Traceback (most recent call last):
1216	  File "<stdin>", line 1, in ?
1217	TTLibError: UIntBase128 value exceeds 2**32-1
1218	"""
1219	if len(data) == 0:
1220		raise TTLibError('not enough data to unpack UIntBase128')
1221	result = 0
1222	if byteord(data[0]) == 0x80:
1223		# font must be rejected if UIntBase128 value starts with 0x80
1224		raise TTLibError('UIntBase128 value must not start with leading zeros')
1225	for i in range(woff2Base128MaxSize):
1226		if len(data) == 0:
1227			raise TTLibError('not enough data to unpack UIntBase128')
1228		code = byteord(data[0])
1229		data = data[1:]
1230		# if any of the top seven bits are set then we're about to overflow
1231		if result & 0xFE000000:
1232			raise TTLibError('UIntBase128 value exceeds 2**32-1')
1233		# set current value = old value times 128 bitwise-or (byte bitwise-and 127)
1234		result = (result << 7) | (code & 0x7f)
1235		# repeat until the most significant bit of byte is false
1236		if (code & 0x80) == 0:
1237			# return result plus left over data
1238			return result, data
1239	# make sure not to exceed the size bound
1240	raise TTLibError('UIntBase128-encoded sequence is longer than 5 bytes')
1241
1242
1243def base128Size(n):
1244	""" Return the length in bytes of a UIntBase128-encoded sequence with value n.
1245
1246	>>> base128Size(0)
1247	1
1248	>>> base128Size(24567)
1249	3
1250	>>> base128Size(2**32-1)
1251	5
1252	"""
1253	assert n >= 0
1254	size = 1
1255	while n >= 128:
1256		size += 1
1257		n >>= 7
1258	return size
1259
1260
1261def packBase128(n):
1262	r""" Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of
1263	bytes using UIntBase128 variable-length encoding. Produce the shortest possible
1264	encoding.
1265
1266	>>> packBase128(63) == b"\x3f"
1267	True
1268	>>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f'
1269	True
1270	"""
1271	if n < 0 or n >= 2**32:
1272		raise TTLibError(
1273			"UIntBase128 format requires 0 <= integer <= 2**32-1")
1274	data = b''
1275	size = base128Size(n)
1276	for i in range(size):
1277		b = (n >> (7 * (size - i - 1))) & 0x7f
1278		if i < size - 1:
1279			b |= 0x80
1280		data += struct.pack('B', b)
1281	return data
1282
1283
1284def unpack255UShort(data):
1285	""" Read one to three bytes from 255UInt16-encoded input string, and return a
1286	tuple containing the decoded integer plus any leftover data.
1287
1288	>>> unpack255UShort(bytechr(252))[0]
1289	252
1290
1291	Note that some numbers (e.g. 506) can have multiple encodings:
1292	>>> unpack255UShort(struct.pack("BB", 254, 0))[0]
1293	506
1294	>>> unpack255UShort(struct.pack("BB", 255, 253))[0]
1295	506
1296	>>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0]
1297	506
1298	"""
1299	code = byteord(data[:1])
1300	data = data[1:]
1301	if code == 253:
1302		# read two more bytes as an unsigned short
1303		if len(data) < 2:
1304			raise TTLibError('not enough data to unpack 255UInt16')
1305		result, = struct.unpack(">H", data[:2])
1306		data = data[2:]
1307	elif code == 254:
1308		# read another byte, plus 253 * 2
1309		if len(data) == 0:
1310			raise TTLibError('not enough data to unpack 255UInt16')
1311		result = byteord(data[:1])
1312		result += 506
1313		data = data[1:]
1314	elif code == 255:
1315		# read another byte, plus 253
1316		if len(data) == 0:
1317			raise TTLibError('not enough data to unpack 255UInt16')
1318		result = byteord(data[:1])
1319		result += 253
1320		data = data[1:]
1321	else:
1322		# leave as is if lower than 253
1323		result = code
1324	# return result plus left over data
1325	return result, data
1326
1327
1328def pack255UShort(value):
1329	r""" Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring
1330	using 255UInt16 variable-length encoding.
1331
1332	>>> pack255UShort(252) == b'\xfc'
1333	True
1334	>>> pack255UShort(506) == b'\xfe\x00'
1335	True
1336	>>> pack255UShort(762) == b'\xfd\x02\xfa'
1337	True
1338	"""
1339	if value < 0 or value > 0xFFFF:
1340		raise TTLibError(
1341			"255UInt16 format requires 0 <= integer <= 65535")
1342	if value < 253:
1343		return struct.pack(">B", value)
1344	elif value < 506:
1345		return struct.pack(">BB", 255, value - 253)
1346	elif value < 762:
1347		return struct.pack(">BB", 254, value - 506)
1348	else:
1349		return struct.pack(">BH", 253, value)
1350
1351
1352def compress(input_file, output_file, transform_tables=None):
1353	"""Compress OpenType font to WOFF2.
1354
1355	Args:
1356		input_file: a file path, file or file-like object (open in binary mode)
1357			containing an OpenType font (either CFF- or TrueType-flavored).
1358		output_file: a file path, file or file-like object where to save the
1359			compressed WOFF2 font.
1360		transform_tables: Optional[Iterable[str]]: a set of table tags for which
1361			to enable preprocessing transformations. By default, only 'glyf'
1362			and 'loca' tables are transformed. An empty set means disable all
1363			transformations.
1364	"""
1365	log.info("Processing %s => %s" % (input_file, output_file))
1366
1367	font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
1368	font.flavor = "woff2"
1369
1370	if transform_tables is not None:
1371		font.flavorData = WOFF2FlavorData(
1372			data=font.flavorData, transformedTables=transform_tables
1373		)
1374
1375	font.save(output_file, reorderTables=False)
1376
1377
1378def decompress(input_file, output_file):
1379	"""Decompress WOFF2 font to OpenType font.
1380
1381	Args:
1382		input_file: a file path, file or file-like object (open in binary mode)
1383			containing a compressed WOFF2 font.
1384		output_file: a file path, file or file-like object where to save the
1385			decompressed OpenType font.
1386	"""
1387	log.info("Processing %s => %s" % (input_file, output_file))
1388
1389	font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
1390	font.flavor = None
1391	font.flavorData = None
1392	font.save(output_file, reorderTables=True)
1393
1394
1395def main(args=None):
1396	"""Compress and decompress WOFF2 fonts"""
1397	import argparse
1398	from fontTools import configLogger
1399	from fontTools.ttx import makeOutputFileName
1400
1401	class _HelpAction(argparse._HelpAction):
1402
1403		def __call__(self, parser, namespace, values, option_string=None):
1404			subparsers_actions = [
1405					action for action in parser._actions
1406					if isinstance(action, argparse._SubParsersAction)]
1407			for subparsers_action in subparsers_actions:
1408					for choice, subparser in subparsers_action.choices.items():
1409							print(subparser.format_help())
1410			parser.exit()
1411
1412	class _NoGlyfTransformAction(argparse.Action):
1413		def __call__(self, parser, namespace, values, option_string=None):
1414			namespace.transform_tables.difference_update({"glyf", "loca"})
1415
1416	class _HmtxTransformAction(argparse.Action):
1417		def __call__(self, parser, namespace, values, option_string=None):
1418			namespace.transform_tables.add("hmtx")
1419
1420	parser = argparse.ArgumentParser(
1421		prog="fonttools ttLib.woff2",
1422		description=main.__doc__,
1423		add_help = False
1424	)
1425
1426	parser.add_argument('-h', '--help', action=_HelpAction,
1427		help='show this help message and exit')
1428
1429	parser_group = parser.add_subparsers(title="sub-commands")
1430	parser_compress = parser_group.add_parser("compress",
1431		description = "Compress a TTF or OTF font to WOFF2")
1432	parser_decompress = parser_group.add_parser("decompress",
1433		description = "Decompress a WOFF2 font to OTF")
1434
1435	for subparser in (parser_compress, parser_decompress):
1436		group = subparser.add_mutually_exclusive_group(required=False)
1437		group.add_argument(
1438			"-v",
1439			"--verbose",
1440			action="store_true",
1441			help="print more messages to console",
1442		)
1443		group.add_argument(
1444			"-q",
1445			"--quiet",
1446			action="store_true",
1447			help="do not print messages to console",
1448		)
1449
1450	parser_compress.add_argument(
1451		"input_file",
1452		metavar="INPUT",
1453		help="the input OpenType font (.ttf or .otf)",
1454	)
1455	parser_decompress.add_argument(
1456		"input_file",
1457		metavar="INPUT",
1458		help="the input WOFF2 font",
1459	)
1460
1461	parser_compress.add_argument(
1462		"-o",
1463		"--output-file",
1464		metavar="OUTPUT",
1465		help="the output WOFF2 font",
1466	)
1467	parser_decompress.add_argument(
1468		"-o",
1469		"--output-file",
1470		metavar="OUTPUT",
1471		help="the output OpenType font",
1472	)
1473
1474	transform_group = parser_compress.add_argument_group()
1475	transform_group.add_argument(
1476		"--no-glyf-transform",
1477		dest="transform_tables",
1478		nargs=0,
1479		action=_NoGlyfTransformAction,
1480		help="Do not transform glyf (and loca) tables",
1481	)
1482	transform_group.add_argument(
1483		"--hmtx-transform",
1484		dest="transform_tables",
1485		nargs=0,
1486		action=_HmtxTransformAction,
1487		help="Enable optional transformation for 'hmtx' table",
1488	)
1489
1490	parser_compress.set_defaults(
1491		subcommand=compress,
1492		transform_tables={"glyf", "loca"},
1493	)
1494	parser_decompress.set_defaults(subcommand=decompress)
1495
1496	options = vars(parser.parse_args(args))
1497
1498	subcommand = options.pop("subcommand", None)
1499	if not subcommand:
1500		parser.print_help()
1501		return
1502
1503	quiet = options.pop("quiet")
1504	verbose = options.pop("verbose")
1505	configLogger(
1506		level=("ERROR" if quiet else "DEBUG" if verbose else "INFO"),
1507	)
1508
1509	if not options["output_file"]:
1510		if subcommand is compress:
1511			extension = ".woff2"
1512		elif subcommand is decompress:
1513			# choose .ttf/.otf file extension depending on sfntVersion
1514			with open(options["input_file"], "rb") as f:
1515				f.seek(4)  # skip 'wOF2' signature
1516				sfntVersion = f.read(4)
1517			assert len(sfntVersion) == 4, "not enough data"
1518			extension = ".otf" if sfntVersion == b"OTTO" else ".ttf"
1519		else:
1520			raise AssertionError(subcommand)
1521		options["output_file"] = makeOutputFileName(
1522			options["input_file"], outputDir=None, extension=extension
1523		)
1524
1525	try:
1526		subcommand(**options)
1527	except TTLibError as e:
1528		parser.error(e)
1529
1530
1531if __name__ == "__main__":
1532	sys.exit(main())
1533