otBase.py revision ab0ca1bd6b495ea3b9523c74cc06ab957a10293c
1from __future__ import print_function, division
2from fontTools.misc.py23 import *
3from .DefaultTable import DefaultTable
4import struct
5
6class OverflowErrorRecord(object):
7	def __init__(self, overflowTuple):
8		self.tableType = overflowTuple[0]
9		self.LookupListIndex = overflowTuple[1]
10		self.SubTableIndex = overflowTuple[2]
11		self.itemName = overflowTuple[3]
12		self.itemIndex = overflowTuple[4]
13
14	def __repr__(self):
15		return str((self.tableType, "LookupIndex:", self.LookupListIndex, "SubTableIndex:", self.SubTableIndex, "ItemName:", self.itemName, "ItemIndex:", self.itemIndex))
16
17class OTLOffsetOverflowError(Exception):
18	def __init__(self, overflowErrorRecord):
19		self.value = overflowErrorRecord
20
21	def __str__(self):
22		return repr(self.value)
23
24
25class BaseTTXConverter(DefaultTable):
26
27	"""Generic base class for TTX table converters. It functions as an
28	adapter between the TTX (ttLib actually) table model and the model
29	we use for OpenType tables, which is necessarily subtly different.
30	"""
31
32	def decompile(self, data, font):
33		from . import otTables
34		cachingStats = None if True else {}
35		class GlobalState(object):
36			def __init__(self, tableType, cachingStats):
37				self.tableType = tableType
38				self.cachingStats = cachingStats
39		globalState = GlobalState(tableType=self.tableTag,
40					  cachingStats=cachingStats)
41		reader = OTTableReader(data, globalState)
42		tableClass = getattr(otTables, self.tableTag)
43		self.table = tableClass()
44		self.table.decompile(reader, font)
45		if cachingStats:
46			stats = sorted([(v, k) for k, v in cachingStats.items()])
47			stats.reverse()
48			print("cachingsstats for ", self.tableTag)
49			for v, k in stats:
50				if v < 2:
51					break
52				print(v, k)
53			print("---", len(stats))
54
55	def compile(self, font):
56		""" Create a top-level OTFWriter for the GPOS/GSUB table.
57			Call the compile method for the the table
58				for each 'converter' record in the table converter list
59					call converter's write method for each item in the value.
60						- For simple items, the write method adds a string to the
61						writer's self.items list.
62						- For Struct/Table/Subtable items, it add first adds new writer to the
63						to the writer's self.items, then calls the item's compile method.
64						This creates a tree of writers, rooted at the GUSB/GPOS writer, with
65						each writer representing a table, and the writer.items list containing
66						the child data strings and writers.
67			call the getAllData method
68				call _doneWriting, which removes duplicates
69				call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables
70				Traverse the flat list of tables, calling getDataLength on each to update their position
71				Traverse the flat list of tables again, calling getData each get the data in the table, now that
72				pos's and offset are known.
73
74				If a lookup subtable overflows an offset, we have to start all over.
75		"""
76		class GlobalState(object):
77			def __init__(self, tableType):
78				self.tableType = tableType
79		globalState = GlobalState(tableType=self.tableTag)
80		writer = OTTableWriter(globalState)
81		writer.parent = None
82		self.table.compile(writer, font)
83		return writer.getAllData()
84
85	def toXML(self, writer, font):
86		self.table.toXML2(writer, font)
87
88	def fromXML(self, name, attrs, content, font):
89		from . import otTables
90		if not hasattr(self, "table"):
91			tableClass = getattr(otTables, self.tableTag)
92			self.table = tableClass()
93		self.table.fromXML(name, attrs, content, font)
94
95
96class OTTableReader(object):
97
98	"""Helper class to retrieve data from an OpenType table."""
99
100	__slots__ = ('data', 'offset', 'pos', 'globalState', 'localState')
101
102	def __init__(self, data, globalState={}, localState=None, offset=0):
103		self.data = data
104		self.offset = offset
105		self.pos = offset
106		self.globalState = globalState
107		self.localState = localState
108
109	def getSubReader(self, offset):
110		offset = self.offset + offset
111		cachingStats = self.globalState.cachingStats
112		if cachingStats is not None:
113			cachingStats[offset] = cachingStats.get(offset, 0) + 1
114		return self.__class__(self.data, self.globalState, self.localState, offset)
115
116	def readUShort(self):
117		pos = self.pos
118		newpos = pos + 2
119		value, = struct.unpack(">H", self.data[pos:newpos])
120		self.pos = newpos
121		return value
122
123	def readShort(self):
124		pos = self.pos
125		newpos = pos + 2
126		value, = struct.unpack(">h", self.data[pos:newpos])
127		self.pos = newpos
128		return value
129
130	def readLong(self):
131		pos = self.pos
132		newpos = pos + 4
133		value, = struct.unpack(">l", self.data[pos:newpos])
134		self.pos = newpos
135		return value
136
137	def readUInt24(self):
138		pos = self.pos
139		newpos = pos + 3
140		value, = struct.unpack(">l", b'\0'+self.data[pos:newpos])
141		self.pos = newpos
142		return value
143
144	def readULong(self):
145		pos = self.pos
146		newpos = pos + 4
147		value, = struct.unpack(">L", self.data[pos:newpos])
148		self.pos = newpos
149		return value
150
151	def readTag(self):
152		pos = self.pos
153		newpos = pos + 4
154		value = Tag(self.data[pos:newpos])
155		assert len(value) == 4
156		self.pos = newpos
157		return value
158
159	def __setitem__(self, name, value):
160		state = self.localState.copy() if self.localState else dict()
161		state[name] = value
162		self.localState = state
163
164	def __getitem__(self, name):
165		return self.localState[name]
166
167
168class OTTableWriter(object):
169
170	"""Helper class to gather and assemble data for OpenType tables."""
171
172	def __init__(self, globalState, localState=None):
173		self.items = []
174		self.pos = None
175		self.globalState = globalState
176		self.localState = localState
177
178	def __setitem__(self, name, value):
179		state = self.localState.copy() if self.localState else dict()
180		state[name] = value
181		self.localState = state
182
183	def __getitem__(self, name):
184		return self.localState[name]
185
186	# assembler interface
187
188	def getAllData(self):
189		"""Assemble all data, including all subtables."""
190		self._doneWriting()
191		tables, extTables = self._gatherTables()
192		tables.reverse()
193		extTables.reverse()
194		# Gather all data in two passes: the absolute positions of all
195		# subtable are needed before the actual data can be assembled.
196		pos = 0
197		for table in tables:
198			table.pos = pos
199			pos = pos + table.getDataLength()
200
201		for table in extTables:
202			table.pos = pos
203			pos = pos + table.getDataLength()
204
205
206		data = []
207		for table in tables:
208			tableData = table.getData()
209			data.append(tableData)
210
211		for table in extTables:
212			tableData = table.getData()
213			data.append(tableData)
214
215		return bytesjoin(data)
216
217	def getDataLength(self):
218		"""Return the length of this table in bytes, without subtables."""
219		l = 0
220		for item in self.items:
221			if hasattr(item, "getData") or hasattr(item, "getCountData"):
222				if item.longOffset:
223					l = l + 4  # sizeof(ULong)
224				else:
225					l = l + 2  # sizeof(UShort)
226			else:
227				l = l + len(item)
228		return l
229
230	def getData(self):
231		"""Assemble the data for this writer/table, without subtables."""
232		items = list(self.items)  # make a shallow copy
233		pos = self.pos
234		numItems = len(items)
235		for i in range(numItems):
236			item = items[i]
237
238			if hasattr(item, "getData"):
239				if item.longOffset:
240					items[i] = packULong(item.pos - pos)
241				else:
242					try:
243						items[i] = packUShort(item.pos - pos)
244					except struct.error:
245						# provide data to fix overflow problem.
246						# If the overflow is to a lookup, or from a lookup to a subtable,
247						# just report the current item.  Otherwise...
248						if self.name not in [ 'LookupList', 'Lookup']:
249							# overflow is within a subTable. Life is more complicated.
250							# If we split the sub-table just before the current item, we may still suffer overflow.
251							# This is because duplicate table merging is done only within an Extension subTable tree;
252							# when we split the subtable in two, some items may no longer be duplicates.
253							# Get worst case by adding up all the item lengths, depth first traversal.
254							# and then report the first item that overflows a short.
255							def getDeepItemLength(table):
256								if hasattr(table, "getDataLength"):
257									length = 0
258									for item in table.items:
259										length = length + getDeepItemLength(item)
260								else:
261									length = len(table)
262								return length
263
264							length = self.getDataLength()
265							if hasattr(self, "sortCoverageLast") and item.name == "Coverage":
266								# Coverage is first in the item list, but last in the table list,
267								# The original overflow is really in the item list. Skip the Coverage
268								# table in the following test.
269								items = items[i+1:]
270
271							for j in range(len(items)):
272								item = items[j]
273								length = length + getDeepItemLength(item)
274								if length > 65535:
275									break
276						overflowErrorRecord = self.getOverflowErrorRecord(item)
277
278
279						raise OTLOffsetOverflowError(overflowErrorRecord)
280
281		return bytesjoin(items)
282
283	def __hash__(self):
284		# only works after self._doneWriting() has been called
285		return hash(self.items)
286
287	def __ne__(self, other):
288		return not self.__eq__(other)
289	def __eq__(self, other):
290		if type(self) != type(other):
291			return NotImplemented
292		return self.items == other.items
293
294	def _doneWriting(self, internedTables=None):
295		# Convert CountData references to data string items
296		# collapse duplicate table references to a unique entry
297		# "tables" are OTTableWriter objects.
298
299		# For Extension Lookup types, we can
300		# eliminate duplicates only within the tree under the Extension Lookup,
301		# as offsets may exceed 64K even between Extension LookupTable subtables.
302		if internedTables is None:
303			internedTables = {}
304		items = self.items
305		iRange = list(range(len(items)))
306
307		if hasattr(self, "Extension"):
308			newTree = 1
309		else:
310			newTree = 0
311		for i in iRange:
312			item = items[i]
313			if hasattr(item, "getCountData"):
314				items[i] = item.getCountData()
315			elif hasattr(item, "getData"):
316				if newTree:
317					item._doneWriting()
318				else:
319					item._doneWriting(internedTables)
320					internedItem = internedTables.get(item)
321					if internedItem:
322						items[i] = item = internedItem
323					else:
324						internedTables[item] = item
325		self.items = tuple(items)
326
327	def _gatherTables(self, tables=None, extTables=None, done=None):
328		# Convert table references in self.items tree to a flat
329		# list of tables in depth-first traversal order.
330		# "tables" are OTTableWriter objects.
331		# We do the traversal in reverse order at each level, in order to
332		# resolve duplicate references to be the last reference in the list of tables.
333		# For extension lookups, duplicate references can be merged only within the
334		# writer tree under the  extension lookup.
335		if tables is None: # init call for first time.
336			tables = []
337			extTables = []
338			done = {}
339
340		done[self] = 1
341
342		numItems = len(self.items)
343		iRange = list(range(numItems))
344		iRange.reverse()
345
346		if hasattr(self, "Extension"):
347			appendExtensions = 1
348		else:
349			appendExtensions = 0
350
351		# add Coverage table if it is sorted last.
352		sortCoverageLast = 0
353		if hasattr(self, "sortCoverageLast"):
354			# Find coverage table
355			for i in range(numItems):
356				item = self.items[i]
357				if hasattr(item, "name") and (item.name == "Coverage"):
358					sortCoverageLast = 1
359					break
360			if item not in done:
361				item._gatherTables(tables, extTables, done)
362			else:
363				index = max(item.parent.keys())
364				item.parent[index + 1] = self
365
366		for i in iRange:
367			item = self.items[i]
368			if not hasattr(item, "getData"):
369				continue
370
371			if sortCoverageLast and (i==1) and item.name == 'Coverage':
372				# we've already 'gathered' it above
373				continue
374
375			if appendExtensions:
376				assert extTables is not None, "Program or XML editing error. Extension subtables cannot contain extensions subtables"
377				newDone = {}
378				item._gatherTables(extTables, None, newDone)
379
380			elif item not in done:
381				item._gatherTables(tables, extTables, done)
382			else:
383				index = max(item.parent.keys())
384				item.parent[index + 1] = self
385
386
387		tables.append(self)
388		return tables, extTables
389
390	# interface for gathering data, as used by table.compile()
391
392	def getSubWriter(self):
393		subwriter = self.__class__(self.globalState, self.localState)
394		subwriter.parent = {0:self} # because some subtables have idential values, we discard
395									# the duplicates under the getAllData method. Hence some
396									# subtable writers can have more than one parent writer.
397		return subwriter
398
399	def writeUShort(self, value):
400		assert 0 <= value < 0x10000
401		self.items.append(struct.pack(">H", value))
402
403	def writeShort(self, value):
404		self.items.append(struct.pack(">h", value))
405
406	def writeUInt24(self, value):
407		assert 0 <= value < 0x1000000
408		b = struct.pack(">L", value)
409		self.items.append(b[1:])
410
411	def writeLong(self, value):
412		self.items.append(struct.pack(">l", value))
413
414	def writeULong(self, value):
415		self.items.append(struct.pack(">L", value))
416
417	def writeTag(self, tag):
418		tag = Tag(tag).tobytes()
419		assert len(tag) == 4
420		self.items.append(tag)
421
422	def writeSubTable(self, subWriter):
423		self.items.append(subWriter)
424
425	def writeCountReference(self, table, name):
426		ref = CountReference(table, name)
427		self.items.append(ref)
428		return ref
429
430	def writeStruct(self, format, values):
431		data = struct.pack(*(format,) + values)
432		self.items.append(data)
433
434	def writeData(self, data):
435		self.items.append(data)
436
437	def	getOverflowErrorRecord(self, item):
438		LookupListIndex = SubTableIndex = itemName = itemIndex = None
439		if self.name == 'LookupList':
440			LookupListIndex = item.repeatIndex
441		elif self.name == 'Lookup':
442			LookupListIndex = self.repeatIndex
443			SubTableIndex = item.repeatIndex
444		else:
445			itemName = item.name
446			if hasattr(item, 'repeatIndex'):
447				itemIndex = item.repeatIndex
448			if self.name == 'SubTable':
449				LookupListIndex = self.parent[0].repeatIndex
450				SubTableIndex = self.repeatIndex
451			elif self.name == 'ExtSubTable':
452				LookupListIndex = self.parent[0].parent[0].repeatIndex
453				SubTableIndex = self.parent[0].repeatIndex
454			else: # who knows how far below the SubTable level we are! Climb back up to the nearest subtable.
455				itemName = ".".join([self.name, item.name])
456				p1 = self.parent[0]
457				while p1 and p1.name not in ['ExtSubTable', 'SubTable']:
458					itemName = ".".join([p1.name, item.name])
459					p1 = p1.parent[0]
460				if p1:
461					if p1.name == 'ExtSubTable':
462						LookupListIndex = p1.parent[0].parent[0].repeatIndex
463						SubTableIndex = p1.parent[0].repeatIndex
464					else:
465						LookupListIndex = p1.parent[0].repeatIndex
466						SubTableIndex = p1.repeatIndex
467
468		return OverflowErrorRecord( (self.globalState.tableType, LookupListIndex, SubTableIndex, itemName, itemIndex) )
469
470
471class CountReference(object):
472	"""A reference to a Count value, not a count of references."""
473	def __init__(self, table, name):
474		self.table = table
475		self.name = name
476	def setValue(self, value):
477		table = self.table
478		name = self.name
479		if table[name] is None:
480			table[name] = value
481		else:
482			assert table[name] == value, (name, table[name], value)
483	def getCountData(self):
484		return packUShort(self.table[self.name])
485
486
487def packUShort(value):
488	return struct.pack(">H", value)
489
490
491def packULong(value):
492	assert 0 <= value < 0x100000000, value
493	return struct.pack(">L", value)
494
495
496class BaseTable(object):
497
498	def __getattr__(self, attr):
499		reader = self.__dict__.get("reader")
500		if reader:
501			del self.reader
502			font = self.font
503			del self.font
504			self.decompile(reader, font)
505			return getattr(self, attr)
506
507		raise AttributeError(attr)
508
509
510	"""Generic base class for all OpenType (sub)tables."""
511
512	def getConverters(self):
513		return self.converters
514
515	def getConverterByName(self, name):
516		return self.convertersByName[name]
517
518	def decompile(self, reader, font):
519		self.readFormat(reader)
520		table = {}
521		self.__rawTable = table  # for debugging
522		converters = self.getConverters()
523		for conv in converters:
524			if conv.name == "SubTable":
525				conv = conv.getConverter(reader.globalState.tableType,
526						table["LookupType"])
527			if conv.name == "ExtSubTable":
528				conv = conv.getConverter(reader.globalState.tableType,
529						table["ExtensionLookupType"])
530			if conv.name == "FeatureParams":
531				conv = conv.getConverter(reader["FeatureTag"])
532			if conv.repeat:
533				l = []
534				if conv.repeat in table:
535					countValue = table[conv.repeat]
536				else:
537					# conv.repeat is a propagated count
538					countValue = reader[conv.repeat]
539				for i in range(countValue + conv.aux):
540					l.append(conv.read(reader, font, table))
541				table[conv.name] = l
542			else:
543				if conv.aux and not eval(conv.aux, None, table):
544					continue
545				table[conv.name] = conv.read(reader, font, table)
546				if conv.isPropagated:
547					reader[conv.name] = table[conv.name]
548
549		self.postRead(table, font)
550
551		del self.__rawTable  # succeeded, get rid of debugging info
552
553	def ensureDecompiled(self):
554		reader = self.__dict__.get("reader")
555		if reader:
556			del self.reader
557			font = self.font
558			del self.font
559			self.decompile(reader, font)
560
561	def compile(self, writer, font):
562		self.ensureDecompiled()
563		table = self.preWrite(font)
564
565		if hasattr(self, 'sortCoverageLast'):
566			writer.sortCoverageLast = 1
567
568		if hasattr(self.__class__, 'LookupType'):
569			writer['LookupType'].setValue(self.__class__.LookupType)
570
571		self.writeFormat(writer)
572		for conv in self.getConverters():
573			value = table.get(conv.name)
574			if conv.repeat:
575				if value is None:
576					value = []
577				countValue = len(value) - conv.aux
578				if conv.repeat in table:
579					CountReference(table, conv.repeat).setValue(countValue)
580				else:
581					# conv.repeat is a propagated count
582					writer[conv.repeat].setValue(countValue)
583				for i in range(len(value)):
584					conv.write(writer, font, table, value[i], i)
585			elif conv.isCount:
586				# Special-case Count values.
587				# Assumption: a Count field will *always* precede
588				# the actual array(s).
589				# We need a default value, as it may be set later by a nested
590				# table. We will later store it here.
591				# We add a reference: by the time the data is assembled
592				# the Count value will be filled in.
593				ref = writer.writeCountReference(table, conv.name)
594				table[conv.name] = None
595				if conv.isPropagated:
596					writer[conv.name] = ref
597			elif conv.isLookupType:
598				ref = writer.writeCountReference(table, conv.name)
599				table[conv.name] = None
600				writer['LookupType'] = ref
601			else:
602				if conv.aux and not eval(conv.aux, None, table):
603					continue
604				conv.write(writer, font, table, value)
605				if conv.isPropagated:
606					writer[conv.name] = value
607
608	def readFormat(self, reader):
609		pass
610
611	def writeFormat(self, writer):
612		pass
613
614	def postRead(self, table, font):
615		self.__dict__.update(table)
616
617	def preWrite(self, font):
618		return self.__dict__.copy()
619
620	def toXML(self, xmlWriter, font, attrs=None, name=None):
621		tableName = name if name else self.__class__.__name__
622		if attrs is None:
623			attrs = []
624		if hasattr(self, "Format"):
625			attrs = attrs + [("Format", self.Format)]
626		xmlWriter.begintag(tableName, attrs)
627		xmlWriter.newline()
628		self.toXML2(xmlWriter, font)
629		xmlWriter.endtag(tableName)
630		xmlWriter.newline()
631
632	def toXML2(self, xmlWriter, font):
633		# Simpler variant of toXML, *only* for the top level tables (like GPOS, GSUB).
634		# This is because in TTX our parent writes our main tag, and in otBase.py we
635		# do it ourselves. I think I'm getting schizophrenic...
636		for conv in self.getConverters():
637			if conv.repeat:
638				value = getattr(self, conv.name)
639				for i in range(len(value)):
640					item = value[i]
641					conv.xmlWrite(xmlWriter, font, item, conv.name,
642							[("index", i)])
643			else:
644				if conv.aux and not eval(conv.aux, None, vars(self)):
645					continue
646				value = getattr(self, conv.name)
647				conv.xmlWrite(xmlWriter, font, value, conv.name, [])
648
649	def fromXML(self, name, attrs, content, font):
650		try:
651			conv = self.getConverterByName(name)
652		except KeyError:
653			raise    # XXX on KeyError, raise nice error
654		value = conv.xmlRead(attrs, content, font)
655		if conv.repeat:
656			seq = getattr(self, conv.name, None)
657			if seq is None:
658				seq = []
659				setattr(self, conv.name, seq)
660			seq.append(value)
661		else:
662			setattr(self, conv.name, value)
663
664	def __ne__(self, other):
665		return not self.__eq__(other)
666	def __eq__(self, other):
667		if type(self) != type(other):
668			return NotImplemented
669
670		self.ensureDecompiled()
671		other.ensureDecompiled()
672
673		return self.__dict__ == other.__dict__
674
675
676class FormatSwitchingBaseTable(BaseTable):
677
678	"""Minor specialization of BaseTable, for tables that have multiple
679	formats, eg. CoverageFormat1 vs. CoverageFormat2."""
680
681	def getConverters(self):
682		return self.converters[self.Format]
683
684	def getConverterByName(self, name):
685		return self.convertersByName[self.Format][name]
686
687	def readFormat(self, reader):
688		self.Format = reader.readUShort()
689		assert self.Format != 0, (self, reader.pos, len(reader.data))
690
691	def writeFormat(self, writer):
692		writer.writeUShort(self.Format)
693
694	def toXML(self, xmlWriter, font, attrs=None, name=None):
695		BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__)
696
697
698#
699# Support for ValueRecords
700#
701# This data type is so different from all other OpenType data types that
702# it requires quite a bit of code for itself. It even has special support
703# in OTTableReader and OTTableWriter...
704#
705
706valueRecordFormat = [
707#	Mask	 Name            isDevice  signed
708	(0x0001, "XPlacement",   0,        1),
709	(0x0002, "YPlacement",   0,        1),
710	(0x0004, "XAdvance",     0,        1),
711	(0x0008, "YAdvance",     0,        1),
712	(0x0010, "XPlaDevice",   1,        0),
713	(0x0020, "YPlaDevice",   1,        0),
714	(0x0040, "XAdvDevice",   1,        0),
715	(0x0080, "YAdvDevice",   1,        0),
716# 	reserved:
717	(0x0100, "Reserved1",    0,        0),
718	(0x0200, "Reserved2",    0,        0),
719	(0x0400, "Reserved3",    0,        0),
720	(0x0800, "Reserved4",    0,        0),
721	(0x1000, "Reserved5",    0,        0),
722	(0x2000, "Reserved6",    0,        0),
723	(0x4000, "Reserved7",    0,        0),
724	(0x8000, "Reserved8",    0,        0),
725]
726
727def _buildDict():
728	d = {}
729	for mask, name, isDevice, signed in valueRecordFormat:
730		d[name] = mask, isDevice, signed
731	return d
732
733valueRecordFormatDict = _buildDict()
734
735
736class ValueRecordFactory(object):
737
738	"""Given a format code, this object convert ValueRecords."""
739
740	def __init__(self, valueFormat):
741		format = []
742		for mask, name, isDevice, signed in valueRecordFormat:
743			if valueFormat & mask:
744				format.append((name, isDevice, signed))
745		self.format = format
746
747	def readValueRecord(self, reader, font):
748		format = self.format
749		if not format:
750			return None
751		valueRecord = ValueRecord()
752		for name, isDevice, signed in format:
753			if signed:
754				value = reader.readShort()
755			else:
756				value = reader.readUShort()
757			if isDevice:
758				if value:
759					from . import otTables
760					subReader = reader.getSubReader(value)
761					value = getattr(otTables, name)()
762					value.decompile(subReader, font)
763				else:
764					value = None
765			setattr(valueRecord, name, value)
766		return valueRecord
767
768	def writeValueRecord(self, writer, font, valueRecord):
769		for name, isDevice, signed in self.format:
770			value = getattr(valueRecord, name, 0)
771			if isDevice:
772				if value:
773					subWriter = writer.getSubWriter()
774					writer.writeSubTable(subWriter)
775					value.compile(subWriter, font)
776				else:
777					writer.writeUShort(0)
778			elif signed:
779				writer.writeShort(value)
780			else:
781				writer.writeUShort(value)
782
783
784class ValueRecord(object):
785
786	# see ValueRecordFactory
787
788	def getFormat(self):
789		format = 0
790		for name in self.__dict__.keys():
791			format = format | valueRecordFormatDict[name][0]
792		return format
793
794	def toXML(self, xmlWriter, font, valueName, attrs=None):
795		if attrs is None:
796			simpleItems = []
797		else:
798			simpleItems = list(attrs)
799		for mask, name, isDevice, format in valueRecordFormat[:4]:  # "simple" values
800			if hasattr(self, name):
801				simpleItems.append((name, getattr(self, name)))
802		deviceItems = []
803		for mask, name, isDevice, format in valueRecordFormat[4:8]:  # device records
804			if hasattr(self, name):
805				device = getattr(self, name)
806				if device is not None:
807					deviceItems.append((name, device))
808		if deviceItems:
809			xmlWriter.begintag(valueName, simpleItems)
810			xmlWriter.newline()
811			for name, deviceRecord in deviceItems:
812				if deviceRecord is not None:
813					deviceRecord.toXML(xmlWriter, font)
814			xmlWriter.endtag(valueName)
815			xmlWriter.newline()
816		else:
817			xmlWriter.simpletag(valueName, simpleItems)
818			xmlWriter.newline()
819
820	def fromXML(self, name, attrs, content, font):
821		from . import otTables
822		for k, v in attrs.items():
823			setattr(self, k, int(v))
824		for element in content:
825			if not isinstance(element, tuple):
826				continue
827			name, attrs, content = element
828			value = getattr(otTables, name)()
829			for elem2 in content:
830				if not isinstance(elem2, tuple):
831					continue
832				name2, attrs2, content2 = elem2
833				value.fromXML(name2, attrs2, content2, font)
834			setattr(self, name, value)
835
836	def __ne__(self, other):
837		return not self.__eq__(other)
838	def __eq__(self, other):
839		if type(self) != type(other):
840			return NotImplemented
841		return self.__dict__ == other.__dict__
842