• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.config import OPTIONS
2from fontTools.misc.textTools import Tag, bytesjoin
3from .DefaultTable import DefaultTable
4from enum import IntEnum
5import sys
6import array
7import struct
8import logging
9from functools import lru_cache
10from typing import Iterator, NamedTuple, Optional, Tuple
11
12log = logging.getLogger(__name__)
13
14have_uharfbuzz = False
15try:
16	import uharfbuzz as hb
17	# repack method added in uharfbuzz >= 0.23; if uharfbuzz *can* be
18	# imported but repack method is missing, behave as if uharfbuzz
19	# is not available (fallback to the slower Python implementation)
20	have_uharfbuzz = callable(getattr(hb, "repack", None))
21except ImportError:
22	pass
23
24USE_HARFBUZZ_REPACKER = OPTIONS[f"{__name__}:USE_HARFBUZZ_REPACKER"]
25
26class OverflowErrorRecord(object):
27	def __init__(self, overflowTuple):
28		self.tableType = overflowTuple[0]
29		self.LookupListIndex = overflowTuple[1]
30		self.SubTableIndex = overflowTuple[2]
31		self.itemName = overflowTuple[3]
32		self.itemIndex = overflowTuple[4]
33
34	def __repr__(self):
35		return str((self.tableType, "LookupIndex:", self.LookupListIndex, "SubTableIndex:", self.SubTableIndex, "ItemName:", self.itemName, "ItemIndex:", self.itemIndex))
36
37class OTLOffsetOverflowError(Exception):
38	def __init__(self, overflowErrorRecord):
39		self.value = overflowErrorRecord
40
41	def __str__(self):
42		return repr(self.value)
43
44class RepackerState(IntEnum):
45	# Repacking control flow is implemnted using a state machine. The state machine table:
46	#
47	# State       | Packing Success | Packing Failed | Exception Raised |
48	# ------------+-----------------+----------------+------------------+
49	# PURE_FT     | Return result   | PURE_FT        | Return failure   |
50	# HB_FT       | Return result   | HB_FT          | FT_FALLBACK      |
51	# FT_FALLBACK | HB_FT           | FT_FALLBACK    | Return failure   |
52
53	# Pack only with fontTools, don't allow sharing between extensions.
54	PURE_FT = 1
55
56	# Attempt to pack with harfbuzz (allowing sharing between extensions)
57	# use fontTools to attempt overflow resolution.
58	HB_FT = 2
59
60	# Fallback if HB/FT packing gets stuck. Pack only with fontTools, don't allow sharing between
61	# extensions.
62	FT_FALLBACK = 3
63
64class BaseTTXConverter(DefaultTable):
65
66	"""Generic base class for TTX table converters. It functions as an
67	adapter between the TTX (ttLib actually) table model and the model
68	we use for OpenType tables, which is necessarily subtly different.
69	"""
70
71	def decompile(self, data, font):
72		"""Create an object from the binary data. Called automatically on access."""
73		from . import otTables
74		reader = OTTableReader(data, tableTag=self.tableTag)
75		tableClass = getattr(otTables, self.tableTag)
76		self.table = tableClass()
77		self.table.decompile(reader, font)
78
79	def compile(self, font):
80		"""Compiles the table into binary. Called automatically on save."""
81
82		# General outline:
83		# Create a top-level OTTableWriter for the GPOS/GSUB table.
84		# 	Call the compile method for the the table
85		# 		for each 'converter' record in the table converter list
86		# 			call converter's write method for each item in the value.
87		# 				- For simple items, the write method adds a string to the
88		# 				writer's self.items list.
89		# 				- For Struct/Table/Subtable items, it add first adds new writer to the
90		# 				to the writer's self.items, then calls the item's compile method.
91		# 				This creates a tree of writers, rooted at the GUSB/GPOS writer, with
92		# 				each writer representing a table, and the writer.items list containing
93		# 				the child data strings and writers.
94		# 	call the getAllData method
95		# 		call _doneWriting, which removes duplicates
96		# 		call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables
97		# 		Traverse the flat list of tables, calling getDataLength on each to update their position
98		# 		Traverse the flat list of tables again, calling getData each get the data in the table, now that
99		# 		pos's and offset are known.
100
101		# 		If a lookup subtable overflows an offset, we have to start all over.
102		overflowRecord = None
103		# this is 3-state option: default (None) means automatically use hb.repack or
104		# silently fall back if it fails; True, use it and raise error if not possible
105		# or it errors out; False, don't use it, even if you can.
106		use_hb_repack = font.cfg[USE_HARFBUZZ_REPACKER]
107		if self.tableTag in ("GSUB", "GPOS"):
108			if use_hb_repack is False:
109				log.debug(
110					"hb.repack disabled, compiling '%s' with pure-python serializer",
111					self.tableTag,
112				)
113			elif not have_uharfbuzz:
114				if use_hb_repack is True:
115					raise ImportError("No module named 'uharfbuzz'")
116				else:
117					assert use_hb_repack is None
118					log.debug(
119						"uharfbuzz not found, compiling '%s' with pure-python serializer",
120						self.tableTag,
121					)
122
123		if (use_hb_repack in (None, True)
124				and have_uharfbuzz
125				and self.tableTag in ("GSUB", "GPOS")):
126			state = RepackerState.HB_FT
127		else:
128			state = RepackerState.PURE_FT
129
130		hb_first_error_logged = False
131		lastOverflowRecord = None
132		while True:
133			try:
134				writer = OTTableWriter(tableTag=self.tableTag)
135				self.table.compile(writer, font)
136				if state == RepackerState.HB_FT:
137					return self.tryPackingHarfbuzz(writer, hb_first_error_logged)
138				elif state == RepackerState.PURE_FT:
139					return self.tryPackingFontTools(writer)
140				elif state == RepackerState.FT_FALLBACK:
141					# Run packing with FontTools only, but don't return the result as it will
142					# not be optimally packed. Once a successful packing has been found, state is
143					# changed back to harfbuzz packing to produce the final, optimal, packing.
144					self.tryPackingFontTools(writer)
145					log.debug("Re-enabling sharing between extensions and switching back to "
146										"harfbuzz+fontTools packing.")
147					state = RepackerState.HB_FT
148
149			except OTLOffsetOverflowError as e:
150				hb_first_error_logged = True
151				ok = self.tryResolveOverflow(font, e, lastOverflowRecord)
152				lastOverflowRecord = e.value
153
154				if ok:
155					continue
156
157				if state is RepackerState.HB_FT:
158					log.debug("Harfbuzz packing out of resolutions, disabling sharing between extensions and "
159									 "switching to fontTools only packing.")
160					state = RepackerState.FT_FALLBACK
161				else:
162					raise
163
164	def tryPackingHarfbuzz(self, writer, hb_first_error_logged):
165		try:
166			log.debug("serializing '%s' with hb.repack", self.tableTag)
167			return writer.getAllDataUsingHarfbuzz(self.tableTag)
168		except (ValueError, MemoryError, hb.RepackerError) as e:
169			# Only log hb repacker errors the first time they occur in
170			# the offset-overflow resolution loop, they are just noisy.
171			# Maybe we can revisit this if/when uharfbuzz actually gives
172			# us more info as to why hb.repack failed...
173			if not hb_first_error_logged:
174				error_msg = f"{type(e).__name__}"
175				if str(e) != "":
176					error_msg += f": {e}"
177				log.warning(
178						"hb.repack failed to serialize '%s', attempting fonttools resolutions "
179						"; the error message was: %s",
180						self.tableTag,
181						error_msg,
182				)
183				hb_first_error_logged = True
184			return writer.getAllData(remove_duplicate=False)
185
186
187	def tryPackingFontTools(self, writer):
188		return writer.getAllData()
189
190
191	def tryResolveOverflow(self, font, e, lastOverflowRecord):
192		ok = 0
193		if lastOverflowRecord == e.value:
194			# Oh well...
195			return ok
196
197		overflowRecord = e.value
198		log.info("Attempting to fix OTLOffsetOverflowError %s", e)
199
200		if overflowRecord.itemName is None:
201			from .otTables import fixLookupOverFlows
202			ok = fixLookupOverFlows(font, overflowRecord)
203		else:
204			from .otTables import fixSubTableOverFlows
205			ok = fixSubTableOverFlows(font, overflowRecord)
206
207		if ok:
208			return ok
209
210		# Try upgrading lookup to Extension and hope
211		# that cross-lookup sharing not happening would
212		# fix overflow...
213		from .otTables import fixLookupOverFlows
214		return fixLookupOverFlows(font, overflowRecord)
215
216	def toXML(self, writer, font):
217		self.table.toXML2(writer, font)
218
219	def fromXML(self, name, attrs, content, font):
220		from . import otTables
221		if not hasattr(self, "table"):
222			tableClass = getattr(otTables, self.tableTag)
223			self.table = tableClass()
224		self.table.fromXML(name, attrs, content, font)
225		self.table.populateDefaults()
226
227	def ensureDecompiled(self, recurse=True):
228		self.table.ensureDecompiled(recurse=recurse)
229
230
231# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928
232assert len(struct.pack('i', 0)) == 4
233assert array.array('i').itemsize == 4, "Oops, file a bug against fonttools."
234
235class OTTableReader(object):
236
237	"""Helper class to retrieve data from an OpenType table."""
238
239	__slots__ = ('data', 'offset', 'pos', 'localState', 'tableTag')
240
241	def __init__(self, data, localState=None, offset=0, tableTag=None):
242		self.data = data
243		self.offset = offset
244		self.pos = offset
245		self.localState = localState
246		self.tableTag = tableTag
247
248	def advance(self, count):
249		self.pos += count
250
251	def seek(self, pos):
252		self.pos = pos
253
254	def copy(self):
255		other = self.__class__(self.data, self.localState, self.offset, self.tableTag)
256		other.pos = self.pos
257		return other
258
259	def getSubReader(self, offset):
260		offset = self.offset + offset
261		return self.__class__(self.data, self.localState, offset, self.tableTag)
262
263	def readValue(self, typecode, staticSize):
264		pos = self.pos
265		newpos = pos + staticSize
266		value, = struct.unpack(f">{typecode}", self.data[pos:newpos])
267		self.pos = newpos
268		return value
269	def readArray(self, typecode, staticSize, count):
270		pos = self.pos
271		newpos = pos + count * staticSize
272		value = array.array(typecode, self.data[pos:newpos])
273		if sys.byteorder != "big": value.byteswap()
274		self.pos = newpos
275		return value.tolist()
276
277	def readInt8(self):
278		return self.readValue("b", staticSize=1)
279	def readInt8Array(self, count):
280		return self.readArray("b", staticSize=1, count=count)
281
282	def readShort(self):
283		return self.readValue("h", staticSize=2)
284	def readShortArray(self, count):
285		return self.readArray("h", staticSize=2, count=count)
286
287	def readLong(self):
288		return self.readValue("i", staticSize=4)
289	def readLongArray(self, count):
290		return self.readArray("i", staticSize=4, count=count)
291
292	def readUInt8(self):
293		return self.readValue("B", staticSize=1)
294	def readUInt8Array(self, count):
295		return self.readArray("B", staticSize=1, count=count)
296
297	def readUShort(self):
298		return self.readValue("H", staticSize=2)
299	def readUShortArray(self, count):
300		return self.readArray("H", staticSize=2, count=count)
301
302	def readULong(self):
303		return self.readValue("I", staticSize=4)
304	def readULongArray(self, count):
305		return self.readArray("I", staticSize=4, count=count)
306
307	def readUInt24(self):
308		pos = self.pos
309		newpos = pos + 3
310		value, = struct.unpack(">l", b'\0'+self.data[pos:newpos])
311		self.pos = newpos
312		return value
313	def readUInt24Array(self, count):
314		return [self.readUInt24() for _ in range(count)]
315
316	def readTag(self):
317		pos = self.pos
318		newpos = pos + 4
319		value = Tag(self.data[pos:newpos])
320		assert len(value) == 4, value
321		self.pos = newpos
322		return value
323
324	def readData(self, count):
325		pos = self.pos
326		newpos = pos + count
327		value = self.data[pos:newpos]
328		self.pos = newpos
329		return value
330
331	def __setitem__(self, name, value):
332		state = self.localState.copy() if self.localState else dict()
333		state[name] = value
334		self.localState = state
335
336	def __getitem__(self, name):
337		return self.localState and self.localState[name]
338
339	def __contains__(self, name):
340		return self.localState and name in self.localState
341
342
343class OTTableWriter(object):
344
345	"""Helper class to gather and assemble data for OpenType tables."""
346
347	def __init__(self, localState=None, tableTag=None, offsetSize=2):
348		self.items = []
349		self.pos = None
350		self.localState = localState
351		self.tableTag = tableTag
352		self.offsetSize = offsetSize
353		self.parent = None
354
355	# DEPRECATED: 'longOffset' is kept as a property for backward compat with old code.
356	# You should use 'offsetSize' instead (2, 3 or 4 bytes).
357	@property
358	def longOffset(self):
359		return self.offsetSize == 4
360
361	@longOffset.setter
362	def longOffset(self, value):
363		self.offsetSize = 4 if value else 2
364
365	def __setitem__(self, name, value):
366		state = self.localState.copy() if self.localState else dict()
367		state[name] = value
368		self.localState = state
369
370	def __getitem__(self, name):
371		return self.localState[name]
372
373	def __delitem__(self, name):
374		del self.localState[name]
375
376	# assembler interface
377
378	def getDataLength(self):
379		"""Return the length of this table in bytes, without subtables."""
380		l = 0
381		for item in self.items:
382			if hasattr(item, "getCountData"):
383				l += item.size
384			elif hasattr(item, "getData"):
385				l += item.offsetSize
386			else:
387				l = l + len(item)
388		return l
389
390	def getData(self):
391		"""Assemble the data for this writer/table, without subtables."""
392		items = list(self.items)  # make a shallow copy
393		pos = self.pos
394		numItems = len(items)
395		for i in range(numItems):
396			item = items[i]
397
398			if hasattr(item, "getData"):
399				if item.offsetSize == 4:
400					items[i] = packULong(item.pos - pos)
401				elif item.offsetSize == 2:
402					try:
403						items[i] = packUShort(item.pos - pos)
404					except struct.error:
405						# provide data to fix overflow problem.
406						overflowErrorRecord = self.getOverflowErrorRecord(item)
407
408						raise OTLOffsetOverflowError(overflowErrorRecord)
409				elif item.offsetSize == 3:
410					items[i] = packUInt24(item.pos - pos)
411				else:
412					raise ValueError(item.offsetSize)
413
414		return bytesjoin(items)
415
416	def getDataForHarfbuzz(self):
417		"""Assemble the data for this writer/table with all offset field set to 0"""
418		items = list(self.items)
419		packFuncs = {2: packUShort, 3: packUInt24, 4: packULong}
420		for i, item in enumerate(items):
421			if hasattr(item, "getData"):
422				# Offset value is not needed in harfbuzz repacker, so setting offset to 0 to avoid overflow here
423				if item.offsetSize in packFuncs:
424					items[i] = packFuncs[item.offsetSize](0)
425				else:
426					raise ValueError(item.offsetSize)
427
428		return bytesjoin(items)
429
430	def __hash__(self):
431		# only works after self._doneWriting() has been called
432		return hash(self.items)
433
434	def __ne__(self, other):
435		result = self.__eq__(other)
436		return result if result is NotImplemented else not result
437
438	def __eq__(self, other):
439		if type(self) != type(other):
440			return NotImplemented
441		return self.offsetSize == other.offsetSize and self.items == other.items
442
443	def _doneWriting(self, internedTables, shareExtension=False):
444		# Convert CountData references to data string items
445		# collapse duplicate table references to a unique entry
446		# "tables" are OTTableWriter objects.
447
448		# For Extension Lookup types, we can
449		# eliminate duplicates only within the tree under the Extension Lookup,
450		# as offsets may exceed 64K even between Extension LookupTable subtables.
451		isExtension = hasattr(self, "Extension")
452
453		# Certain versions of Uniscribe reject the font if the GSUB/GPOS top-level
454		# arrays (ScriptList, FeatureList, LookupList) point to the same, possibly
455		# empty, array.  So, we don't share those.
456		# See: https://github.com/fonttools/fonttools/issues/518
457		dontShare = hasattr(self, 'DontShare')
458
459		if isExtension and not shareExtension:
460			internedTables = {}
461
462		items = self.items
463		for i in range(len(items)):
464			item = items[i]
465			if hasattr(item, "getCountData"):
466				items[i] = item.getCountData()
467			elif hasattr(item, "getData"):
468				item._doneWriting(internedTables, shareExtension=shareExtension)
469				# At this point, all subwriters are hashable based on their items.
470				# (See hash and comparison magic methods above.) So the ``setdefault``
471				# call here will return the first writer object we've seen with
472				# equal content, or store it in the dictionary if it's not been
473				# seen yet. We therefore replace the subwriter object with an equivalent
474				# object, which deduplicates the tree.
475				if not dontShare:
476					items[i] = item = internedTables.setdefault(item, item)
477		self.items = tuple(items)
478
479	def _gatherTables(self, tables, extTables, done):
480		# Convert table references in self.items tree to a flat
481		# list of tables in depth-first traversal order.
482		# "tables" are OTTableWriter objects.
483		# We do the traversal in reverse order at each level, in order to
484		# resolve duplicate references to be the last reference in the list of tables.
485		# For extension lookups, duplicate references can be merged only within the
486		# writer tree under the  extension lookup.
487
488		done[id(self)] = True
489
490		numItems = len(self.items)
491		iRange = list(range(numItems))
492		iRange.reverse()
493
494		isExtension = hasattr(self, "Extension")
495
496		selfTables = tables
497
498		if isExtension:
499			assert extTables is not None, "Program or XML editing error. Extension subtables cannot contain extensions subtables"
500			tables, extTables, done = extTables, None, {}
501
502		# add Coverage table if it is sorted last.
503		sortCoverageLast = False
504		if hasattr(self, "sortCoverageLast"):
505			# Find coverage table
506			for i in range(numItems):
507				item = self.items[i]
508				if getattr(item, 'name', None) == "Coverage":
509					sortCoverageLast = True
510					break
511			if id(item) not in done:
512				item._gatherTables(tables, extTables, done)
513			else:
514				# We're a new parent of item
515				pass
516
517		for i in iRange:
518			item = self.items[i]
519			if not hasattr(item, "getData"):
520				continue
521
522			if sortCoverageLast and (i==1) and getattr(item, 'name', None) == 'Coverage':
523				# we've already 'gathered' it above
524				continue
525
526			if id(item) not in done:
527				item._gatherTables(tables, extTables, done)
528			else:
529				# Item is already written out by other parent
530				pass
531
532		selfTables.append(self)
533
534	def _gatherGraphForHarfbuzz(self, tables, obj_list, done, objidx, virtual_edges):
535		real_links = []
536		virtual_links = []
537		item_idx = objidx
538
539		# Merge virtual_links from parent
540		for idx in virtual_edges:
541			virtual_links.append((0, 0, idx))
542
543		sortCoverageLast = False
544		coverage_idx = 0
545		if hasattr(self, "sortCoverageLast"):
546			# Find coverage table
547			for i, item in enumerate(self.items):
548				if getattr(item, 'name', None) == "Coverage":
549					sortCoverageLast = True
550					if id(item) not in done:
551						coverage_idx = item_idx = item._gatherGraphForHarfbuzz(tables, obj_list, done, item_idx, virtual_edges)
552					else:
553						coverage_idx = done[id(item)]
554					virtual_edges.append(coverage_idx)
555					break
556
557		child_idx = 0
558		offset_pos = 0
559		for i, item in enumerate(self.items):
560			if hasattr(item, "getData"):
561				pos = offset_pos
562			elif hasattr(item, "getCountData"):
563				offset_pos += item.size
564				continue
565			else:
566				offset_pos = offset_pos + len(item)
567				continue
568
569			if id(item) not in done:
570				child_idx = item_idx = item._gatherGraphForHarfbuzz(tables, obj_list, done, item_idx, virtual_edges)
571			else:
572				child_idx = done[id(item)]
573
574			real_edge = (pos, item.offsetSize, child_idx)
575			real_links.append(real_edge)
576			offset_pos += item.offsetSize
577
578		tables.append(self)
579		obj_list.append((real_links,virtual_links))
580		item_idx += 1
581		done[id(self)] = item_idx
582		if sortCoverageLast:
583			virtual_edges.pop()
584
585		return item_idx
586
587	def getAllDataUsingHarfbuzz(self, tableTag):
588		"""The Whole table is represented as a Graph.
589                Assemble graph data and call Harfbuzz repacker to pack the table.
590                Harfbuzz repacker is faster and retain as much sub-table sharing as possible, see also:
591                https://github.com/harfbuzz/harfbuzz/blob/main/docs/repacker.md
592                The input format for hb.repack() method is explained here:
593                https://github.com/harfbuzz/uharfbuzz/blob/main/src/uharfbuzz/_harfbuzz.pyx#L1149
594                """
595		internedTables = {}
596		self._doneWriting(internedTables, shareExtension=True)
597		tables = []
598		obj_list = []
599		done = {}
600		objidx = 0
601		virtual_edges = []
602		self._gatherGraphForHarfbuzz(tables, obj_list, done, objidx, virtual_edges)
603		# Gather all data in two passes: the absolute positions of all
604		# subtable are needed before the actual data can be assembled.
605		pos = 0
606		for table in tables:
607			table.pos = pos
608			pos = pos + table.getDataLength()
609
610		data = []
611		for table in tables:
612			tableData = table.getDataForHarfbuzz()
613			data.append(tableData)
614
615		if hasattr(hb, "repack_with_tag"):
616			return hb.repack_with_tag(str(tableTag), data, obj_list)
617		else:
618			return hb.repack(data, obj_list)
619
620	def getAllData(self, remove_duplicate=True):
621		"""Assemble all data, including all subtables."""
622		if remove_duplicate:
623			internedTables = {}
624			self._doneWriting(internedTables)
625		tables = []
626		extTables = []
627		done = {}
628		self._gatherTables(tables, extTables, done)
629		tables.reverse()
630		extTables.reverse()
631		# Gather all data in two passes: the absolute positions of all
632		# subtable are needed before the actual data can be assembled.
633		pos = 0
634		for table in tables:
635			table.pos = pos
636			pos = pos + table.getDataLength()
637
638		for table in extTables:
639			table.pos = pos
640			pos = pos + table.getDataLength()
641
642		data = []
643		for table in tables:
644			tableData = table.getData()
645			data.append(tableData)
646
647		for table in extTables:
648			tableData = table.getData()
649			data.append(tableData)
650
651		return bytesjoin(data)
652
653	# interface for gathering data, as used by table.compile()
654
655	def getSubWriter(self, offsetSize=2):
656		subwriter = self.__class__(self.localState, self.tableTag, offsetSize=offsetSize)
657		subwriter.parent = self # because some subtables have idential values, we discard
658					# the duplicates under the getAllData method. Hence some
659					# subtable writers can have more than one parent writer.
660					# But we just care about first one right now.
661		return subwriter
662
663	def writeValue(self, typecode, value):
664		self.items.append(struct.pack(f">{typecode}", value))
665	def writeArray(self, typecode, values):
666		a = array.array(typecode, values)
667		if sys.byteorder != "big": a.byteswap()
668		self.items.append(a.tobytes())
669
670	def writeInt8(self, value):
671		assert -128 <= value < 128, value
672		self.items.append(struct.pack(">b", value))
673	def writeInt8Array(self, values):
674		self.writeArray('b', values)
675
676	def writeShort(self, value):
677		assert -32768 <= value < 32768, value
678		self.items.append(struct.pack(">h", value))
679	def writeShortArray(self, values):
680		self.writeArray('h', values)
681
682	def writeLong(self, value):
683		self.items.append(struct.pack(">i", value))
684	def writeLongArray(self, values):
685		self.writeArray('i', values)
686
687	def writeUInt8(self, value):
688		assert 0 <= value < 256, value
689		self.items.append(struct.pack(">B", value))
690	def writeUInt8Array(self, values):
691		self.writeArray('B', values)
692
693	def writeUShort(self, value):
694		assert 0 <= value < 0x10000, value
695		self.items.append(struct.pack(">H", value))
696	def writeUShortArray(self, values):
697		self.writeArray('H', values)
698
699	def writeULong(self, value):
700		self.items.append(struct.pack(">I", value))
701	def writeULongArray(self, values):
702		self.writeArray('I', values)
703
704	def writeUInt24(self, value):
705		assert 0 <= value < 0x1000000, value
706		b = struct.pack(">L", value)
707		self.items.append(b[1:])
708	def writeUInt24Array(self, values):
709		for value in values:
710			self.writeUInt24(value)
711
712	def writeTag(self, tag):
713		tag = Tag(tag).tobytes()
714		assert len(tag) == 4, tag
715		self.items.append(tag)
716
717	def writeSubTable(self, subWriter):
718		self.items.append(subWriter)
719
720	def writeCountReference(self, table, name, size=2, value=None):
721		ref = CountReference(table, name, size=size, value=value)
722		self.items.append(ref)
723		return ref
724
725	def writeStruct(self, format, values):
726		data = struct.pack(*(format,) + values)
727		self.items.append(data)
728
729	def writeData(self, data):
730		self.items.append(data)
731
732	def getOverflowErrorRecord(self, item):
733		LookupListIndex = SubTableIndex = itemName = itemIndex = None
734		if self.name == 'LookupList':
735			LookupListIndex = item.repeatIndex
736		elif self.name == 'Lookup':
737			LookupListIndex = self.repeatIndex
738			SubTableIndex = item.repeatIndex
739		else:
740			itemName = getattr(item, 'name', '<none>')
741			if hasattr(item, 'repeatIndex'):
742				itemIndex = item.repeatIndex
743			if self.name == 'SubTable':
744				LookupListIndex = self.parent.repeatIndex
745				SubTableIndex = self.repeatIndex
746			elif self.name == 'ExtSubTable':
747				LookupListIndex = self.parent.parent.repeatIndex
748				SubTableIndex = self.parent.repeatIndex
749			else: # who knows how far below the SubTable level we are! Climb back up to the nearest subtable.
750				itemName = ".".join([self.name, itemName])
751				p1 = self.parent
752				while p1 and p1.name not in ['ExtSubTable', 'SubTable']:
753					itemName = ".".join([p1.name, itemName])
754					p1 = p1.parent
755				if p1:
756					if p1.name == 'ExtSubTable':
757						LookupListIndex = p1.parent.parent.repeatIndex
758						SubTableIndex = p1.parent.repeatIndex
759					else:
760						LookupListIndex = p1.parent.repeatIndex
761						SubTableIndex = p1.repeatIndex
762
763		return OverflowErrorRecord( (self.tableTag, LookupListIndex, SubTableIndex, itemName, itemIndex) )
764
765
766class CountReference(object):
767	"""A reference to a Count value, not a count of references."""
768	def __init__(self, table, name, size=None, value=None):
769		self.table = table
770		self.name = name
771		self.size = size
772		if value is not None:
773			self.setValue(value)
774	def setValue(self, value):
775		table = self.table
776		name = self.name
777		if table[name] is None:
778			table[name] = value
779		else:
780			assert table[name] == value, (name, table[name], value)
781	def getValue(self):
782		return self.table[self.name]
783	def getCountData(self):
784		v = self.table[self.name]
785		if v is None: v = 0
786		return {1:packUInt8, 2:packUShort, 4:packULong}[self.size](v)
787
788
789def packUInt8 (value):
790	return struct.pack(">B", value)
791
792def packUShort(value):
793	return struct.pack(">H", value)
794
795def packULong(value):
796	assert 0 <= value < 0x100000000, value
797	return struct.pack(">I", value)
798
799def packUInt24(value):
800	assert 0 <= value < 0x1000000, value
801	return struct.pack(">I", value)[1:]
802
803
804class BaseTable(object):
805
806	"""Generic base class for all OpenType (sub)tables."""
807
808	def __getattr__(self, attr):
809		reader = self.__dict__.get("reader")
810		if reader:
811			del self.reader
812			font = self.font
813			del self.font
814			self.decompile(reader, font)
815			return getattr(self, attr)
816
817		raise AttributeError(attr)
818
819	def ensureDecompiled(self, recurse=False):
820		reader = self.__dict__.get("reader")
821		if reader:
822			del self.reader
823			font = self.font
824			del self.font
825			self.decompile(reader, font)
826		if recurse:
827			for subtable in self.iterSubTables():
828				subtable.value.ensureDecompiled(recurse)
829
830	@classmethod
831	def getRecordSize(cls, reader):
832		totalSize = 0
833		for conv in cls.converters:
834			size = conv.getRecordSize(reader)
835			if size is NotImplemented: return NotImplemented
836			countValue = 1
837			if conv.repeat:
838				if conv.repeat in reader:
839					countValue = reader[conv.repeat] + conv.aux
840				else:
841					return NotImplemented
842			totalSize += size * countValue
843		return totalSize
844
845	def getConverters(self):
846		return self.converters
847
848	def getConverterByName(self, name):
849		return self.convertersByName[name]
850
851	def populateDefaults(self, propagator=None):
852		for conv in self.getConverters():
853			if conv.repeat:
854				if not hasattr(self, conv.name):
855					setattr(self, conv.name, [])
856				countValue = len(getattr(self, conv.name)) - conv.aux
857				try:
858					count_conv = self.getConverterByName(conv.repeat)
859					setattr(self, conv.repeat, countValue)
860				except KeyError:
861					# conv.repeat is a propagated count
862					if propagator and conv.repeat in propagator:
863						propagator[conv.repeat].setValue(countValue)
864			else:
865				if conv.aux and not eval(conv.aux, None, self.__dict__):
866					continue
867				if hasattr(self, conv.name):
868					continue # Warn if it should NOT be present?!
869				if hasattr(conv, 'writeNullOffset'):
870					setattr(self, conv.name, None) # Warn?
871				#elif not conv.isCount:
872				#	# Warn?
873				#	pass
874				if hasattr(conv, "DEFAULT"):
875					# OptionalValue converters (e.g. VarIndex)
876					setattr(self, conv.name, conv.DEFAULT)
877
878	def decompile(self, reader, font):
879		self.readFormat(reader)
880		table = {}
881		self.__rawTable = table  # for debugging
882		for conv in self.getConverters():
883			if conv.name == "SubTable":
884				conv = conv.getConverter(reader.tableTag,
885						table["LookupType"])
886			if conv.name == "ExtSubTable":
887				conv = conv.getConverter(reader.tableTag,
888						table["ExtensionLookupType"])
889			if conv.name == "FeatureParams":
890				conv = conv.getConverter(reader["FeatureTag"])
891			if conv.name == "SubStruct":
892				conv = conv.getConverter(reader.tableTag,
893				                         table["MorphType"])
894			try:
895				if conv.repeat:
896					if isinstance(conv.repeat, int):
897						countValue = conv.repeat
898					elif conv.repeat in table:
899						countValue = table[conv.repeat]
900					else:
901						# conv.repeat is a propagated count
902						countValue = reader[conv.repeat]
903					countValue += conv.aux
904					table[conv.name] = conv.readArray(reader, font, table, countValue)
905				else:
906					if conv.aux and not eval(conv.aux, None, table):
907						continue
908					table[conv.name] = conv.read(reader, font, table)
909					if conv.isPropagated:
910						reader[conv.name] = table[conv.name]
911			except Exception as e:
912				name = conv.name
913				e.args = e.args + (name,)
914				raise
915
916		if hasattr(self, 'postRead'):
917			self.postRead(table, font)
918		else:
919			self.__dict__.update(table)
920
921		del self.__rawTable  # succeeded, get rid of debugging info
922
923	def compile(self, writer, font):
924		self.ensureDecompiled()
925		# TODO Following hack to be removed by rewriting how FormatSwitching tables
926		# are handled.
927		# https://github.com/fonttools/fonttools/pull/2238#issuecomment-805192631
928		if hasattr(self, 'preWrite'):
929			deleteFormat = not hasattr(self, 'Format')
930			table = self.preWrite(font)
931			deleteFormat = deleteFormat and hasattr(self, 'Format')
932		else:
933			deleteFormat = False
934			table = self.__dict__.copy()
935
936		# some count references may have been initialized in a custom preWrite; we set
937		# these in the writer's state beforehand (instead of sequentially) so they will
938		# be propagated to all nested subtables even if the count appears in the current
939		# table only *after* the offset to the subtable that it is counting.
940		for conv in self.getConverters():
941			if conv.isCount and conv.isPropagated:
942				value = table.get(conv.name)
943				if isinstance(value, CountReference):
944					writer[conv.name] = value
945
946		if hasattr(self, 'sortCoverageLast'):
947			writer.sortCoverageLast = 1
948
949		if hasattr(self, 'DontShare'):
950			writer.DontShare = True
951
952		if hasattr(self.__class__, 'LookupType'):
953			writer['LookupType'].setValue(self.__class__.LookupType)
954
955		self.writeFormat(writer)
956		for conv in self.getConverters():
957			value = table.get(conv.name) # TODO Handle defaults instead of defaulting to None!
958			if conv.repeat:
959				if value is None:
960					value = []
961				countValue = len(value) - conv.aux
962				if isinstance(conv.repeat, int):
963					assert len(value) == conv.repeat, 'expected %d values, got %d' % (conv.repeat, len(value))
964				elif conv.repeat in table:
965					CountReference(table, conv.repeat, value=countValue)
966				else:
967					# conv.repeat is a propagated count
968					writer[conv.repeat].setValue(countValue)
969				try:
970					conv.writeArray(writer, font, table, value)
971				except Exception as e:
972					e.args = e.args + (conv.name+'[]',)
973					raise
974			elif conv.isCount:
975				# Special-case Count values.
976				# Assumption: a Count field will *always* precede
977				# the actual array(s).
978				# We need a default value, as it may be set later by a nested
979				# table. We will later store it here.
980				# We add a reference: by the time the data is assembled
981				# the Count value will be filled in.
982				# We ignore the current count value since it will be recomputed,
983				# unless it's a CountReference that was already initialized in a custom preWrite.
984				if isinstance(value, CountReference):
985					ref = value
986					ref.size = conv.staticSize
987					writer.writeData(ref)
988					table[conv.name] = ref.getValue()
989				else:
990					ref = writer.writeCountReference(table, conv.name, conv.staticSize)
991					table[conv.name] = None
992				if conv.isPropagated:
993					writer[conv.name] = ref
994			elif conv.isLookupType:
995				# We make sure that subtables have the same lookup type,
996				# and that the type is the same as the one set on the
997				# Lookup object, if any is set.
998				if conv.name not in table:
999					table[conv.name] = None
1000				ref = writer.writeCountReference(table, conv.name, conv.staticSize, table[conv.name])
1001				writer['LookupType'] = ref
1002			else:
1003				if conv.aux and not eval(conv.aux, None, table):
1004					continue
1005				try:
1006					conv.write(writer, font, table, value)
1007				except Exception as e:
1008					name = value.__class__.__name__ if value is not None else conv.name
1009					e.args = e.args + (name,)
1010					raise
1011				if conv.isPropagated:
1012					writer[conv.name] = value
1013
1014		if deleteFormat:
1015			del self.Format
1016
1017	def readFormat(self, reader):
1018		pass
1019
1020	def writeFormat(self, writer):
1021		pass
1022
1023	def toXML(self, xmlWriter, font, attrs=None, name=None):
1024		tableName = name if name else self.__class__.__name__
1025		if attrs is None:
1026			attrs = []
1027		if hasattr(self, "Format"):
1028			attrs = attrs + [("Format", self.Format)]
1029		xmlWriter.begintag(tableName, attrs)
1030		xmlWriter.newline()
1031		self.toXML2(xmlWriter, font)
1032		xmlWriter.endtag(tableName)
1033		xmlWriter.newline()
1034
1035	def toXML2(self, xmlWriter, font):
1036		# Simpler variant of toXML, *only* for the top level tables (like GPOS, GSUB).
1037		# This is because in TTX our parent writes our main tag, and in otBase.py we
1038		# do it ourselves. I think I'm getting schizophrenic...
1039		for conv in self.getConverters():
1040			if conv.repeat:
1041				value = getattr(self, conv.name, [])
1042				for i in range(len(value)):
1043					item = value[i]
1044					conv.xmlWrite(xmlWriter, font, item, conv.name,
1045							[("index", i)])
1046			else:
1047				if conv.aux and not eval(conv.aux, None, vars(self)):
1048					continue
1049				value = getattr(self, conv.name, None) # TODO Handle defaults instead of defaulting to None!
1050				conv.xmlWrite(xmlWriter, font, value, conv.name, [])
1051
1052	def fromXML(self, name, attrs, content, font):
1053		try:
1054			conv = self.getConverterByName(name)
1055		except KeyError:
1056			raise    # XXX on KeyError, raise nice error
1057		value = conv.xmlRead(attrs, content, font)
1058		if conv.repeat:
1059			seq = getattr(self, conv.name, None)
1060			if seq is None:
1061				seq = []
1062				setattr(self, conv.name, seq)
1063			seq.append(value)
1064		else:
1065			setattr(self, conv.name, value)
1066
1067	def __ne__(self, other):
1068		result = self.__eq__(other)
1069		return result if result is NotImplemented else not result
1070
1071	def __eq__(self, other):
1072		if type(self) != type(other):
1073			return NotImplemented
1074
1075		self.ensureDecompiled()
1076		other.ensureDecompiled()
1077
1078		return self.__dict__ == other.__dict__
1079
1080	class SubTableEntry(NamedTuple):
1081		"""See BaseTable.iterSubTables()"""
1082		name: str
1083		value: "BaseTable"
1084		index: Optional[int] = None  # index into given array, None for single values
1085
1086	def iterSubTables(self) -> Iterator[SubTableEntry]:
1087		"""Yield (name, value, index) namedtuples for all subtables of current table.
1088
1089		A sub-table is an instance of BaseTable (or subclass thereof) that is a child
1090		of self, the current parent table.
1091		The tuples also contain the attribute name (str) of the of parent table to get
1092		a subtable, and optionally, for lists of subtables (i.e. attributes associated
1093		with a converter that has a 'repeat'), an index into the list containing the
1094		given subtable value.
1095		This method can be useful to traverse trees of otTables.
1096		"""
1097		for conv in self.getConverters():
1098			name = conv.name
1099			value = getattr(self, name, None)
1100			if value is None:
1101				continue
1102			if isinstance(value, BaseTable):
1103				yield self.SubTableEntry(name, value)
1104			elif isinstance(value, list):
1105				yield from (
1106					self.SubTableEntry(name, v, index=i)
1107					for i, v in enumerate(value)
1108					if isinstance(v, BaseTable)
1109				)
1110
1111	# instance (not @class)method for consistency with FormatSwitchingBaseTable
1112	def getVariableAttrs(self):
1113		return getVariableAttrs(self.__class__)
1114
1115
1116class FormatSwitchingBaseTable(BaseTable):
1117
1118	"""Minor specialization of BaseTable, for tables that have multiple
1119	formats, eg. CoverageFormat1 vs. CoverageFormat2."""
1120
1121	@classmethod
1122	def getRecordSize(cls, reader):
1123		return NotImplemented
1124
1125	def getConverters(self):
1126		try:
1127			fmt = self.Format
1128		except AttributeError:
1129			# some FormatSwitchingBaseTables (e.g. Coverage) no longer have 'Format'
1130			# attribute after fully decompiled, only gain one in preWrite before being
1131			# recompiled. In the decompiled state, these hand-coded classes defined in
1132			# otTables.py lose their format-specific nature and gain more high-level
1133			# attributes that are not tied to converters.
1134			return []
1135		return self.converters.get(self.Format, [])
1136
1137	def getConverterByName(self, name):
1138		return self.convertersByName[self.Format][name]
1139
1140	def readFormat(self, reader):
1141		self.Format = reader.readUShort()
1142
1143	def writeFormat(self, writer):
1144		writer.writeUShort(self.Format)
1145
1146	def toXML(self, xmlWriter, font, attrs=None, name=None):
1147		BaseTable.toXML(self, xmlWriter, font, attrs, name)
1148
1149	def getVariableAttrs(self):
1150		return getVariableAttrs(self.__class__, self.Format)
1151
1152
1153class UInt8FormatSwitchingBaseTable(FormatSwitchingBaseTable):
1154	def readFormat(self, reader):
1155		self.Format = reader.readUInt8()
1156
1157	def writeFormat(self, writer):
1158		writer.writeUInt8(self.Format)
1159
1160
1161formatSwitchingBaseTables = {
1162	"uint16": FormatSwitchingBaseTable,
1163	"uint8": UInt8FormatSwitchingBaseTable,
1164}
1165
1166def getFormatSwitchingBaseTableClass(formatType):
1167	try:
1168		return formatSwitchingBaseTables[formatType]
1169	except KeyError:
1170		raise TypeError(f"Unsupported format type: {formatType!r}")
1171
1172
1173# memoize since these are parsed from otData.py, thus stay constant
1174@lru_cache()
1175def getVariableAttrs(cls: BaseTable, fmt: Optional[int] = None) -> Tuple[str]:
1176	"""Return sequence of variable table field names (can be empty).
1177
1178	Attributes are deemed "variable" when their otData.py's description contain
1179	'VarIndexBase + {offset}', e.g. COLRv1 PaintVar* tables.
1180	"""
1181	if not issubclass(cls, BaseTable):
1182		raise TypeError(cls)
1183	if issubclass(cls, FormatSwitchingBaseTable):
1184		if fmt is None:
1185			raise TypeError(f"'fmt' is required for format-switching {cls.__name__}")
1186		converters = cls.convertersByName[fmt]
1187	else:
1188		converters = cls.convertersByName
1189	# assume if no 'VarIndexBase' field is present, table has no variable fields
1190	if "VarIndexBase" not in converters:
1191		return ()
1192	varAttrs = {}
1193	for name, conv in converters.items():
1194		offset = conv.getVarIndexOffset()
1195		if offset is not None:
1196			varAttrs[name] = offset
1197	return tuple(sorted(varAttrs, key=varAttrs.__getitem__))
1198
1199
1200#
1201# Support for ValueRecords
1202#
1203# This data type is so different from all other OpenType data types that
1204# it requires quite a bit of code for itself. It even has special support
1205# in OTTableReader and OTTableWriter...
1206#
1207
1208valueRecordFormat = [
1209#	Mask	 Name		isDevice signed
1210	(0x0001, "XPlacement",	0,	1),
1211	(0x0002, "YPlacement",	0,	1),
1212	(0x0004, "XAdvance",	0,	1),
1213	(0x0008, "YAdvance",	0,	1),
1214	(0x0010, "XPlaDevice",	1,	0),
1215	(0x0020, "YPlaDevice",	1,	0),
1216	(0x0040, "XAdvDevice",	1,	0),
1217	(0x0080, "YAdvDevice",	1,	0),
1218#	reserved:
1219	(0x0100, "Reserved1",	0,	0),
1220	(0x0200, "Reserved2",	0,	0),
1221	(0x0400, "Reserved3",	0,	0),
1222	(0x0800, "Reserved4",	0,	0),
1223	(0x1000, "Reserved5",	0,	0),
1224	(0x2000, "Reserved6",	0,	0),
1225	(0x4000, "Reserved7",	0,	0),
1226	(0x8000, "Reserved8",	0,	0),
1227]
1228
1229def _buildDict():
1230	d = {}
1231	for mask, name, isDevice, signed in valueRecordFormat:
1232		d[name] = mask, isDevice, signed
1233	return d
1234
1235valueRecordFormatDict = _buildDict()
1236
1237
1238class ValueRecordFactory(object):
1239
1240	"""Given a format code, this object convert ValueRecords."""
1241
1242	def __init__(self, valueFormat):
1243		format = []
1244		for mask, name, isDevice, signed in valueRecordFormat:
1245			if valueFormat & mask:
1246				format.append((name, isDevice, signed))
1247		self.format = format
1248
1249	def __len__(self):
1250		return len(self.format)
1251
1252	def readValueRecord(self, reader, font):
1253		format = self.format
1254		if not format:
1255			return None
1256		valueRecord = ValueRecord()
1257		for name, isDevice, signed in format:
1258			if signed:
1259				value = reader.readShort()
1260			else:
1261				value = reader.readUShort()
1262			if isDevice:
1263				if value:
1264					from . import otTables
1265					subReader = reader.getSubReader(value)
1266					value = getattr(otTables, name)()
1267					value.decompile(subReader, font)
1268				else:
1269					value = None
1270			setattr(valueRecord, name, value)
1271		return valueRecord
1272
1273	def writeValueRecord(self, writer, font, valueRecord):
1274		for name, isDevice, signed in self.format:
1275			value = getattr(valueRecord, name, 0)
1276			if isDevice:
1277				if value:
1278					subWriter = writer.getSubWriter()
1279					writer.writeSubTable(subWriter)
1280					value.compile(subWriter, font)
1281				else:
1282					writer.writeUShort(0)
1283			elif signed:
1284				writer.writeShort(value)
1285			else:
1286				writer.writeUShort(value)
1287
1288
1289class ValueRecord(object):
1290
1291	# see ValueRecordFactory
1292
1293	def __init__(self, valueFormat=None, src=None):
1294		if valueFormat is not None:
1295			for mask, name, isDevice, signed in valueRecordFormat:
1296				if valueFormat & mask:
1297					setattr(self, name, None if isDevice else 0)
1298			if src is not None:
1299				for key,val in src.__dict__.items():
1300					if not hasattr(self, key):
1301						continue
1302					setattr(self, key, val)
1303		elif src is not None:
1304			self.__dict__ = src.__dict__.copy()
1305
1306	def getFormat(self):
1307		format = 0
1308		for name in self.__dict__.keys():
1309			format = format | valueRecordFormatDict[name][0]
1310		return format
1311
1312	def getEffectiveFormat(self):
1313		format = 0
1314		for name,value in self.__dict__.items():
1315			if value:
1316				format = format | valueRecordFormatDict[name][0]
1317		return format
1318
1319	def toXML(self, xmlWriter, font, valueName, attrs=None):
1320		if attrs is None:
1321			simpleItems = []
1322		else:
1323			simpleItems = list(attrs)
1324		for mask, name, isDevice, format in valueRecordFormat[:4]:  # "simple" values
1325			if hasattr(self, name):
1326				simpleItems.append((name, getattr(self, name)))
1327		deviceItems = []
1328		for mask, name, isDevice, format in valueRecordFormat[4:8]:  # device records
1329			if hasattr(self, name):
1330				device = getattr(self, name)
1331				if device is not None:
1332					deviceItems.append((name, device))
1333		if deviceItems:
1334			xmlWriter.begintag(valueName, simpleItems)
1335			xmlWriter.newline()
1336			for name, deviceRecord in deviceItems:
1337				if deviceRecord is not None:
1338					deviceRecord.toXML(xmlWriter, font, name=name)
1339			xmlWriter.endtag(valueName)
1340			xmlWriter.newline()
1341		else:
1342			xmlWriter.simpletag(valueName, simpleItems)
1343			xmlWriter.newline()
1344
1345	def fromXML(self, name, attrs, content, font):
1346		from . import otTables
1347		for k, v in attrs.items():
1348			setattr(self, k, int(v))
1349		for element in content:
1350			if not isinstance(element, tuple):
1351				continue
1352			name, attrs, content = element
1353			value = getattr(otTables, name)()
1354			for elem2 in content:
1355				if not isinstance(elem2, tuple):
1356					continue
1357				name2, attrs2, content2 = elem2
1358				value.fromXML(name2, attrs2, content2, font)
1359			setattr(self, name, value)
1360
1361	def __ne__(self, other):
1362		result = self.__eq__(other)
1363		return result if result is NotImplemented else not result
1364
1365	def __eq__(self, other):
1366		if type(self) != type(other):
1367			return NotImplemented
1368		return self.__dict__ == other.__dict__
1369