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