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