• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.misc.fixedTools import (
2	fixedToFloat as fi2fl,
3	floatToFixed as fl2fi,
4	floatToFixedToStr as fl2str,
5	strToFixedToFloat as str2fl,
6	ensureVersionIsLong as fi2ve,
7	versionToFixed as ve2fi,
8)
9from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound
10from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval
11from fontTools.ttLib import getSearchRange
12from .otBase import (CountReference, FormatSwitchingBaseTable,
13                     OTTableReader, OTTableWriter, ValueRecordFactory)
14from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
15                       ContextualMorphAction, LigatureMorphAction,
16                       InsertionMorphAction, MorxSubtable,
17                       ExtendMode as _ExtendMode,
18                       CompositeMode as _CompositeMode,
19                       NO_VARIATION_INDEX)
20from itertools import zip_longest
21from functools import partial
22import re
23import struct
24from typing import Optional
25import logging
26
27
28log = logging.getLogger(__name__)
29istuple = lambda t: isinstance(t, tuple)
30
31
32def buildConverters(tableSpec, tableNamespace):
33	"""Given a table spec from otData.py, build a converter object for each
34	field of the table. This is called for each table in otData.py, and
35	the results are assigned to the corresponding class in otTables.py."""
36	converters = []
37	convertersByName = {}
38	for tp, name, repeat, aux, descr in tableSpec:
39		tableName = name
40		if name.startswith("ValueFormat"):
41			assert tp == "uint16"
42			converterClass = ValueFormat
43		elif name.endswith("Count") or name in ("StructLength", "MorphType"):
44			converterClass = {
45				"uint8": ComputedUInt8,
46				"uint16": ComputedUShort,
47				"uint32": ComputedULong,
48			}[tp]
49		elif name == "SubTable":
50			converterClass = SubTable
51		elif name == "ExtSubTable":
52			converterClass = ExtSubTable
53		elif name == "SubStruct":
54			converterClass = SubStruct
55		elif name == "FeatureParams":
56			converterClass = FeatureParams
57		elif name in ("CIDGlyphMapping", "GlyphCIDMapping"):
58			converterClass = StructWithLength
59		else:
60			if not tp in converterMapping and '(' not in tp:
61				tableName = tp
62				converterClass = Struct
63			else:
64				converterClass = eval(tp, tableNamespace, converterMapping)
65
66		conv = converterClass(name, repeat, aux, description=descr)
67
68		if conv.tableClass:
69			# A "template" such as OffsetTo(AType) knowss the table class already
70			tableClass = conv.tableClass
71		elif tp in ('MortChain', 'MortSubtable', 'MorxChain'):
72			tableClass = tableNamespace.get(tp)
73		else:
74			tableClass = tableNamespace.get(tableName)
75
76		if not conv.tableClass:
77			conv.tableClass = tableClass
78
79		if name in ["SubTable", "ExtSubTable", "SubStruct"]:
80			conv.lookupTypes = tableNamespace['lookupTypes']
81			# also create reverse mapping
82			for t in conv.lookupTypes.values():
83				for cls in t.values():
84					convertersByName[cls.__name__] = Table(name, repeat, aux, cls)
85		if name == "FeatureParams":
86			conv.featureParamTypes = tableNamespace['featureParamTypes']
87			conv.defaultFeatureParams = tableNamespace['FeatureParams']
88			for cls in conv.featureParamTypes.values():
89				convertersByName[cls.__name__] = Table(name, repeat, aux, cls)
90		converters.append(conv)
91		assert name not in convertersByName, name
92		convertersByName[name] = conv
93	return converters, convertersByName
94
95
96class _MissingItem(tuple):
97	__slots__ = ()
98
99
100try:
101	from collections import UserList
102except ImportError:
103	from UserList import UserList
104
105
106class _LazyList(UserList):
107
108	def __getslice__(self, i, j):
109		return self.__getitem__(slice(i, j))
110
111	def __getitem__(self, k):
112		if isinstance(k, slice):
113			indices = range(*k.indices(len(self)))
114			return [self[i] for i in indices]
115		item = self.data[k]
116		if isinstance(item, _MissingItem):
117			self.reader.seek(self.pos + item[0] * self.recordSize)
118			item = self.conv.read(self.reader, self.font, {})
119			self.data[k] = item
120		return item
121
122	def __add__(self, other):
123		if isinstance(other, _LazyList):
124			other = list(other)
125		elif isinstance(other, list):
126			pass
127		else:
128			return NotImplemented
129		return list(self) + other
130
131	def __radd__(self, other):
132		if not isinstance(other, list):
133			return NotImplemented
134		return other + list(self)
135
136
137class BaseConverter(object):
138
139	"""Base class for converter objects. Apart from the constructor, this
140	is an abstract class."""
141
142	def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
143		self.name = name
144		self.repeat = repeat
145		self.aux = aux
146		self.tableClass = tableClass
147		self.isCount = name.endswith("Count") or name in ['DesignAxisRecordSize', 'ValueRecordSize']
148		self.isLookupType = name.endswith("LookupType") or name == "MorphType"
149		self.isPropagated = name in [
150			"ClassCount",
151			"Class2Count",
152			"FeatureTag",
153			"SettingsCount",
154			"VarRegionCount",
155			"MappingCount",
156			"RegionAxisCount",
157			"DesignAxisCount",
158			"DesignAxisRecordSize",
159			"AxisValueCount",
160			"ValueRecordSize",
161			"AxisCount",
162			"BaseGlyphRecordCount",
163			"LayerRecordCount",
164		]
165		self.description = description
166
167	def readArray(self, reader, font, tableDict, count):
168		"""Read an array of values from the reader."""
169		lazy = font.lazy and count > 8
170		if lazy:
171			recordSize = self.getRecordSize(reader)
172			if recordSize is NotImplemented:
173				lazy = False
174		if not lazy:
175			l = []
176			for i in range(count):
177				l.append(self.read(reader, font, tableDict))
178			return l
179		else:
180			l = _LazyList()
181			l.reader = reader.copy()
182			l.pos = l.reader.pos
183			l.font = font
184			l.conv = self
185			l.recordSize = recordSize
186			l.extend(_MissingItem([i]) for i in range(count))
187			reader.advance(count * recordSize)
188			return l
189
190	def getRecordSize(self, reader):
191		if hasattr(self, 'staticSize'): return self.staticSize
192		return NotImplemented
193
194	def read(self, reader, font, tableDict):
195		"""Read a value from the reader."""
196		raise NotImplementedError(self)
197
198	def writeArray(self, writer, font, tableDict, values):
199		try:
200			for i, value in enumerate(values):
201				self.write(writer, font, tableDict, value, i)
202		except Exception as e:
203			e.args = e.args + (i,)
204			raise
205
206	def write(self, writer, font, tableDict, value, repeatIndex=None):
207		"""Write a value to the writer."""
208		raise NotImplementedError(self)
209
210	def xmlRead(self, attrs, content, font):
211		"""Read a value from XML."""
212		raise NotImplementedError(self)
213
214	def xmlWrite(self, xmlWriter, font, value, name, attrs):
215		"""Write a value to XML."""
216		raise NotImplementedError(self)
217
218	varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)")
219
220	def getVarIndexOffset(self) -> Optional[int]:
221		"""If description has `VarIndexBase + {offset}`, return the offset else None."""
222		m = self.varIndexBasePlusOffsetRE.search(self.description)
223		if not m:
224			return None
225		return int(m.group(1))
226
227
228class SimpleValue(BaseConverter):
229	@staticmethod
230	def toString(value):
231		return value
232	@staticmethod
233	def fromString(value):
234		return value
235	def xmlWrite(self, xmlWriter, font, value, name, attrs):
236		xmlWriter.simpletag(name, attrs + [("value", self.toString(value))])
237		xmlWriter.newline()
238	def xmlRead(self, attrs, content, font):
239		return self.fromString(attrs["value"])
240
241class OptionalValue(SimpleValue):
242	DEFAULT = None
243	def xmlWrite(self, xmlWriter, font, value, name, attrs):
244		if value != self.DEFAULT:
245			attrs.append(("value", self.toString(value)))
246		xmlWriter.simpletag(name, attrs)
247		xmlWriter.newline()
248	def xmlRead(self, attrs, content, font):
249		if "value" in attrs:
250			return self.fromString(attrs["value"])
251		return self.DEFAULT
252
253class IntValue(SimpleValue):
254	@staticmethod
255	def fromString(value):
256		return int(value, 0)
257
258class Long(IntValue):
259	staticSize = 4
260	def read(self, reader, font, tableDict):
261		return reader.readLong()
262	def readArray(self, reader, font, tableDict, count):
263		return reader.readLongArray(count)
264	def write(self, writer, font, tableDict, value, repeatIndex=None):
265		writer.writeLong(value)
266	def writeArray(self, writer, font, tableDict, values):
267		writer.writeLongArray(values)
268
269class ULong(IntValue):
270	staticSize = 4
271	def read(self, reader, font, tableDict):
272		return reader.readULong()
273	def readArray(self, reader, font, tableDict, count):
274		return reader.readULongArray(count)
275	def write(self, writer, font, tableDict, value, repeatIndex=None):
276		writer.writeULong(value)
277	def writeArray(self, writer, font, tableDict, values):
278		writer.writeULongArray(values)
279
280class Flags32(ULong):
281	@staticmethod
282	def toString(value):
283		return "0x%08X" % value
284
285class VarIndex(OptionalValue, ULong):
286	DEFAULT = NO_VARIATION_INDEX
287
288class Short(IntValue):
289	staticSize = 2
290	def read(self, reader, font, tableDict):
291		return reader.readShort()
292	def readArray(self, reader, font, tableDict, count):
293		return reader.readShortArray(count)
294	def write(self, writer, font, tableDict, value, repeatIndex=None):
295		writer.writeShort(value)
296	def writeArray(self, writer, font, tableDict, values):
297		writer.writeShortArray(values)
298
299class UShort(IntValue):
300	staticSize = 2
301	def read(self, reader, font, tableDict):
302		return reader.readUShort()
303	def readArray(self, reader, font, tableDict, count):
304		return reader.readUShortArray(count)
305	def write(self, writer, font, tableDict, value, repeatIndex=None):
306		writer.writeUShort(value)
307	def writeArray(self, writer, font, tableDict, values):
308		writer.writeUShortArray(values)
309
310class Int8(IntValue):
311	staticSize = 1
312	def read(self, reader, font, tableDict):
313		return reader.readInt8()
314	def readArray(self, reader, font, tableDict, count):
315		return reader.readInt8Array(count)
316	def write(self, writer, font, tableDict, value, repeatIndex=None):
317		writer.writeInt8(value)
318	def writeArray(self, writer, font, tableDict, values):
319		writer.writeInt8Array(values)
320
321class UInt8(IntValue):
322	staticSize = 1
323	def read(self, reader, font, tableDict):
324		return reader.readUInt8()
325	def readArray(self, reader, font, tableDict, count):
326		return reader.readUInt8Array(count)
327	def write(self, writer, font, tableDict, value, repeatIndex=None):
328		writer.writeUInt8(value)
329	def writeArray(self, writer, font, tableDict, values):
330		writer.writeUInt8Array(values)
331
332class UInt24(IntValue):
333	staticSize = 3
334	def read(self, reader, font, tableDict):
335		return reader.readUInt24()
336	def write(self, writer, font, tableDict, value, repeatIndex=None):
337		writer.writeUInt24(value)
338
339class ComputedInt(IntValue):
340	def xmlWrite(self, xmlWriter, font, value, name, attrs):
341		if value is not None:
342			xmlWriter.comment("%s=%s" % (name, value))
343			xmlWriter.newline()
344
345class ComputedUInt8(ComputedInt, UInt8):
346	pass
347class ComputedUShort(ComputedInt, UShort):
348	pass
349class ComputedULong(ComputedInt, ULong):
350	pass
351
352class Tag(SimpleValue):
353	staticSize = 4
354	def read(self, reader, font, tableDict):
355		return reader.readTag()
356	def write(self, writer, font, tableDict, value, repeatIndex=None):
357		writer.writeTag(value)
358
359class GlyphID(SimpleValue):
360	staticSize = 2
361	typecode = "H"
362	def readArray(self, reader, font, tableDict, count):
363		return font.getGlyphNameMany(reader.readArray(self.typecode, self.staticSize, count))
364	def read(self, reader, font, tableDict):
365		return font.getGlyphName(reader.readValue(self.typecode, self.staticSize))
366	def writeArray(self, writer, font, tableDict, values):
367		writer.writeArray(self.typecode, font.getGlyphIDMany(values))
368	def write(self, writer, font, tableDict, value, repeatIndex=None):
369		writer.writeValue(self.typecode, font.getGlyphID(value))
370
371
372class GlyphID32(GlyphID):
373	staticSize = 4
374	typecode = "L"
375
376
377class NameID(UShort):
378	def xmlWrite(self, xmlWriter, font, value, name, attrs):
379		xmlWriter.simpletag(name, attrs + [("value", value)])
380		if font and value:
381			nameTable = font.get("name")
382			if nameTable:
383				name = nameTable.getDebugName(value)
384				xmlWriter.write("  ")
385				if name:
386					xmlWriter.comment(name)
387				else:
388					xmlWriter.comment("missing from name table")
389					log.warning("name id %d missing from name table" % value)
390		xmlWriter.newline()
391
392class STATFlags(UShort):
393	def xmlWrite(self, xmlWriter, font, value, name, attrs):
394		xmlWriter.simpletag(name, attrs + [("value", value)])
395		flags = []
396		if value & 0x01:
397			flags.append("OlderSiblingFontAttribute")
398		if value & 0x02:
399			flags.append("ElidableAxisValueName")
400		if flags:
401			xmlWriter.write("  ")
402			xmlWriter.comment(" ".join(flags))
403		xmlWriter.newline()
404
405class FloatValue(SimpleValue):
406	@staticmethod
407	def fromString(value):
408		return float(value)
409
410class DeciPoints(FloatValue):
411	staticSize = 2
412	def read(self, reader, font, tableDict):
413		return reader.readUShort() / 10
414
415	def write(self, writer, font, tableDict, value, repeatIndex=None):
416		writer.writeUShort(round(value * 10))
417
418class BaseFixedValue(FloatValue):
419	staticSize = NotImplemented
420	precisionBits = NotImplemented
421	readerMethod = NotImplemented
422	writerMethod = NotImplemented
423	def read(self, reader, font, tableDict):
424		return  self.fromInt(getattr(reader, self.readerMethod)())
425	def write(self, writer, font, tableDict, value, repeatIndex=None):
426		getattr(writer, self.writerMethod)(self.toInt(value))
427	@classmethod
428	def fromInt(cls, value):
429		return fi2fl(value, cls.precisionBits)
430	@classmethod
431	def toInt(cls, value):
432		return fl2fi(value, cls.precisionBits)
433	@classmethod
434	def fromString(cls, value):
435		return str2fl(value, cls.precisionBits)
436	@classmethod
437	def toString(cls, value):
438		return fl2str(value, cls.precisionBits)
439
440class Fixed(BaseFixedValue):
441	staticSize = 4
442	precisionBits = 16
443	readerMethod = "readLong"
444	writerMethod = "writeLong"
445
446class F2Dot14(BaseFixedValue):
447	staticSize = 2
448	precisionBits = 14
449	readerMethod = "readShort"
450	writerMethod = "writeShort"
451
452class Angle(F2Dot14):
453	# angles are specified in degrees, and encoded as F2Dot14 fractions of half
454	# circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc.
455	bias = 0.0
456	factor = 1.0/(1<<14) * 180  # 0.010986328125
457	@classmethod
458	def fromInt(cls, value):
459		return (super().fromInt(value) + cls.bias) * 180
460	@classmethod
461	def toInt(cls, value):
462		return super().toInt((value / 180) - cls.bias)
463	@classmethod
464	def fromString(cls, value):
465		# quantize to nearest multiples of minimum fixed-precision angle
466		return otRound(float(value) / cls.factor) * cls.factor
467	@classmethod
468	def toString(cls, value):
469		return nearestMultipleShortestRepr(value, cls.factor)
470
471class BiasedAngle(Angle):
472	# A bias of 1.0 is used in the representation of start and end angles
473	# of COLRv1 PaintSweepGradients to allow for encoding +360deg
474	bias = 1.0
475
476class Version(SimpleValue):
477	staticSize = 4
478	def read(self, reader, font, tableDict):
479		value = reader.readLong()
480		assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
481		return value
482	def write(self, writer, font, tableDict, value, repeatIndex=None):
483		value = fi2ve(value)
484		assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
485		writer.writeLong(value)
486	@staticmethod
487	def fromString(value):
488		return ve2fi(value)
489	@staticmethod
490	def toString(value):
491		return "0x%08x" % value
492	@staticmethod
493	def fromFloat(v):
494		return fl2fi(v, 16)
495
496
497class Char64(SimpleValue):
498	"""An ASCII string with up to 64 characters.
499
500	Unused character positions are filled with 0x00 bytes.
501	Used in Apple AAT fonts in the `gcid` table.
502	"""
503	staticSize = 64
504
505	def read(self, reader, font, tableDict):
506		data = reader.readData(self.staticSize)
507		zeroPos = data.find(b"\0")
508		if zeroPos >= 0:
509			data = data[:zeroPos]
510		s = tostr(data, encoding="ascii", errors="replace")
511		if s != tostr(data, encoding="ascii", errors="ignore"):
512			log.warning('replaced non-ASCII characters in "%s"' %
513			            s)
514		return s
515
516	def write(self, writer, font, tableDict, value, repeatIndex=None):
517		data = tobytes(value, encoding="ascii", errors="replace")
518		if data != tobytes(value, encoding="ascii", errors="ignore"):
519			log.warning('replacing non-ASCII characters in "%s"' %
520			            value)
521		if len(data) > self.staticSize:
522			log.warning('truncating overlong "%s" to %d bytes' %
523			            (value, self.staticSize))
524		data = (data + b"\0" * self.staticSize)[:self.staticSize]
525		writer.writeData(data)
526
527
528class Struct(BaseConverter):
529
530	def getRecordSize(self, reader):
531		return self.tableClass and self.tableClass.getRecordSize(reader)
532
533	def read(self, reader, font, tableDict):
534		table = self.tableClass()
535		table.decompile(reader, font)
536		return table
537
538	def write(self, writer, font, tableDict, value, repeatIndex=None):
539		value.compile(writer, font)
540
541	def xmlWrite(self, xmlWriter, font, value, name, attrs):
542		if value is None:
543			if attrs:
544				# If there are attributes (probably index), then
545				# don't drop this even if it's NULL.  It will mess
546				# up the array indices of the containing element.
547				xmlWriter.simpletag(name, attrs + [("empty", 1)])
548				xmlWriter.newline()
549			else:
550				pass # NULL table, ignore
551		else:
552			value.toXML(xmlWriter, font, attrs, name=name)
553
554	def xmlRead(self, attrs, content, font):
555		if "empty" in attrs and safeEval(attrs["empty"]):
556			return None
557		table = self.tableClass()
558		Format = attrs.get("Format")
559		if Format is not None:
560			table.Format = int(Format)
561
562		noPostRead = not hasattr(table, 'postRead')
563		if noPostRead:
564			# TODO Cache table.hasPropagated.
565			cleanPropagation = False
566			for conv in table.getConverters():
567				if conv.isPropagated:
568					cleanPropagation = True
569					if not hasattr(font, '_propagator'):
570						font._propagator = {}
571					propagator = font._propagator
572					assert conv.name not in propagator, (conv.name, propagator)
573					setattr(table, conv.name, None)
574					propagator[conv.name] = CountReference(table.__dict__, conv.name)
575
576		for element in content:
577			if isinstance(element, tuple):
578				name, attrs, content = element
579				table.fromXML(name, attrs, content, font)
580			else:
581				pass
582
583		table.populateDefaults(propagator=getattr(font, '_propagator', None))
584
585		if noPostRead:
586			if cleanPropagation:
587				for conv in table.getConverters():
588					if conv.isPropagated:
589						propagator = font._propagator
590						del propagator[conv.name]
591						if not propagator:
592							del font._propagator
593
594		return table
595
596	def __repr__(self):
597		return "Struct of " + repr(self.tableClass)
598
599
600class StructWithLength(Struct):
601	def read(self, reader, font, tableDict):
602		pos = reader.pos
603		table = self.tableClass()
604		table.decompile(reader, font)
605		reader.seek(pos + table.StructLength)
606		return table
607
608	def write(self, writer, font, tableDict, value, repeatIndex=None):
609		for convIndex, conv in enumerate(value.getConverters()):
610			if conv.name == "StructLength":
611				break
612		lengthIndex = len(writer.items) + convIndex
613		if isinstance(value, FormatSwitchingBaseTable):
614			lengthIndex += 1  # implicit Format field
615		deadbeef = {1:0xDE, 2:0xDEAD, 4:0xDEADBEEF}[conv.staticSize]
616
617		before = writer.getDataLength()
618		value.StructLength = deadbeef
619		value.compile(writer, font)
620		length = writer.getDataLength() - before
621		lengthWriter = writer.getSubWriter()
622		conv.write(lengthWriter, font, tableDict, length)
623		assert(writer.items[lengthIndex] ==
624		       b"\xde\xad\xbe\xef"[:conv.staticSize])
625		writer.items[lengthIndex] = lengthWriter.getAllData()
626
627
628class Table(Struct):
629
630	staticSize = 2
631
632	def readOffset(self, reader):
633		return reader.readUShort()
634
635	def writeNullOffset(self, writer):
636		writer.writeUShort(0)
637
638	def read(self, reader, font, tableDict):
639		offset = self.readOffset(reader)
640		if offset == 0:
641			return None
642		table = self.tableClass()
643		reader = reader.getSubReader(offset)
644		if font.lazy:
645			table.reader = reader
646			table.font = font
647		else:
648			table.decompile(reader, font)
649		return table
650
651	def write(self, writer, font, tableDict, value, repeatIndex=None):
652		if value is None:
653			self.writeNullOffset(writer)
654		else:
655			subWriter = writer.getSubWriter(offsetSize=self.staticSize)
656			subWriter.name = self.name
657			if repeatIndex is not None:
658				subWriter.repeatIndex = repeatIndex
659			writer.writeSubTable(subWriter)
660			value.compile(subWriter, font)
661
662class LTable(Table):
663
664	staticSize = 4
665
666	def readOffset(self, reader):
667		return reader.readULong()
668
669	def writeNullOffset(self, writer):
670		writer.writeULong(0)
671
672
673# Table pointed to by a 24-bit, 3-byte long offset
674class Table24(Table):
675
676	staticSize = 3
677
678	def readOffset(self, reader):
679		return reader.readUInt24()
680
681	def writeNullOffset(self, writer):
682		writer.writeUInt24(0)
683
684
685# TODO Clean / merge the SubTable and SubStruct
686
687class SubStruct(Struct):
688	def getConverter(self, tableType, lookupType):
689		tableClass = self.lookupTypes[tableType][lookupType]
690		return self.__class__(self.name, self.repeat, self.aux, tableClass)
691
692	def xmlWrite(self, xmlWriter, font, value, name, attrs):
693		super(SubStruct, self).xmlWrite(xmlWriter, font, value, None, attrs)
694
695class SubTable(Table):
696	def getConverter(self, tableType, lookupType):
697		tableClass = self.lookupTypes[tableType][lookupType]
698		return self.__class__(self.name, self.repeat, self.aux, tableClass)
699
700	def xmlWrite(self, xmlWriter, font, value, name, attrs):
701		super(SubTable, self).xmlWrite(xmlWriter, font, value, None, attrs)
702
703class ExtSubTable(LTable, SubTable):
704
705	def write(self, writer, font, tableDict, value, repeatIndex=None):
706		writer.Extension = True # actually, mere presence of the field flags it as an Ext Subtable writer.
707		Table.write(self, writer, font, tableDict, value, repeatIndex)
708
709
710class FeatureParams(Table):
711	def getConverter(self, featureTag):
712		tableClass = self.featureParamTypes.get(featureTag, self.defaultFeatureParams)
713		return self.__class__(self.name, self.repeat, self.aux, tableClass)
714
715
716class ValueFormat(IntValue):
717	staticSize = 2
718	def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
719		BaseConverter.__init__(
720			self, name, repeat, aux, tableClass, description=description
721		)
722		self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1")
723	def read(self, reader, font, tableDict):
724		format = reader.readUShort()
725		reader[self.which] = ValueRecordFactory(format)
726		return format
727	def write(self, writer, font, tableDict, format, repeatIndex=None):
728		writer.writeUShort(format)
729		writer[self.which] = ValueRecordFactory(format)
730
731
732class ValueRecord(ValueFormat):
733	def getRecordSize(self, reader):
734		return 2 * len(reader[self.which])
735	def read(self, reader, font, tableDict):
736		return reader[self.which].readValueRecord(reader, font)
737	def write(self, writer, font, tableDict, value, repeatIndex=None):
738		writer[self.which].writeValueRecord(writer, font, value)
739	def xmlWrite(self, xmlWriter, font, value, name, attrs):
740		if value is None:
741			pass  # NULL table, ignore
742		else:
743			value.toXML(xmlWriter, font, self.name, attrs)
744	def xmlRead(self, attrs, content, font):
745		from .otBase import ValueRecord
746		value = ValueRecord()
747		value.fromXML(None, attrs, content, font)
748		return value
749
750
751class AATLookup(BaseConverter):
752	BIN_SEARCH_HEADER_SIZE = 10
753
754	def __init__(self, name, repeat, aux, tableClass, *, description=""):
755		BaseConverter.__init__(
756			self, name, repeat, aux, tableClass, description=description
757		)
758		if issubclass(self.tableClass, SimpleValue):
759			self.converter = self.tableClass(name='Value', repeat=None, aux=None)
760		else:
761			self.converter = Table(name='Value', repeat=None, aux=None, tableClass=self.tableClass)
762
763	def read(self, reader, font, tableDict):
764		format = reader.readUShort()
765		if format == 0:
766			return self.readFormat0(reader, font)
767		elif format == 2:
768			return self.readFormat2(reader, font)
769		elif format == 4:
770			return self.readFormat4(reader, font)
771		elif format == 6:
772			return self.readFormat6(reader, font)
773		elif format == 8:
774			return self.readFormat8(reader, font)
775		else:
776			assert False, "unsupported lookup format: %d" % format
777
778	def write(self, writer, font, tableDict, value, repeatIndex=None):
779		values = list(sorted([(font.getGlyphID(glyph), val)
780		                      for glyph, val in value.items()]))
781		# TODO: Also implement format 4.
782		formats = list(sorted(filter(None, [
783			self.buildFormat0(writer, font, values),
784			self.buildFormat2(writer, font, values),
785			self.buildFormat6(writer, font, values),
786			self.buildFormat8(writer, font, values),
787		])))
788		# We use the format ID as secondary sort key to make the output
789		# deterministic when multiple formats have same encoded size.
790		dataSize, lookupFormat, writeMethod = formats[0]
791		pos = writer.getDataLength()
792		writeMethod()
793		actualSize = writer.getDataLength() - pos
794		assert actualSize == dataSize, (
795			"AATLookup format %d claimed to write %d bytes, but wrote %d" %
796			(lookupFormat, dataSize, actualSize))
797
798	@staticmethod
799	def writeBinSearchHeader(writer, numUnits, unitSize):
800		writer.writeUShort(unitSize)
801		writer.writeUShort(numUnits)
802		searchRange, entrySelector, rangeShift = \
803			getSearchRange(n=numUnits, itemSize=unitSize)
804		writer.writeUShort(searchRange)
805		writer.writeUShort(entrySelector)
806		writer.writeUShort(rangeShift)
807
808	def buildFormat0(self, writer, font, values):
809		numGlyphs = len(font.getGlyphOrder())
810		if len(values) != numGlyphs:
811			return None
812		valueSize = self.converter.staticSize
813		return (2 + numGlyphs * valueSize, 0,
814			lambda: self.writeFormat0(writer, font, values))
815
816	def writeFormat0(self, writer, font, values):
817		writer.writeUShort(0)
818		for glyphID_, value in values:
819			self.converter.write(
820				writer, font, tableDict=None,
821				value=value, repeatIndex=None)
822
823	def buildFormat2(self, writer, font, values):
824		segStart, segValue = values[0]
825		segEnd = segStart
826		segments = []
827		for glyphID, curValue in values[1:]:
828			if glyphID != segEnd + 1 or curValue != segValue:
829				segments.append((segStart, segEnd, segValue))
830				segStart = segEnd = glyphID
831				segValue = curValue
832			else:
833				segEnd = glyphID
834		segments.append((segStart, segEnd, segValue))
835		valueSize = self.converter.staticSize
836		numUnits, unitSize = len(segments) + 1, valueSize + 4
837		return (2 + self.BIN_SEARCH_HEADER_SIZE + numUnits * unitSize, 2,
838		        lambda: self.writeFormat2(writer, font, segments))
839
840	def writeFormat2(self, writer, font, segments):
841		writer.writeUShort(2)
842		valueSize = self.converter.staticSize
843		numUnits, unitSize = len(segments), valueSize + 4
844		self.writeBinSearchHeader(writer, numUnits, unitSize)
845		for firstGlyph, lastGlyph, value in segments:
846			writer.writeUShort(lastGlyph)
847			writer.writeUShort(firstGlyph)
848			self.converter.write(
849				writer, font, tableDict=None,
850				value=value, repeatIndex=None)
851		writer.writeUShort(0xFFFF)
852		writer.writeUShort(0xFFFF)
853		writer.writeData(b'\x00' * valueSize)
854
855	def buildFormat6(self, writer, font, values):
856		valueSize = self.converter.staticSize
857		numUnits, unitSize = len(values), valueSize + 2
858		return (2 + self.BIN_SEARCH_HEADER_SIZE + (numUnits + 1) * unitSize, 6,
859			lambda: self.writeFormat6(writer, font, values))
860
861	def writeFormat6(self, writer, font, values):
862		writer.writeUShort(6)
863		valueSize = self.converter.staticSize
864		numUnits, unitSize = len(values), valueSize + 2
865		self.writeBinSearchHeader(writer, numUnits, unitSize)
866		for glyphID, value in values:
867			writer.writeUShort(glyphID)
868			self.converter.write(
869				writer, font, tableDict=None,
870				value=value, repeatIndex=None)
871		writer.writeUShort(0xFFFF)
872		writer.writeData(b'\x00' * valueSize)
873
874	def buildFormat8(self, writer, font, values):
875		minGlyphID, maxGlyphID = values[0][0], values[-1][0]
876		if len(values) != maxGlyphID - minGlyphID + 1:
877			return None
878		valueSize = self.converter.staticSize
879		return (6 + len(values) * valueSize, 8,
880                        lambda: self.writeFormat8(writer, font, values))
881
882	def writeFormat8(self, writer, font, values):
883		firstGlyphID = values[0][0]
884		writer.writeUShort(8)
885		writer.writeUShort(firstGlyphID)
886		writer.writeUShort(len(values))
887		for _, value in values:
888			self.converter.write(
889				writer, font, tableDict=None,
890				value=value, repeatIndex=None)
891
892	def readFormat0(self, reader, font):
893		numGlyphs = len(font.getGlyphOrder())
894		data = self.converter.readArray(
895			reader, font, tableDict=None, count=numGlyphs)
896		return {font.getGlyphName(k): value
897		        for k, value in enumerate(data)}
898
899	def readFormat2(self, reader, font):
900		mapping = {}
901		pos = reader.pos - 2  # start of table is at UShort for format
902		unitSize, numUnits = reader.readUShort(), reader.readUShort()
903		assert unitSize >= 4 + self.converter.staticSize, unitSize
904		for i in range(numUnits):
905			reader.seek(pos + i * unitSize + 12)
906			last = reader.readUShort()
907			first = reader.readUShort()
908			value = self.converter.read(reader, font, tableDict=None)
909			if last != 0xFFFF:
910				for k in range(first, last + 1):
911					mapping[font.getGlyphName(k)] = value
912		return mapping
913
914	def readFormat4(self, reader, font):
915		mapping = {}
916		pos = reader.pos - 2  # start of table is at UShort for format
917		unitSize = reader.readUShort()
918		assert unitSize >= 6, unitSize
919		for i in range(reader.readUShort()):
920			reader.seek(pos + i * unitSize + 12)
921			last = reader.readUShort()
922			first = reader.readUShort()
923			offset = reader.readUShort()
924			if last != 0xFFFF:
925				dataReader = reader.getSubReader(0)  # relative to current position
926				dataReader.seek(pos + offset)  # relative to start of table
927				data = self.converter.readArray(
928					dataReader, font, tableDict=None,
929					count=last - first + 1)
930				for k, v in enumerate(data):
931					mapping[font.getGlyphName(first + k)] = v
932		return mapping
933
934	def readFormat6(self, reader, font):
935		mapping = {}
936		pos = reader.pos - 2  # start of table is at UShort for format
937		unitSize = reader.readUShort()
938		assert unitSize >= 2 + self.converter.staticSize, unitSize
939		for i in range(reader.readUShort()):
940			reader.seek(pos + i * unitSize + 12)
941			glyphID = reader.readUShort()
942			value = self.converter.read(
943				reader, font, tableDict=None)
944			if glyphID != 0xFFFF:
945				mapping[font.getGlyphName(glyphID)] = value
946		return mapping
947
948	def readFormat8(self, reader, font):
949		first = reader.readUShort()
950		count = reader.readUShort()
951		data = self.converter.readArray(
952			reader, font, tableDict=None, count=count)
953		return {font.getGlyphName(first + k): value
954		        for (k, value) in enumerate(data)}
955
956	def xmlRead(self, attrs, content, font):
957		value = {}
958		for element in content:
959			if isinstance(element, tuple):
960				name, a, eltContent = element
961				if name == "Lookup":
962					value[a["glyph"]] = self.converter.xmlRead(a, eltContent, font)
963		return value
964
965	def xmlWrite(self, xmlWriter, font, value, name, attrs):
966		xmlWriter.begintag(name, attrs)
967		xmlWriter.newline()
968		for glyph, value in sorted(value.items()):
969			self.converter.xmlWrite(
970				xmlWriter, font, value=value,
971				name="Lookup", attrs=[("glyph", glyph)])
972		xmlWriter.endtag(name)
973		xmlWriter.newline()
974
975
976# The AAT 'ankr' table has an unusual structure: An offset to an AATLookup
977# followed by an offset to a glyph data table. Other than usual, the
978# offsets in the AATLookup are not relative to the beginning of
979# the beginning of the 'ankr' table, but relative to the glyph data table.
980# So, to find the anchor data for a glyph, one needs to add the offset
981# to the data table to the offset found in the AATLookup, and then use
982# the sum of these two offsets to find the actual data.
983class AATLookupWithDataOffset(BaseConverter):
984	def read(self, reader, font, tableDict):
985		lookupOffset = reader.readULong()
986		dataOffset = reader.readULong()
987		lookupReader = reader.getSubReader(lookupOffset)
988		lookup = AATLookup('DataOffsets', None, None, UShort)
989		offsets = lookup.read(lookupReader, font, tableDict)
990		result = {}
991		for glyph, offset in offsets.items():
992			dataReader = reader.getSubReader(offset + dataOffset)
993			item = self.tableClass()
994			item.decompile(dataReader, font)
995			result[glyph] = item
996		return result
997
998	def write(self, writer, font, tableDict, value, repeatIndex=None):
999		# We do not work with OTTableWriter sub-writers because
1000		# the offsets in our AATLookup are relative to our data
1001		# table, for which we need to provide an offset value itself.
1002		# It might have been possible to somehow make a kludge for
1003		# performing this indirect offset computation directly inside
1004		# OTTableWriter. But this would have made the internal logic
1005		# of OTTableWriter even more complex than it already is,
1006		# so we decided to roll our own offset computation for the
1007		# contents of the AATLookup and associated data table.
1008		offsetByGlyph, offsetByData, dataLen = {}, {}, 0
1009		compiledData = []
1010		for glyph in sorted(value, key=font.getGlyphID):
1011			subWriter = OTTableWriter()
1012			value[glyph].compile(subWriter, font)
1013			data = subWriter.getAllData()
1014			offset = offsetByData.get(data, None)
1015			if offset == None:
1016				offset = dataLen
1017				dataLen = dataLen + len(data)
1018				offsetByData[data] = offset
1019				compiledData.append(data)
1020			offsetByGlyph[glyph] = offset
1021		# For calculating the offsets to our AATLookup and data table,
1022		# we can use the regular OTTableWriter infrastructure.
1023		lookupWriter = writer.getSubWriter(offsetSize=4)
1024		lookup = AATLookup('DataOffsets', None, None, UShort)
1025		lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None)
1026
1027		dataWriter = writer.getSubWriter(offsetSize=4)
1028		writer.writeSubTable(lookupWriter)
1029		writer.writeSubTable(dataWriter)
1030		for d in compiledData:
1031			dataWriter.writeData(d)
1032
1033	def xmlRead(self, attrs, content, font):
1034		lookup = AATLookup('DataOffsets', None, None, self.tableClass)
1035		return lookup.xmlRead(attrs, content, font)
1036
1037	def xmlWrite(self, xmlWriter, font, value, name, attrs):
1038		lookup = AATLookup('DataOffsets', None, None, self.tableClass)
1039		lookup.xmlWrite(xmlWriter, font, value, name, attrs)
1040
1041
1042class MorxSubtableConverter(BaseConverter):
1043	_PROCESSING_ORDERS = {
1044		# bits 30 and 28 of morx.CoverageFlags; see morx spec
1045		(False, False): "LayoutOrder",
1046		(True, False): "ReversedLayoutOrder",
1047		(False, True): "LogicalOrder",
1048		(True, True): "ReversedLogicalOrder",
1049	}
1050
1051	_PROCESSING_ORDERS_REVERSED = {
1052		val: key for key, val in _PROCESSING_ORDERS.items()
1053	}
1054
1055	def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
1056		BaseConverter.__init__(
1057			self, name, repeat, aux, tableClass, description=description
1058		)
1059
1060	def _setTextDirectionFromCoverageFlags(self, flags, subtable):
1061		if (flags & 0x20) != 0:
1062			subtable.TextDirection = "Any"
1063		elif (flags & 0x80) != 0:
1064			subtable.TextDirection = "Vertical"
1065		else:
1066			subtable.TextDirection = "Horizontal"
1067
1068	def read(self, reader, font, tableDict):
1069		pos = reader.pos
1070		m = MorxSubtable()
1071		m.StructLength = reader.readULong()
1072		flags = reader.readUInt8()
1073		orderKey = ((flags & 0x40) != 0, (flags & 0x10) != 0)
1074		m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey]
1075		self._setTextDirectionFromCoverageFlags(flags, m)
1076		m.Reserved = reader.readUShort()
1077		m.Reserved |= (flags & 0xF) << 16
1078		m.MorphType = reader.readUInt8()
1079		m.SubFeatureFlags = reader.readULong()
1080		tableClass = lookupTypes["morx"].get(m.MorphType)
1081		if tableClass is None:
1082			assert False, ("unsupported 'morx' lookup type %s" %
1083			               m.MorphType)
1084		# To decode AAT ligatures, we need to know the subtable size.
1085		# The easiest way to pass this along is to create a new reader
1086		# that works on just the subtable as its data.
1087		headerLength = reader.pos - pos
1088		data = reader.data[
1089			reader.pos
1090			: reader.pos + m.StructLength - headerLength]
1091		assert len(data) == m.StructLength - headerLength
1092		subReader = OTTableReader(data=data, tableTag=reader.tableTag)
1093		m.SubStruct = tableClass()
1094		m.SubStruct.decompile(subReader, font)
1095		reader.seek(pos + m.StructLength)
1096		return m
1097
1098	def xmlWrite(self, xmlWriter, font, value, name, attrs):
1099		xmlWriter.begintag(name, attrs)
1100		xmlWriter.newline()
1101		xmlWriter.comment("StructLength=%d" % value.StructLength)
1102		xmlWriter.newline()
1103		xmlWriter.simpletag("TextDirection", value=value.TextDirection)
1104		xmlWriter.newline()
1105		xmlWriter.simpletag("ProcessingOrder",
1106		                    value=value.ProcessingOrder)
1107		xmlWriter.newline()
1108		if value.Reserved != 0:
1109			xmlWriter.simpletag("Reserved",
1110			                    value="0x%04x" % value.Reserved)
1111			xmlWriter.newline()
1112		xmlWriter.comment("MorphType=%d" % value.MorphType)
1113		xmlWriter.newline()
1114		xmlWriter.simpletag("SubFeatureFlags",
1115		                    value="0x%08x" % value.SubFeatureFlags)
1116		xmlWriter.newline()
1117		value.SubStruct.toXML(xmlWriter, font)
1118		xmlWriter.endtag(name)
1119		xmlWriter.newline()
1120
1121	def xmlRead(self, attrs, content, font):
1122		m = MorxSubtable()
1123		covFlags = 0
1124		m.Reserved = 0
1125		for eltName, eltAttrs, eltContent in filter(istuple, content):
1126			if eltName == "CoverageFlags":
1127				# Only in XML from old versions of fonttools.
1128				covFlags = safeEval(eltAttrs["value"])
1129				orderKey = ((covFlags & 0x40) != 0,
1130				            (covFlags & 0x10) != 0)
1131				m.ProcessingOrder = self._PROCESSING_ORDERS[
1132					orderKey]
1133				self._setTextDirectionFromCoverageFlags(
1134					covFlags, m)
1135			elif eltName == "ProcessingOrder":
1136				m.ProcessingOrder = eltAttrs["value"]
1137				assert m.ProcessingOrder in self._PROCESSING_ORDERS_REVERSED, "unknown ProcessingOrder: %s" % m.ProcessingOrder
1138			elif eltName == "TextDirection":
1139				m.TextDirection = eltAttrs["value"]
1140				assert m.TextDirection in {"Horizontal", "Vertical", "Any"}, "unknown TextDirection %s" % m.TextDirection
1141			elif eltName == "Reserved":
1142				m.Reserved = safeEval(eltAttrs["value"])
1143			elif eltName == "SubFeatureFlags":
1144				m.SubFeatureFlags = safeEval(eltAttrs["value"])
1145			elif eltName.endswith("Morph"):
1146				m.fromXML(eltName, eltAttrs, eltContent, font)
1147			else:
1148				assert False, eltName
1149		m.Reserved = (covFlags & 0xF) << 16 | m.Reserved
1150		return m
1151
1152	def write(self, writer, font, tableDict, value, repeatIndex=None):
1153		covFlags = (value.Reserved & 0x000F0000) >> 16
1154		reverseOrder, logicalOrder = self._PROCESSING_ORDERS_REVERSED[
1155			value.ProcessingOrder]
1156		covFlags |= 0x80 if value.TextDirection == "Vertical" else 0
1157		covFlags |= 0x40 if reverseOrder else 0
1158		covFlags |= 0x20 if value.TextDirection == "Any" else 0
1159		covFlags |= 0x10 if logicalOrder else 0
1160		value.CoverageFlags = covFlags
1161		lengthIndex = len(writer.items)
1162		before = writer.getDataLength()
1163		value.StructLength = 0xdeadbeef
1164		# The high nibble of value.Reserved is actuallly encoded
1165		# into coverageFlags, so we need to clear it here.
1166		origReserved = value.Reserved # including high nibble
1167		value.Reserved = value.Reserved & 0xFFFF # without high nibble
1168		value.compile(writer, font)
1169		value.Reserved = origReserved  # restore original value
1170		assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef"
1171		length = writer.getDataLength() - before
1172		writer.items[lengthIndex] = struct.pack(">L", length)
1173
1174
1175# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader
1176# TODO: Untangle the implementation of the various lookup-specific formats.
1177class STXHeader(BaseConverter):
1178	def __init__(self, name, repeat, aux, tableClass, *, description=""):
1179		BaseConverter.__init__(
1180			self, name, repeat, aux, tableClass, description=description
1181		)
1182		assert issubclass(self.tableClass, AATAction)
1183		self.classLookup = AATLookup("GlyphClasses", None, None, UShort)
1184		if issubclass(self.tableClass, ContextualMorphAction):
1185			self.perGlyphLookup = AATLookup("PerGlyphLookup",
1186			                                None, None, GlyphID)
1187		else:
1188			self.perGlyphLookup = None
1189
1190	def read(self, reader, font, tableDict):
1191		table = AATStateTable()
1192		pos = reader.pos
1193		classTableReader = reader.getSubReader(0)
1194		stateArrayReader = reader.getSubReader(0)
1195		entryTableReader = reader.getSubReader(0)
1196		actionReader = None
1197		ligaturesReader = None
1198		table.GlyphClassCount = reader.readULong()
1199		classTableReader.seek(pos + reader.readULong())
1200		stateArrayReader.seek(pos + reader.readULong())
1201		entryTableReader.seek(pos + reader.readULong())
1202		if self.perGlyphLookup is not None:
1203			perGlyphTableReader = reader.getSubReader(0)
1204			perGlyphTableReader.seek(pos + reader.readULong())
1205		if issubclass(self.tableClass, LigatureMorphAction):
1206			actionReader = reader.getSubReader(0)
1207			actionReader.seek(pos + reader.readULong())
1208			ligComponentReader = reader.getSubReader(0)
1209			ligComponentReader.seek(pos + reader.readULong())
1210			ligaturesReader = reader.getSubReader(0)
1211			ligaturesReader.seek(pos + reader.readULong())
1212			numLigComponents = (ligaturesReader.pos
1213			                    - ligComponentReader.pos) // 2
1214			assert numLigComponents >= 0
1215			table.LigComponents = \
1216				ligComponentReader.readUShortArray(numLigComponents)
1217			table.Ligatures = self._readLigatures(ligaturesReader, font)
1218		elif issubclass(self.tableClass, InsertionMorphAction):
1219			actionReader = reader.getSubReader(0)
1220			actionReader.seek(pos + reader.readULong())
1221		table.GlyphClasses = self.classLookup.read(classTableReader,
1222		                                           font, tableDict)
1223		numStates = int((entryTableReader.pos - stateArrayReader.pos)
1224		                 / (table.GlyphClassCount * 2))
1225		for stateIndex in range(numStates):
1226			state = AATState()
1227			table.States.append(state)
1228			for glyphClass in range(table.GlyphClassCount):
1229				entryIndex = stateArrayReader.readUShort()
1230				state.Transitions[glyphClass] = \
1231					self._readTransition(entryTableReader,
1232					                     entryIndex, font,
1233					                     actionReader)
1234		if self.perGlyphLookup is not None:
1235			table.PerGlyphLookups = self._readPerGlyphLookups(
1236				table, perGlyphTableReader, font)
1237		return table
1238
1239	def _readTransition(self, reader, entryIndex, font, actionReader):
1240		transition = self.tableClass()
1241		entryReader = reader.getSubReader(
1242			reader.pos + entryIndex * transition.staticSize)
1243		transition.decompile(entryReader, font, actionReader)
1244		return transition
1245
1246	def _readLigatures(self, reader, font):
1247		limit = len(reader.data)
1248		numLigatureGlyphs = (limit - reader.pos) // 2
1249		return font.getGlyphNameMany(reader.readUShortArray(numLigatureGlyphs))
1250
1251	def _countPerGlyphLookups(self, table):
1252		# Somewhat annoyingly, the morx table does not encode
1253		# the size of the per-glyph table. So we need to find
1254		# the maximum value that MorphActions use as index
1255		# into this table.
1256		numLookups = 0
1257		for state in table.States:
1258			for t in state.Transitions.values():
1259				if isinstance(t, ContextualMorphAction):
1260					if t.MarkIndex != 0xFFFF:
1261						numLookups = max(
1262							numLookups,
1263							t.MarkIndex + 1)
1264					if t.CurrentIndex != 0xFFFF:
1265						numLookups = max(
1266							numLookups,
1267							t.CurrentIndex + 1)
1268		return numLookups
1269
1270	def _readPerGlyphLookups(self, table, reader, font):
1271		pos = reader.pos
1272		lookups = []
1273		for _ in range(self._countPerGlyphLookups(table)):
1274			lookupReader = reader.getSubReader(0)
1275			lookupReader.seek(pos + reader.readULong())
1276			lookups.append(
1277				self.perGlyphLookup.read(lookupReader, font, {}))
1278		return lookups
1279
1280	def write(self, writer, font, tableDict, value, repeatIndex=None):
1281		glyphClassWriter = OTTableWriter()
1282		self.classLookup.write(glyphClassWriter, font, tableDict,
1283		                       value.GlyphClasses, repeatIndex=None)
1284		glyphClassData = pad(glyphClassWriter.getAllData(), 2)
1285		glyphClassCount = max(value.GlyphClasses.values()) + 1
1286		glyphClassTableOffset = 16  # size of STXHeader
1287		if self.perGlyphLookup is not None:
1288			glyphClassTableOffset += 4
1289
1290		glyphClassTableOffset += self.tableClass.actionHeaderSize
1291		actionData, actionIndex = \
1292			self.tableClass.compileActions(font, value.States)
1293		stateArrayData, entryTableData = self._compileStates(
1294			font, value.States, glyphClassCount, actionIndex)
1295		stateArrayOffset = glyphClassTableOffset + len(glyphClassData)
1296		entryTableOffset = stateArrayOffset + len(stateArrayData)
1297		perGlyphOffset = entryTableOffset + len(entryTableData)
1298		perGlyphData = \
1299			pad(self._compilePerGlyphLookups(value, font), 4)
1300		if actionData is not None:
1301			actionOffset = entryTableOffset + len(entryTableData)
1302		else:
1303			actionOffset = None
1304
1305		ligaturesOffset, ligComponentsOffset = None, None
1306		ligComponentsData = self._compileLigComponents(value, font)
1307		ligaturesData = self._compileLigatures(value, font)
1308		if ligComponentsData is not None:
1309			assert len(perGlyphData) == 0
1310			ligComponentsOffset = actionOffset + len(actionData)
1311			ligaturesOffset = ligComponentsOffset + len(ligComponentsData)
1312
1313		writer.writeULong(glyphClassCount)
1314		writer.writeULong(glyphClassTableOffset)
1315		writer.writeULong(stateArrayOffset)
1316		writer.writeULong(entryTableOffset)
1317		if self.perGlyphLookup is not None:
1318			writer.writeULong(perGlyphOffset)
1319		if actionOffset is not None:
1320			writer.writeULong(actionOffset)
1321		if ligComponentsOffset is not None:
1322			writer.writeULong(ligComponentsOffset)
1323			writer.writeULong(ligaturesOffset)
1324		writer.writeData(glyphClassData)
1325		writer.writeData(stateArrayData)
1326		writer.writeData(entryTableData)
1327		writer.writeData(perGlyphData)
1328		if actionData is not None:
1329			writer.writeData(actionData)
1330		if ligComponentsData is not None:
1331			writer.writeData(ligComponentsData)
1332		if ligaturesData is not None:
1333			writer.writeData(ligaturesData)
1334
1335	def _compileStates(self, font, states, glyphClassCount, actionIndex):
1336		stateArrayWriter = OTTableWriter()
1337		entries, entryIDs = [], {}
1338		for state in states:
1339			for glyphClass in range(glyphClassCount):
1340				transition = state.Transitions[glyphClass]
1341				entryWriter = OTTableWriter()
1342				transition.compile(entryWriter, font,
1343				                   actionIndex)
1344				entryData = entryWriter.getAllData()
1345				assert len(entryData)  == transition.staticSize, ( \
1346					"%s has staticSize %d, "
1347					"but actually wrote %d bytes" % (
1348						repr(transition),
1349						transition.staticSize,
1350						len(entryData)))
1351				entryIndex = entryIDs.get(entryData)
1352				if entryIndex is None:
1353					entryIndex = len(entries)
1354					entryIDs[entryData] = entryIndex
1355					entries.append(entryData)
1356				stateArrayWriter.writeUShort(entryIndex)
1357		stateArrayData = pad(stateArrayWriter.getAllData(), 4)
1358		entryTableData = pad(bytesjoin(entries), 4)
1359		return stateArrayData, entryTableData
1360
1361	def _compilePerGlyphLookups(self, table, font):
1362		if self.perGlyphLookup is None:
1363			return b""
1364		numLookups = self._countPerGlyphLookups(table)
1365		assert len(table.PerGlyphLookups) == numLookups, (
1366			"len(AATStateTable.PerGlyphLookups) is %d, "
1367			"but the actions inside the table refer to %d" %
1368				(len(table.PerGlyphLookups), numLookups))
1369		writer = OTTableWriter()
1370		for lookup in table.PerGlyphLookups:
1371			lookupWriter = writer.getSubWriter(offsetSize=4)
1372			self.perGlyphLookup.write(lookupWriter, font,
1373			                          {}, lookup, None)
1374			writer.writeSubTable(lookupWriter)
1375		return writer.getAllData()
1376
1377	def _compileLigComponents(self, table, font):
1378		if not hasattr(table, "LigComponents"):
1379			return None
1380		writer = OTTableWriter()
1381		for component in table.LigComponents:
1382			writer.writeUShort(component)
1383		return writer.getAllData()
1384
1385	def _compileLigatures(self, table, font):
1386		if not hasattr(table, "Ligatures"):
1387			return None
1388		writer = OTTableWriter()
1389		for glyphName in table.Ligatures:
1390			writer.writeUShort(font.getGlyphID(glyphName))
1391		return writer.getAllData()
1392
1393	def xmlWrite(self, xmlWriter, font, value, name, attrs):
1394		xmlWriter.begintag(name, attrs)
1395		xmlWriter.newline()
1396		xmlWriter.comment("GlyphClassCount=%s" %value.GlyphClassCount)
1397		xmlWriter.newline()
1398		for g, klass in sorted(value.GlyphClasses.items()):
1399			xmlWriter.simpletag("GlyphClass", glyph=g, value=klass)
1400			xmlWriter.newline()
1401		for stateIndex, state in enumerate(value.States):
1402			xmlWriter.begintag("State", index=stateIndex)
1403			xmlWriter.newline()
1404			for glyphClass, trans in sorted(state.Transitions.items()):
1405				trans.toXML(xmlWriter, font=font,
1406				            attrs={"onGlyphClass": glyphClass},
1407				            name="Transition")
1408			xmlWriter.endtag("State")
1409			xmlWriter.newline()
1410		for i, lookup in enumerate(value.PerGlyphLookups):
1411			xmlWriter.begintag("PerGlyphLookup", index=i)
1412			xmlWriter.newline()
1413			for glyph, val in sorted(lookup.items()):
1414				xmlWriter.simpletag("Lookup", glyph=glyph,
1415				                    value=val)
1416				xmlWriter.newline()
1417			xmlWriter.endtag("PerGlyphLookup")
1418			xmlWriter.newline()
1419		if hasattr(value, "LigComponents"):
1420			xmlWriter.begintag("LigComponents")
1421			xmlWriter.newline()
1422			for i, val in enumerate(getattr(value, "LigComponents")):
1423				xmlWriter.simpletag("LigComponent", index=i,
1424				                    value=val)
1425				xmlWriter.newline()
1426			xmlWriter.endtag("LigComponents")
1427			xmlWriter.newline()
1428		self._xmlWriteLigatures(xmlWriter, font, value, name, attrs)
1429		xmlWriter.endtag(name)
1430		xmlWriter.newline()
1431
1432	def _xmlWriteLigatures(self, xmlWriter, font, value, name, attrs):
1433		if not hasattr(value, "Ligatures"):
1434			return
1435		xmlWriter.begintag("Ligatures")
1436		xmlWriter.newline()
1437		for i, g in enumerate(getattr(value, "Ligatures")):
1438			xmlWriter.simpletag("Ligature", index=i, glyph=g)
1439			xmlWriter.newline()
1440		xmlWriter.endtag("Ligatures")
1441		xmlWriter.newline()
1442
1443	def xmlRead(self, attrs, content, font):
1444		table = AATStateTable()
1445		for eltName, eltAttrs, eltContent in filter(istuple, content):
1446			if eltName == "GlyphClass":
1447				glyph = eltAttrs["glyph"]
1448				value = eltAttrs["value"]
1449				table.GlyphClasses[glyph] = safeEval(value)
1450			elif eltName == "State":
1451				state = self._xmlReadState(eltAttrs, eltContent, font)
1452				table.States.append(state)
1453			elif eltName == "PerGlyphLookup":
1454				lookup = self.perGlyphLookup.xmlRead(
1455					eltAttrs, eltContent, font)
1456				table.PerGlyphLookups.append(lookup)
1457			elif eltName == "LigComponents":
1458				table.LigComponents = \
1459					self._xmlReadLigComponents(
1460						eltAttrs, eltContent, font)
1461			elif eltName == "Ligatures":
1462				table.Ligatures = \
1463					self._xmlReadLigatures(
1464						eltAttrs, eltContent, font)
1465		table.GlyphClassCount = max(table.GlyphClasses.values()) + 1
1466		return table
1467
1468	def _xmlReadState(self, attrs, content, font):
1469		state = AATState()
1470		for eltName, eltAttrs, eltContent in filter(istuple, content):
1471			if eltName == "Transition":
1472				glyphClass = safeEval(eltAttrs["onGlyphClass"])
1473				transition = self.tableClass()
1474				transition.fromXML(eltName, eltAttrs,
1475				                   eltContent, font)
1476				state.Transitions[glyphClass] = transition
1477		return state
1478
1479	def _xmlReadLigComponents(self, attrs, content, font):
1480		ligComponents = []
1481		for eltName, eltAttrs, _eltContent in filter(istuple, content):
1482			if eltName == "LigComponent":
1483				ligComponents.append(
1484					safeEval(eltAttrs["value"]))
1485		return ligComponents
1486
1487	def _xmlReadLigatures(self, attrs, content, font):
1488		ligs = []
1489		for eltName, eltAttrs, _eltContent in filter(istuple, content):
1490			if eltName == "Ligature":
1491				ligs.append(eltAttrs["glyph"])
1492		return ligs
1493
1494
1495class CIDGlyphMap(BaseConverter):
1496	def read(self, reader, font, tableDict):
1497		numCIDs = reader.readUShort()
1498		result = {}
1499		for cid, glyphID in enumerate(reader.readUShortArray(numCIDs)):
1500			if glyphID != 0xFFFF:
1501				result[cid] = font.getGlyphName(glyphID)
1502		return result
1503
1504	def write(self, writer, font, tableDict, value, repeatIndex=None):
1505		items = {cid: font.getGlyphID(glyph)
1506		         for cid, glyph in value.items()}
1507		count = max(items) + 1 if items else 0
1508		writer.writeUShort(count)
1509		for cid in range(count):
1510			writer.writeUShort(items.get(cid, 0xFFFF))
1511
1512	def xmlRead(self, attrs, content, font):
1513		result = {}
1514		for eName, eAttrs, _eContent in filter(istuple, content):
1515			if eName == "CID":
1516				result[safeEval(eAttrs["cid"])] = \
1517					eAttrs["glyph"].strip()
1518		return result
1519
1520	def xmlWrite(self, xmlWriter, font, value, name, attrs):
1521		xmlWriter.begintag(name, attrs)
1522		xmlWriter.newline()
1523		for cid, glyph in sorted(value.items()):
1524			if glyph is not None and glyph != 0xFFFF:
1525				xmlWriter.simpletag(
1526					"CID", cid=cid, glyph=glyph)
1527				xmlWriter.newline()
1528		xmlWriter.endtag(name)
1529		xmlWriter.newline()
1530
1531
1532class GlyphCIDMap(BaseConverter):
1533	def read(self, reader, font, tableDict):
1534		glyphOrder = font.getGlyphOrder()
1535		count = reader.readUShort()
1536		cids = reader.readUShortArray(count)
1537		if count > len(glyphOrder):
1538			log.warning("GlyphCIDMap has %d elements, "
1539			            "but the font has only %d glyphs; "
1540			            "ignoring the rest" %
1541			             (count, len(glyphOrder)))
1542		result = {}
1543		for glyphID in range(min(len(cids), len(glyphOrder))):
1544			cid = cids[glyphID]
1545			if cid != 0xFFFF:
1546				result[glyphOrder[glyphID]] = cid
1547		return result
1548
1549	def write(self, writer, font, tableDict, value, repeatIndex=None):
1550		items = {font.getGlyphID(g): cid
1551		         for g, cid in value.items()
1552		         if cid is not None and cid != 0xFFFF}
1553		count = max(items) + 1 if items else 0
1554		writer.writeUShort(count)
1555		for glyphID in range(count):
1556			writer.writeUShort(items.get(glyphID, 0xFFFF))
1557
1558	def xmlRead(self, attrs, content, font):
1559		result = {}
1560		for eName, eAttrs, _eContent in filter(istuple, content):
1561			if eName == "CID":
1562				result[eAttrs["glyph"]] = \
1563					safeEval(eAttrs["value"])
1564		return result
1565
1566	def xmlWrite(self, xmlWriter, font, value, name, attrs):
1567		xmlWriter.begintag(name, attrs)
1568		xmlWriter.newline()
1569		for glyph, cid in sorted(value.items()):
1570			if cid is not None and cid != 0xFFFF:
1571				xmlWriter.simpletag(
1572					"CID", glyph=glyph, value=cid)
1573				xmlWriter.newline()
1574		xmlWriter.endtag(name)
1575		xmlWriter.newline()
1576
1577
1578class DeltaValue(BaseConverter):
1579
1580	def read(self, reader, font, tableDict):
1581		StartSize = tableDict["StartSize"]
1582		EndSize = tableDict["EndSize"]
1583		DeltaFormat = tableDict["DeltaFormat"]
1584		assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat"
1585		nItems = EndSize - StartSize + 1
1586		nBits = 1 << DeltaFormat
1587		minusOffset = 1 << nBits
1588		mask = (1 << nBits) - 1
1589		signMask = 1 << (nBits - 1)
1590
1591		DeltaValue = []
1592		tmp, shift = 0, 0
1593		for i in range(nItems):
1594			if shift == 0:
1595				tmp, shift = reader.readUShort(), 16
1596			shift = shift - nBits
1597			value = (tmp >> shift) & mask
1598			if value & signMask:
1599				value = value - minusOffset
1600			DeltaValue.append(value)
1601		return DeltaValue
1602
1603	def write(self, writer, font, tableDict, value, repeatIndex=None):
1604		StartSize = tableDict["StartSize"]
1605		EndSize = tableDict["EndSize"]
1606		DeltaFormat = tableDict["DeltaFormat"]
1607		DeltaValue = value
1608		assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat"
1609		nItems = EndSize - StartSize + 1
1610		nBits = 1 << DeltaFormat
1611		assert len(DeltaValue) == nItems
1612		mask = (1 << nBits) - 1
1613
1614		tmp, shift = 0, 16
1615		for value in DeltaValue:
1616			shift = shift - nBits
1617			tmp = tmp | ((value & mask) << shift)
1618			if shift == 0:
1619				writer.writeUShort(tmp)
1620				tmp, shift = 0, 16
1621		if shift != 16:
1622			writer.writeUShort(tmp)
1623
1624	def xmlWrite(self, xmlWriter, font, value, name, attrs):
1625		xmlWriter.simpletag(name, attrs + [("value", value)])
1626		xmlWriter.newline()
1627
1628	def xmlRead(self, attrs, content, font):
1629		return safeEval(attrs["value"])
1630
1631
1632class VarIdxMapValue(BaseConverter):
1633
1634	def read(self, reader, font, tableDict):
1635		fmt = tableDict['EntryFormat']
1636		nItems = tableDict['MappingCount']
1637
1638		innerBits = 1 + (fmt & 0x000F)
1639		innerMask = (1<<innerBits) - 1
1640		outerMask = 0xFFFFFFFF - innerMask
1641		outerShift = 16 - innerBits
1642
1643		entrySize = 1 + ((fmt & 0x0030) >> 4)
1644		readArray = {
1645			1: reader.readUInt8Array,
1646			2: reader.readUShortArray,
1647			3: reader.readUInt24Array,
1648			4: reader.readULongArray,
1649		}[entrySize]
1650
1651		return [(((raw & outerMask) << outerShift) | (raw & innerMask))
1652			for raw in readArray(nItems)]
1653
1654	def write(self, writer, font, tableDict, value, repeatIndex=None):
1655		fmt = tableDict['EntryFormat']
1656		mapping = value
1657		writer['MappingCount'].setValue(len(mapping))
1658
1659		innerBits = 1 + (fmt & 0x000F)
1660		innerMask = (1<<innerBits) - 1
1661		outerShift = 16 - innerBits
1662
1663		entrySize = 1 + ((fmt & 0x0030) >> 4)
1664		writeArray = {
1665			1: writer.writeUInt8Array,
1666			2: writer.writeUShortArray,
1667			3: writer.writeUInt24Array,
1668			4: writer.writeULongArray,
1669		}[entrySize]
1670
1671		writeArray([(((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask))
1672			    for idx in mapping])
1673
1674
1675class VarDataValue(BaseConverter):
1676
1677	def read(self, reader, font, tableDict):
1678		values = []
1679
1680		regionCount = tableDict["VarRegionCount"]
1681		wordCount = tableDict["NumShorts"]
1682
1683		# https://github.com/fonttools/fonttools/issues/2279
1684		longWords = bool(wordCount & 0x8000)
1685		wordCount = wordCount & 0x7FFF
1686
1687		if longWords:
1688			readBigArray, readSmallArray = reader.readLongArray, reader.readShortArray
1689		else:
1690			readBigArray, readSmallArray = reader.readShortArray, reader.readInt8Array
1691
1692		n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
1693		values.extend(readBigArray(n1))
1694		values.extend(readSmallArray(n2 - n1))
1695		if n2 > regionCount: # Padding
1696			del values[regionCount:]
1697
1698		return values
1699
1700	def write(self, writer, font, tableDict, values, repeatIndex=None):
1701		regionCount = tableDict["VarRegionCount"]
1702		wordCount = tableDict["NumShorts"]
1703
1704		# https://github.com/fonttools/fonttools/issues/2279
1705		longWords = bool(wordCount & 0x8000)
1706		wordCount = wordCount & 0x7FFF
1707
1708		(writeBigArray, writeSmallArray) = {
1709			False: (writer.writeShortArray, writer.writeInt8Array),
1710			True:  (writer.writeLongArray,  writer.writeShortArray),
1711		}[longWords]
1712
1713		n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
1714		writeBigArray(values[:n1])
1715		writeSmallArray(values[n1:regionCount])
1716		if n2 > regionCount: # Padding
1717			writer.writeSmallArray([0] * (n2 - regionCount))
1718
1719	def xmlWrite(self, xmlWriter, font, value, name, attrs):
1720		xmlWriter.simpletag(name, attrs + [("value", value)])
1721		xmlWriter.newline()
1722
1723	def xmlRead(self, attrs, content, font):
1724		return safeEval(attrs["value"])
1725
1726class LookupFlag(UShort):
1727	def xmlWrite(self, xmlWriter, font, value, name, attrs):
1728		xmlWriter.simpletag(name, attrs + [("value", value)])
1729		flags = []
1730		if value & 0x01: flags.append("rightToLeft")
1731		if value & 0x02: flags.append("ignoreBaseGlyphs")
1732		if value & 0x04: flags.append("ignoreLigatures")
1733		if value & 0x08: flags.append("ignoreMarks")
1734		if value & 0x10: flags.append("useMarkFilteringSet")
1735		if value & 0xff00: flags.append("markAttachmentType[%i]" % (value >> 8))
1736		if flags:
1737			xmlWriter.comment(" ".join(flags))
1738		xmlWriter.newline()
1739
1740
1741class _UInt8Enum(UInt8):
1742	enumClass = NotImplemented
1743
1744	def read(self, reader, font, tableDict):
1745		return self.enumClass(super().read(reader, font, tableDict))
1746	@classmethod
1747	def fromString(cls, value):
1748		return getattr(cls.enumClass, value.upper())
1749	@classmethod
1750	def toString(cls, value):
1751		return cls.enumClass(value).name.lower()
1752
1753
1754class ExtendMode(_UInt8Enum):
1755	enumClass = _ExtendMode
1756
1757
1758class CompositeMode(_UInt8Enum):
1759	enumClass = _CompositeMode
1760
1761
1762converterMapping = {
1763	# type		class
1764	"int8":		Int8,
1765	"int16":	Short,
1766	"uint8":	UInt8,
1767	"uint16":	UShort,
1768	"uint24":	UInt24,
1769	"uint32":	ULong,
1770	"char64":	Char64,
1771	"Flags32":	Flags32,
1772	"VarIndex":	VarIndex,
1773	"Version":	Version,
1774	"Tag":		Tag,
1775	"GlyphID":	GlyphID,
1776	"GlyphID32":	GlyphID32,
1777	"NameID":	NameID,
1778	"DeciPoints":	DeciPoints,
1779	"Fixed":	Fixed,
1780	"F2Dot14":	F2Dot14,
1781	"Angle":	Angle,
1782	"BiasedAngle":	BiasedAngle,
1783	"struct":	Struct,
1784	"Offset":	Table,
1785	"LOffset":	LTable,
1786	"Offset24":	Table24,
1787	"ValueRecord":	ValueRecord,
1788	"DeltaValue":	DeltaValue,
1789	"VarIdxMapValue":	VarIdxMapValue,
1790	"VarDataValue":	VarDataValue,
1791	"LookupFlag": LookupFlag,
1792	"ExtendMode": ExtendMode,
1793	"CompositeMode": CompositeMode,
1794	"STATFlags": STATFlags,
1795
1796	# AAT
1797	"CIDGlyphMap":	CIDGlyphMap,
1798	"GlyphCIDMap":	GlyphCIDMap,
1799	"MortChain":	StructWithLength,
1800	"MortSubtable": StructWithLength,
1801	"MorxChain":	StructWithLength,
1802	"MorxSubtable": MorxSubtableConverter,
1803
1804	# "Template" types
1805	"AATLookup":	lambda C: partial(AATLookup, tableClass=C),
1806	"AATLookupWithDataOffset":	lambda C: partial(AATLookupWithDataOffset, tableClass=C),
1807	"STXHeader":	lambda C: partial(STXHeader, tableClass=C),
1808	"OffsetTo":	lambda C: partial(Table, tableClass=C),
1809	"LOffsetTo":	lambda C: partial(LTable, tableClass=C),
1810	"LOffset24To":	lambda C: partial(Table24, tableClass=C),
1811}
1812