• 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(__name__)
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 tag in woff2TransformedTableTags:
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		if tag not in woff2TransformedTableTags:
94			raise TTLibError("transform for table '%s' is unknown" % tag)
95		entry = self.tables[Tag(tag)]
96		rawData = entry.loadData(self.transformBuffer)
97		if tag == 'glyf':
98			# no need to pad glyph data when reconstructing
99			padding = self.padding if hasattr(self, 'padding') else None
100			data = self._reconstructGlyf(rawData, padding)
101		elif tag == 'loca':
102			data = self._reconstructLoca()
103		else:
104			raise NotImplementedError
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
134class WOFF2Writer(SFNTWriter):
135
136	flavor = "woff2"
137
138	def __init__(self, file, numTables, sfntVersion="\000\001\000\000",
139		         flavor=None, flavorData=None):
140		if not haveBrotli:
141			log.error(
142				'The WOFF2 encoder requires the Brotli Python extension, available at: '
143				'https://github.com/google/brotli')
144			raise ImportError("No module named brotli")
145
146		self.file = file
147		self.numTables = numTables
148		self.sfntVersion = Tag(sfntVersion)
149		self.flavorData = flavorData or WOFF2FlavorData()
150
151		self.directoryFormat = woff2DirectoryFormat
152		self.directorySize = woff2DirectorySize
153		self.DirectoryEntry = WOFF2DirectoryEntry
154
155		self.signature = Tag("wOF2")
156
157		self.nextTableOffset = 0
158		self.transformBuffer = BytesIO()
159
160		self.tables = OrderedDict()
161
162		# make empty TTFont to store data while normalising and transforming tables
163		self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
164
165	def __setitem__(self, tag, data):
166		"""Associate new entry named 'tag' with raw table data."""
167		if tag in self.tables:
168			raise TTLibError("cannot rewrite '%s' table" % tag)
169		if tag == 'DSIG':
170			# always drop DSIG table, since the encoding process can invalidate it
171			self.numTables -= 1
172			return
173
174		entry = self.DirectoryEntry()
175		entry.tag = Tag(tag)
176		entry.flags = getKnownTagIndex(entry.tag)
177		# WOFF2 table data are written to disk only on close(), after all tags
178		# have been specified
179		entry.data = data
180
181		self.tables[tag] = entry
182
183	def close(self):
184		""" All tags must have been specified. Now write the table data and directory.
185		"""
186		if len(self.tables) != self.numTables:
187			raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(self.tables)))
188
189		if self.sfntVersion in ("\x00\x01\x00\x00", "true"):
190			isTrueType = True
191		elif self.sfntVersion == "OTTO":
192			isTrueType = False
193		else:
194			raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
195
196		# The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned.
197		# However, the reference WOFF2 implementation still fails to reconstruct
198		# 'unpadded' glyf tables, therefore we need to 'normalise' them.
199		# See:
200		# https://github.com/khaledhosny/ots/issues/60
201		# https://github.com/google/woff2/issues/15
202		if isTrueType:
203			self._normaliseGlyfAndLoca(padding=4)
204		self._setHeadTransformFlag()
205
206		# To pass the legacy OpenType Sanitiser currently included in browsers,
207		# we must sort the table directory and data alphabetically by tag.
208		# See:
209		# https://github.com/google/woff2/pull/3
210		# https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html
211		# TODO(user): remove to match spec once browsers are on newer OTS
212		self.tables = OrderedDict(sorted(self.tables.items()))
213
214		self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets()
215
216		fontData = self._transformTables()
217		compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT)
218
219		self.totalCompressedSize = len(compressedFont)
220		self.length = self._calcTotalSize()
221		self.majorVersion, self.minorVersion = self._getVersion()
222		self.reserved = 0
223
224		directory = self._packTableDirectory()
225		self.file.seek(0)
226		self.file.write(pad(directory + compressedFont, size=4))
227		self._writeFlavorData()
228
229	def _normaliseGlyfAndLoca(self, padding=4):
230		""" Recompile glyf and loca tables, aligning glyph offsets to multiples of
231		'padding' size. Update the head table's 'indexToLocFormat' accordingly while
232		compiling loca.
233		"""
234		if self.sfntVersion == "OTTO":
235			return
236
237		# make up glyph names required to decompile glyf table
238		self._decompileTable('maxp')
239		numGlyphs = self.ttFont['maxp'].numGlyphs
240		glyphOrder = ['.notdef'] + ["glyph%.5d" % i for i in range(1, numGlyphs)]
241		self.ttFont.setGlyphOrder(glyphOrder)
242
243		for tag in ('head', 'loca', 'glyf'):
244			self._decompileTable(tag)
245		self.ttFont['glyf'].padding = padding
246		for tag in ('glyf', 'loca'):
247			self._compileTable(tag)
248
249	def _setHeadTransformFlag(self):
250		""" Set bit 11 of 'head' table flags to indicate that the font has undergone
251		a lossless modifying transform. Re-compile head table data."""
252		self._decompileTable('head')
253		self.ttFont['head'].flags |= (1 << 11)
254		self._compileTable('head')
255
256	def _decompileTable(self, tag):
257		""" Fetch table data, decompile it, and store it inside self.ttFont. """
258		tag = Tag(tag)
259		if tag not in self.tables:
260			raise TTLibError("missing required table: %s" % tag)
261		if self.ttFont.isLoaded(tag):
262			return
263		data = self.tables[tag].data
264		if tag == 'loca':
265			tableClass = WOFF2LocaTable
266		elif tag == 'glyf':
267			tableClass = WOFF2GlyfTable
268		else:
269			tableClass = getTableClass(tag)
270		table = tableClass(tag)
271		self.ttFont.tables[tag] = table
272		table.decompile(data, self.ttFont)
273
274	def _compileTable(self, tag):
275		""" Compile table and store it in its 'data' attribute. """
276		self.tables[tag].data = self.ttFont[tag].compile(self.ttFont)
277
278	def _calcSFNTChecksumsLengthsAndOffsets(self):
279		""" Compute the 'original' SFNT checksums, lengths and offsets for checksum
280		adjustment calculation. Return the total size of the uncompressed font.
281		"""
282		offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables)
283		for tag, entry in self.tables.items():
284			data = entry.data
285			entry.origOffset = offset
286			entry.origLength = len(data)
287			if tag == 'head':
288				entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:])
289			else:
290				entry.checkSum = calcChecksum(data)
291			offset += (entry.origLength + 3) & ~3
292		return offset
293
294	def _transformTables(self):
295		"""Return transformed font data."""
296		for tag, entry in self.tables.items():
297			if tag in woff2TransformedTableTags:
298				data = self.transformTable(tag)
299			else:
300				data = entry.data
301			entry.offset = self.nextTableOffset
302			entry.saveData(self.transformBuffer, data)
303			self.nextTableOffset += entry.length
304		self.writeMasterChecksum()
305		fontData = self.transformBuffer.getvalue()
306		return fontData
307
308	def transformTable(self, tag):
309		"""Return transformed table data."""
310		if tag not in woff2TransformedTableTags:
311			raise TTLibError("Transform for table '%s' is unknown" % tag)
312		if tag == "loca":
313			data = b""
314		elif tag == "glyf":
315			for tag in ('maxp', 'head', 'loca', 'glyf'):
316				self._decompileTable(tag)
317			glyfTable = self.ttFont['glyf']
318			data = glyfTable.transform(self.ttFont)
319		else:
320			raise NotImplementedError
321		return data
322
323	def _calcMasterChecksum(self):
324		"""Calculate checkSumAdjustment."""
325		tags = list(self.tables.keys())
326		checksums = []
327		for i in range(len(tags)):
328			checksums.append(self.tables[tags[i]].checkSum)
329
330		# Create a SFNT directory for checksum calculation purposes
331		self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16)
332		directory = sstruct.pack(sfntDirectoryFormat, self)
333		tables = sorted(self.tables.items())
334		for tag, entry in tables:
335			sfntEntry = SFNTDirectoryEntry()
336			sfntEntry.tag = entry.tag
337			sfntEntry.checkSum = entry.checkSum
338			sfntEntry.offset = entry.origOffset
339			sfntEntry.length = entry.origLength
340			directory = directory + sfntEntry.toString()
341
342		directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
343		assert directory_end == len(directory)
344
345		checksums.append(calcChecksum(directory))
346		checksum = sum(checksums) & 0xffffffff
347		# BiboAfba!
348		checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff
349		return checksumadjustment
350
351	def writeMasterChecksum(self):
352		"""Write checkSumAdjustment to the transformBuffer."""
353		checksumadjustment = self._calcMasterChecksum()
354		self.transformBuffer.seek(self.tables['head'].offset + 8)
355		self.transformBuffer.write(struct.pack(">L", checksumadjustment))
356
357	def _calcTotalSize(self):
358		"""Calculate total size of WOFF2 font, including any meta- and/or private data."""
359		offset = self.directorySize
360		for entry in self.tables.values():
361			offset += len(entry.toString())
362		offset += self.totalCompressedSize
363		offset = (offset + 3) & ~3
364		offset = self._calcFlavorDataOffsetsAndSize(offset)
365		return offset
366
367	def _calcFlavorDataOffsetsAndSize(self, start):
368		"""Calculate offsets and lengths for any meta- and/or private data."""
369		offset = start
370		data = self.flavorData
371		if data.metaData:
372			self.metaOrigLength = len(data.metaData)
373			self.metaOffset = offset
374			self.compressedMetaData = brotli.compress(
375				data.metaData, mode=brotli.MODE_TEXT)
376			self.metaLength = len(self.compressedMetaData)
377			offset += self.metaLength
378		else:
379			self.metaOffset = self.metaLength = self.metaOrigLength = 0
380			self.compressedMetaData = b""
381		if data.privData:
382			# make sure private data is padded to 4-byte boundary
383			offset = (offset + 3) & ~3
384			self.privOffset = offset
385			self.privLength = len(data.privData)
386			offset += self.privLength
387		else:
388			self.privOffset = self.privLength = 0
389		return offset
390
391	def _getVersion(self):
392		"""Return the WOFF2 font's (majorVersion, minorVersion) tuple."""
393		data = self.flavorData
394		if data.majorVersion is not None and data.minorVersion is not None:
395			return data.majorVersion, data.minorVersion
396		else:
397			# if None, return 'fontRevision' from 'head' table
398			if 'head' in self.tables:
399				return struct.unpack(">HH", self.tables['head'].data[4:8])
400			else:
401				return 0, 0
402
403	def _packTableDirectory(self):
404		"""Return WOFF2 table directory data."""
405		directory = sstruct.pack(self.directoryFormat, self)
406		for entry in self.tables.values():
407			directory = directory + entry.toString()
408		return directory
409
410	def _writeFlavorData(self):
411		"""Write metadata and/or private data using appropiate padding."""
412		compressedMetaData = self.compressedMetaData
413		privData = self.flavorData.privData
414		if compressedMetaData and privData:
415			compressedMetaData = pad(compressedMetaData, size=4)
416		if compressedMetaData:
417			self.file.seek(self.metaOffset)
418			assert self.file.tell() == self.metaOffset
419			self.file.write(compressedMetaData)
420		if privData:
421			self.file.seek(self.privOffset)
422			assert self.file.tell() == self.privOffset
423			self.file.write(privData)
424
425	def reordersTables(self):
426		return True
427
428
429# -- woff2 directory helpers and cruft
430
431woff2DirectoryFormat = """
432		> # big endian
433		signature:           4s   # "wOF2"
434		sfntVersion:         4s
435		length:              L    # total woff2 file size
436		numTables:           H    # number of tables
437		reserved:            H    # set to 0
438		totalSfntSize:       L    # uncompressed size
439		totalCompressedSize: L    # compressed size
440		majorVersion:        H    # major version of WOFF file
441		minorVersion:        H    # minor version of WOFF file
442		metaOffset:          L    # offset to metadata block
443		metaLength:          L    # length of compressed metadata
444		metaOrigLength:      L    # length of uncompressed metadata
445		privOffset:          L    # offset to private data block
446		privLength:          L    # length of private data block
447"""
448
449woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat)
450
451woff2KnownTags = (
452	"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post", "cvt ",
453	"fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT", "EBLC", "gasp",
454	"hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea", "vmtx", "BASE", "GDEF",
455	"GPOS", "GSUB", "EBSC", "JSTF", "MATH", "CBDT", "CBLC", "COLR", "CPAL",
456	"SVG ", "sbix", "acnt", "avar", "bdat", "bloc", "bsln", "cvar", "fdsc",
457	"feat", "fmtx", "fvar", "gvar", "hsty", "just", "lcar", "mort", "morx",
458	"opbd", "prop", "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill")
459
460woff2FlagsFormat = """
461		> # big endian
462		flags: B  # table type and flags
463"""
464
465woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat)
466
467woff2UnknownTagFormat = """
468		> # big endian
469		tag: 4s  # 4-byte tag (optional)
470"""
471
472woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat)
473
474woff2UnknownTagIndex = 0x3F
475
476woff2Base128MaxSize = 5
477woff2DirectoryEntryMaxSize = woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize
478
479woff2TransformedTableTags = ('glyf', 'loca')
480
481woff2GlyfTableFormat = """
482		> # big endian
483		version:                  L  # = 0x00000000
484		numGlyphs:                H  # Number of glyphs
485		indexFormat:              H  # Offset format for loca table
486		nContourStreamSize:       L  # Size of nContour stream
487		nPointsStreamSize:        L  # Size of nPoints stream
488		flagStreamSize:           L  # Size of flag stream
489		glyphStreamSize:          L  # Size of glyph stream
490		compositeStreamSize:      L  # Size of composite stream
491		bboxStreamSize:           L  # Comnined size of bboxBitmap and bboxStream
492		instructionStreamSize:    L  # Size of instruction stream
493"""
494
495woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat)
496
497bboxFormat = """
498		>	# big endian
499		xMin:				h
500		yMin:				h
501		xMax:				h
502		yMax:				h
503"""
504
505
506def getKnownTagIndex(tag):
507	"""Return index of 'tag' in woff2KnownTags list. Return 63 if not found."""
508	for i in range(len(woff2KnownTags)):
509		if tag == woff2KnownTags[i]:
510			return i
511	return woff2UnknownTagIndex
512
513
514class WOFF2DirectoryEntry(DirectoryEntry):
515
516	def fromFile(self, file):
517		pos = file.tell()
518		data = file.read(woff2DirectoryEntryMaxSize)
519		left = self.fromString(data)
520		consumed = len(data) - len(left)
521		file.seek(pos + consumed)
522
523	def fromString(self, data):
524		if len(data) < 1:
525			raise TTLibError("can't read table 'flags': not enough data")
526		dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self)
527		if self.flags & 0x3F == 0x3F:
528			# if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value
529			if len(data) < woff2UnknownTagSize:
530				raise TTLibError("can't read table 'tag': not enough data")
531			dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self)
532		else:
533			# otherwise, tag is derived from a fixed 'Known Tags' table
534			self.tag = woff2KnownTags[self.flags & 0x3F]
535		self.tag = Tag(self.tag)
536		if self.flags & 0xC0 != 0:
537			raise TTLibError('bits 6-7 are reserved and must be 0')
538		self.origLength, data = unpackBase128(data)
539		self.length = self.origLength
540		if self.tag in woff2TransformedTableTags:
541			self.length, data = unpackBase128(data)
542			if self.tag == 'loca' and self.length != 0:
543				raise TTLibError(
544					"the transformLength of the 'loca' table must be 0")
545		# return left over data
546		return data
547
548	def toString(self):
549		data = bytechr(self.flags)
550		if (self.flags & 0x3F) == 0x3F:
551			data += struct.pack('>4s', self.tag.tobytes())
552		data += packBase128(self.origLength)
553		if self.tag in woff2TransformedTableTags:
554			data += packBase128(self.length)
555		return data
556
557
558class WOFF2LocaTable(getTableClass('loca')):
559	"""Same as parent class. The only difference is that it attempts to preserve
560	the 'indexFormat' as encoded in the WOFF2 glyf table.
561	"""
562
563	def __init__(self, tag=None):
564		self.tableTag = Tag(tag or 'loca')
565
566	def compile(self, ttFont):
567		try:
568			max_location = max(self.locations)
569		except AttributeError:
570			self.set([])
571			max_location = 0
572		if 'glyf' in ttFont and hasattr(ttFont['glyf'], 'indexFormat'):
573			# copile loca using the indexFormat specified in the WOFF2 glyf table
574			indexFormat = ttFont['glyf'].indexFormat
575			if indexFormat == 0:
576				if max_location >= 0x20000:
577					raise TTLibError("indexFormat is 0 but local offsets > 0x20000")
578				if not all(l % 2 == 0 for l in self.locations):
579					raise TTLibError("indexFormat is 0 but local offsets not multiples of 2")
580				locations = array.array("H")
581				for i in range(len(self.locations)):
582					locations.append(self.locations[i] // 2)
583			else:
584				locations = array.array("I", self.locations)
585			if sys.byteorder != "big": locations.byteswap()
586			data = locations.tostring()
587		else:
588			# use the most compact indexFormat given the current glyph offsets
589			data = super(WOFF2LocaTable, self).compile(ttFont)
590		return data
591
592
593class WOFF2GlyfTable(getTableClass('glyf')):
594	"""Decoder/Encoder for WOFF2 'glyf' table transform."""
595
596	subStreams = (
597		'nContourStream', 'nPointsStream', 'flagStream', 'glyphStream',
598		'compositeStream', 'bboxStream', 'instructionStream')
599
600	def __init__(self, tag=None):
601		self.tableTag = Tag(tag or 'glyf')
602
603	def reconstruct(self, data, ttFont):
604		""" Decompile transformed 'glyf' data. """
605		inputDataSize = len(data)
606
607		if inputDataSize < woff2GlyfTableFormatSize:
608			raise TTLibError("not enough 'glyf' data")
609		dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self)
610		offset = woff2GlyfTableFormatSize
611
612		for stream in self.subStreams:
613			size = getattr(self, stream + 'Size')
614			setattr(self, stream, data[:size])
615			data = data[size:]
616			offset += size
617
618		if offset != inputDataSize:
619			raise TTLibError(
620				"incorrect size of transformed 'glyf' table: expected %d, received %d bytes"
621				% (offset, inputDataSize))
622
623		bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
624		bboxBitmap = self.bboxStream[:bboxBitmapSize]
625		self.bboxBitmap = array.array('B', bboxBitmap)
626		self.bboxStream = self.bboxStream[bboxBitmapSize:]
627
628		self.nContourStream = array.array("h", self.nContourStream)
629		if sys.byteorder != "big": self.nContourStream.byteswap()
630		assert len(self.nContourStream) == self.numGlyphs
631
632		if 'head' in ttFont:
633			ttFont['head'].indexToLocFormat = self.indexFormat
634		try:
635			self.glyphOrder = ttFont.getGlyphOrder()
636		except:
637			self.glyphOrder = None
638		if self.glyphOrder is None:
639			self.glyphOrder = [".notdef"]
640			self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)])
641		else:
642			if len(self.glyphOrder) != self.numGlyphs:
643				raise TTLibError(
644					"incorrect glyphOrder: expected %d glyphs, found %d" %
645					(len(self.glyphOrder), self.numGlyphs))
646
647		glyphs = self.glyphs = {}
648		for glyphID, glyphName in enumerate(self.glyphOrder):
649			glyph = self._decodeGlyph(glyphID)
650			glyphs[glyphName] = glyph
651
652	def transform(self, ttFont):
653		""" Return transformed 'glyf' data """
654		self.numGlyphs = len(self.glyphs)
655		if not hasattr(self, "glyphOrder"):
656			try:
657				self.glyphOrder = ttFont.getGlyphOrder()
658			except:
659				self.glyphOrder = None
660			if self.glyphOrder is None:
661				self.glyphOrder = [".notdef"]
662				self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)])
663		if len(self.glyphOrder) != self.numGlyphs:
664			raise TTLibError(
665				"incorrect glyphOrder: expected %d glyphs, found %d" %
666				(len(self.glyphOrder), self.numGlyphs))
667
668		if 'maxp' in ttFont:
669			ttFont['maxp'].numGlyphs = self.numGlyphs
670		self.indexFormat = ttFont['head'].indexToLocFormat
671
672		for stream in self.subStreams:
673			setattr(self, stream, b"")
674		bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
675		self.bboxBitmap = array.array('B', [0]*bboxBitmapSize)
676
677		for glyphID in range(self.numGlyphs):
678			self._encodeGlyph(glyphID)
679
680		self.bboxStream = self.bboxBitmap.tostring() + self.bboxStream
681		for stream in self.subStreams:
682			setattr(self, stream + 'Size', len(getattr(self, stream)))
683		self.version = 0
684		data = sstruct.pack(woff2GlyfTableFormat, self)
685		data += bytesjoin([getattr(self, s) for s in self.subStreams])
686		return data
687
688	def _decodeGlyph(self, glyphID):
689		glyph = getTableModule('glyf').Glyph()
690		glyph.numberOfContours = self.nContourStream[glyphID]
691		if glyph.numberOfContours == 0:
692			return glyph
693		elif glyph.isComposite():
694			self._decodeComponents(glyph)
695		else:
696			self._decodeCoordinates(glyph)
697		self._decodeBBox(glyphID, glyph)
698		return glyph
699
700	def _decodeComponents(self, glyph):
701		data = self.compositeStream
702		glyph.components = []
703		more = 1
704		haveInstructions = 0
705		while more:
706			component = getTableModule('glyf').GlyphComponent()
707			more, haveInstr, data = component.decompile(data, self)
708			haveInstructions = haveInstructions | haveInstr
709			glyph.components.append(component)
710		self.compositeStream = data
711		if haveInstructions:
712			self._decodeInstructions(glyph)
713
714	def _decodeCoordinates(self, glyph):
715		data = self.nPointsStream
716		endPtsOfContours = []
717		endPoint = -1
718		for i in range(glyph.numberOfContours):
719			ptsOfContour, data = unpack255UShort(data)
720			endPoint += ptsOfContour
721			endPtsOfContours.append(endPoint)
722		glyph.endPtsOfContours = endPtsOfContours
723		self.nPointsStream = data
724		self._decodeTriplets(glyph)
725		self._decodeInstructions(glyph)
726
727	def _decodeInstructions(self, glyph):
728		glyphStream = self.glyphStream
729		instructionStream = self.instructionStream
730		instructionLength, glyphStream = unpack255UShort(glyphStream)
731		glyph.program = ttProgram.Program()
732		glyph.program.fromBytecode(instructionStream[:instructionLength])
733		self.glyphStream = glyphStream
734		self.instructionStream = instructionStream[instructionLength:]
735
736	def _decodeBBox(self, glyphID, glyph):
737		haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7)))
738		if glyph.isComposite() and not haveBBox:
739			raise TTLibError('no bbox values for composite glyph %d' % glyphID)
740		if haveBBox:
741			dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph)
742		else:
743			glyph.recalcBounds(self)
744
745	def _decodeTriplets(self, glyph):
746
747		def withSign(flag, baseval):
748			assert 0 <= baseval and baseval < 65536, 'integer overflow'
749			return baseval if flag & 1 else -baseval
750
751		nPoints = glyph.endPtsOfContours[-1] + 1
752		flagSize = nPoints
753		if flagSize > len(self.flagStream):
754			raise TTLibError("not enough 'flagStream' data")
755		flagsData = self.flagStream[:flagSize]
756		self.flagStream = self.flagStream[flagSize:]
757		flags = array.array('B', flagsData)
758
759		triplets = array.array('B', self.glyphStream)
760		nTriplets = len(triplets)
761		assert nPoints <= nTriplets
762
763		x = 0
764		y = 0
765		glyph.coordinates = getTableModule('glyf').GlyphCoordinates.zeros(nPoints)
766		glyph.flags = array.array("B")
767		tripletIndex = 0
768		for i in range(nPoints):
769			flag = flags[i]
770			onCurve = not bool(flag >> 7)
771			flag &= 0x7f
772			if flag < 84:
773				nBytes = 1
774			elif flag < 120:
775				nBytes = 2
776			elif flag < 124:
777				nBytes = 3
778			else:
779				nBytes = 4
780			assert ((tripletIndex + nBytes) <= nTriplets)
781			if flag < 10:
782				dx = 0
783				dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex])
784			elif flag < 20:
785				dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex])
786				dy = 0
787			elif flag < 84:
788				b0 = flag - 20
789				b1 = triplets[tripletIndex]
790				dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4))
791				dy = withSign(flag >> 1, 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f))
792			elif flag < 120:
793				b0 = flag - 84
794				dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex])
795				dy = withSign(flag >> 1,
796					1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1])
797			elif flag < 124:
798				b2 = triplets[tripletIndex + 1]
799				dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4))
800				dy = withSign(flag >> 1,
801					((b2 & 0x0f) << 8) + triplets[tripletIndex + 2])
802			else:
803				dx = withSign(flag,
804					(triplets[tripletIndex] << 8) + triplets[tripletIndex + 1])
805				dy = withSign(flag >> 1,
806					(triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3])
807			tripletIndex += nBytes
808			x += dx
809			y += dy
810			glyph.coordinates[i] = (x, y)
811			glyph.flags.append(int(onCurve))
812		bytesConsumed = tripletIndex
813		self.glyphStream = self.glyphStream[bytesConsumed:]
814
815	def _encodeGlyph(self, glyphID):
816		glyphName = self.getGlyphName(glyphID)
817		glyph = self[glyphName]
818		self.nContourStream += struct.pack(">h", glyph.numberOfContours)
819		if glyph.numberOfContours == 0:
820			return
821		elif glyph.isComposite():
822			self._encodeComponents(glyph)
823		else:
824			self._encodeCoordinates(glyph)
825		self._encodeBBox(glyphID, glyph)
826
827	def _encodeComponents(self, glyph):
828		lastcomponent = len(glyph.components) - 1
829		more = 1
830		haveInstructions = 0
831		for i in range(len(glyph.components)):
832			if i == lastcomponent:
833				haveInstructions = hasattr(glyph, "program")
834				more = 0
835			component = glyph.components[i]
836			self.compositeStream += component.compile(more, haveInstructions, self)
837		if haveInstructions:
838			self._encodeInstructions(glyph)
839
840	def _encodeCoordinates(self, glyph):
841		lastEndPoint = -1
842		for endPoint in glyph.endPtsOfContours:
843			ptsOfContour = endPoint - lastEndPoint
844			self.nPointsStream += pack255UShort(ptsOfContour)
845			lastEndPoint = endPoint
846		self._encodeTriplets(glyph)
847		self._encodeInstructions(glyph)
848
849	def _encodeInstructions(self, glyph):
850		instructions = glyph.program.getBytecode()
851		self.glyphStream += pack255UShort(len(instructions))
852		self.instructionStream += instructions
853
854	def _encodeBBox(self, glyphID, glyph):
855		assert glyph.numberOfContours != 0, "empty glyph has no bbox"
856		if not glyph.isComposite():
857			# for simple glyphs, compare the encoded bounding box info with the calculated
858			# values, and if they match omit the bounding box info
859			currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax
860			calculatedBBox = calcIntBounds(glyph.coordinates)
861			if currentBBox == calculatedBBox:
862				return
863		self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7)
864		self.bboxStream += sstruct.pack(bboxFormat, glyph)
865
866	def _encodeTriplets(self, glyph):
867		assert len(glyph.coordinates) == len(glyph.flags)
868		coordinates = glyph.coordinates.copy()
869		coordinates.absoluteToRelative()
870
871		flags = array.array('B')
872		triplets = array.array('B')
873		for i in range(len(coordinates)):
874			onCurve = glyph.flags[i]
875			x, y = coordinates[i]
876			absX = abs(x)
877			absY = abs(y)
878			onCurveBit = 0 if onCurve else 128
879			xSignBit = 0 if (x < 0) else 1
880			ySignBit = 0 if (y < 0) else 1
881			xySignBits = xSignBit + 2 * ySignBit
882
883			if x == 0 and absY < 1280:
884				flags.append(onCurveBit + ((absY & 0xf00) >> 7) + ySignBit)
885				triplets.append(absY & 0xff)
886			elif y == 0 and absX < 1280:
887				flags.append(onCurveBit + 10 + ((absX & 0xf00) >> 7) + xSignBit)
888				triplets.append(absX & 0xff)
889			elif absX < 65 and absY < 65:
890				flags.append(onCurveBit + 20 + ((absX - 1) & 0x30) + (((absY - 1) & 0x30) >> 2) + xySignBits)
891				triplets.append((((absX - 1) & 0xf) << 4) | ((absY - 1) & 0xf))
892			elif absX < 769 and absY < 769:
893				flags.append(onCurveBit + 84 + 12 * (((absX - 1) & 0x300) >> 8) + (((absY - 1) & 0x300) >> 6) + xySignBits)
894				triplets.append((absX - 1) & 0xff)
895				triplets.append((absY - 1) & 0xff)
896			elif absX < 4096 and absY < 4096:
897				flags.append(onCurveBit + 120 + xySignBits)
898				triplets.append(absX >> 4)
899				triplets.append(((absX & 0xf) << 4) | (absY >> 8))
900				triplets.append(absY & 0xff)
901			else:
902				flags.append(onCurveBit + 124 + xySignBits)
903				triplets.append(absX >> 8)
904				triplets.append(absX & 0xff)
905				triplets.append(absY >> 8)
906				triplets.append(absY & 0xff)
907
908		self.flagStream += flags.tostring()
909		self.glyphStream += triplets.tostring()
910
911
912class WOFF2FlavorData(WOFFFlavorData):
913
914	Flavor = 'woff2'
915
916	def __init__(self, reader=None):
917		if not haveBrotli:
918			raise ImportError("No module named brotli")
919		self.majorVersion = None
920		self.minorVersion = None
921		self.metaData = None
922		self.privData = None
923		if reader:
924			self.majorVersion = reader.majorVersion
925			self.minorVersion = reader.minorVersion
926			if reader.metaLength:
927				reader.file.seek(reader.metaOffset)
928				rawData = reader.file.read(reader.metaLength)
929				assert len(rawData) == reader.metaLength
930				data = brotli.decompress(rawData)
931				assert len(data) == reader.metaOrigLength
932				self.metaData = data
933			if reader.privLength:
934				reader.file.seek(reader.privOffset)
935				data = reader.file.read(reader.privLength)
936				assert len(data) == reader.privLength
937				self.privData = data
938
939
940def unpackBase128(data):
941	r""" Read one to five bytes from UIntBase128-encoded input string, and return
942	a tuple containing the decoded integer plus any leftover data.
943
944	>>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00")
945	True
946	>>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295
947	True
948	>>> unpackBase128(b'\x80\x80\x3f')  # doctest: +IGNORE_EXCEPTION_DETAIL
949	Traceback (most recent call last):
950	  File "<stdin>", line 1, in ?
951	TTLibError: UIntBase128 value must not start with leading zeros
952	>>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0]  # doctest: +IGNORE_EXCEPTION_DETAIL
953	Traceback (most recent call last):
954	  File "<stdin>", line 1, in ?
955	TTLibError: UIntBase128-encoded sequence is longer than 5 bytes
956	>>> unpackBase128(b'\x90\x80\x80\x80\x00')[0]  # doctest: +IGNORE_EXCEPTION_DETAIL
957	Traceback (most recent call last):
958	  File "<stdin>", line 1, in ?
959	TTLibError: UIntBase128 value exceeds 2**32-1
960	"""
961	if len(data) == 0:
962		raise TTLibError('not enough data to unpack UIntBase128')
963	result = 0
964	if byteord(data[0]) == 0x80:
965		# font must be rejected if UIntBase128 value starts with 0x80
966		raise TTLibError('UIntBase128 value must not start with leading zeros')
967	for i in range(woff2Base128MaxSize):
968		if len(data) == 0:
969			raise TTLibError('not enough data to unpack UIntBase128')
970		code = byteord(data[0])
971		data = data[1:]
972		# if any of the top seven bits are set then we're about to overflow
973		if result & 0xFE000000:
974			raise TTLibError('UIntBase128 value exceeds 2**32-1')
975		# set current value = old value times 128 bitwise-or (byte bitwise-and 127)
976		result = (result << 7) | (code & 0x7f)
977		# repeat until the most significant bit of byte is false
978		if (code & 0x80) == 0:
979			# return result plus left over data
980			return result, data
981	# make sure not to exceed the size bound
982	raise TTLibError('UIntBase128-encoded sequence is longer than 5 bytes')
983
984
985def base128Size(n):
986	""" Return the length in bytes of a UIntBase128-encoded sequence with value n.
987
988	>>> base128Size(0)
989	1
990	>>> base128Size(24567)
991	3
992	>>> base128Size(2**32-1)
993	5
994	"""
995	assert n >= 0
996	size = 1
997	while n >= 128:
998		size += 1
999		n >>= 7
1000	return size
1001
1002
1003def packBase128(n):
1004	r""" Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of
1005	bytes using UIntBase128 variable-length encoding. Produce the shortest possible
1006	encoding.
1007
1008	>>> packBase128(63) == b"\x3f"
1009	True
1010	>>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f'
1011	True
1012	"""
1013	if n < 0 or n >= 2**32:
1014		raise TTLibError(
1015			"UIntBase128 format requires 0 <= integer <= 2**32-1")
1016	data = b''
1017	size = base128Size(n)
1018	for i in range(size):
1019		b = (n >> (7 * (size - i - 1))) & 0x7f
1020		if i < size - 1:
1021			b |= 0x80
1022		data += struct.pack('B', b)
1023	return data
1024
1025
1026def unpack255UShort(data):
1027	""" Read one to three bytes from 255UInt16-encoded input string, and return a
1028	tuple containing the decoded integer plus any leftover data.
1029
1030	>>> unpack255UShort(bytechr(252))[0]
1031	252
1032
1033	Note that some numbers (e.g. 506) can have multiple encodings:
1034	>>> unpack255UShort(struct.pack("BB", 254, 0))[0]
1035	506
1036	>>> unpack255UShort(struct.pack("BB", 255, 253))[0]
1037	506
1038	>>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0]
1039	506
1040	"""
1041	code = byteord(data[:1])
1042	data = data[1:]
1043	if code == 253:
1044		# read two more bytes as an unsigned short
1045		if len(data) < 2:
1046			raise TTLibError('not enough data to unpack 255UInt16')
1047		result, = struct.unpack(">H", data[:2])
1048		data = data[2:]
1049	elif code == 254:
1050		# read another byte, plus 253 * 2
1051		if len(data) == 0:
1052			raise TTLibError('not enough data to unpack 255UInt16')
1053		result = byteord(data[:1])
1054		result += 506
1055		data = data[1:]
1056	elif code == 255:
1057		# read another byte, plus 253
1058		if len(data) == 0:
1059			raise TTLibError('not enough data to unpack 255UInt16')
1060		result = byteord(data[:1])
1061		result += 253
1062		data = data[1:]
1063	else:
1064		# leave as is if lower than 253
1065		result = code
1066	# return result plus left over data
1067	return result, data
1068
1069
1070def pack255UShort(value):
1071	r""" Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring
1072	using 255UInt16 variable-length encoding.
1073
1074	>>> pack255UShort(252) == b'\xfc'
1075	True
1076	>>> pack255UShort(506) == b'\xfe\x00'
1077	True
1078	>>> pack255UShort(762) == b'\xfd\x02\xfa'
1079	True
1080	"""
1081	if value < 0 or value > 0xFFFF:
1082		raise TTLibError(
1083			"255UInt16 format requires 0 <= integer <= 65535")
1084	if value < 253:
1085		return struct.pack(">B", value)
1086	elif value < 506:
1087		return struct.pack(">BB", 255, value - 253)
1088	elif value < 762:
1089		return struct.pack(">BB", 254, value - 506)
1090	else:
1091		return struct.pack(">BH", 253, value)
1092
1093
1094if __name__ == "__main__":
1095	import doctest
1096	sys.exit(doctest.testmod().failed)
1097