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