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