# Source code for fontTools.ttLib.tables.otConverters

    from fontTools.misc.fixedTools import (
        fixedToFloat as fi2fl,
        floatToFixed as fl2fi,
        floatToFixedToStr as fl2str,
        strToFixedToFloat as str2fl,
        ensureVersionIsLong as fi2ve,
        versionToFixed as ve2fi,
    )
    from fontTools.ttLib.tables.TupleVariation import TupleVariation
    from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound
    from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval
    from fontTools.misc.lazyTools import LazyList
    from fontTools.ttLib import OPTIMIZE_FONT_SPEED, getSearchRange
    from .otBase import (
        CountReference,
        FormatSwitchingBaseTable,
        OTTableReader,
        OTTableWriter,
        ValueRecordFactory,
    )
    from .otTables import (
        lookupTypes,
        VarCompositeGlyph,
        AATStateTable,
        AATState,
        AATAction,
        ContextualMorphAction,
        LigatureMorphAction,
        InsertionMorphAction,
        MorxSubtable,
        ExtendMode as _ExtendMode,
        CompositeMode as _CompositeMode,
        NO_VARIATION_INDEX,
    )
    from itertools import zip_longest, accumulate
    from functools import partial
    from types import SimpleNamespace
    import re
    import struct
    from typing import Optional
    import logging

    log = logging.getLogger(__name__)
    istuple = lambda t: isinstance(t, tuple)

    def buildConverters(tableSpec, tableNamespace):
        """Given a table spec from otData.py, build a converter object for each
        field of the table. This is called for each table in otData.py, and
        the results are assigned to the corresponding class in otTables.py."""
        converters = []
        convertersByName = {}
        for tp, name, repeat, aux, descr in tableSpec:
            tableName = name
            if name.startswith("ValueFormat"):
                assert tp == "uint16"
                converterClass = ValueFormat
            elif name.endswith("Count") or name in ("StructLength", "MorphType"):
                converterClass = {
                    "uint8": ComputedUInt8,
                    "uint16": ComputedUShort,
                    "uint32": ComputedULong,
                }[tp]
            elif name == "SubTable":
                converterClass = SubTable
            elif name == "ExtSubTable":
                converterClass = ExtSubTable
            elif name == "SubStruct":
                converterClass = SubStruct
            elif name == "FeatureParams":
                converterClass = FeatureParams
            elif name in ("CIDGlyphMapping", "GlyphCIDMapping"):
                converterClass = StructWithLength
            else:
                if not tp in converterMapping and "(" not in tp:
                    tableName = tp
                    converterClass = Struct
                else:
                    converterClass = eval(tp, tableNamespace, converterMapping)

            conv = converterClass(name, repeat, aux, description=descr)

            if conv.tableClass:
                # A "template" such as OffsetTo(AType) knows the table class already
                tableClass = conv.tableClass
            elif tp in ("MortChain", "MortSubtable", "MorxChain"):
                tableClass = tableNamespace.get(tp)
            else:
                tableClass = tableNamespace.get(tableName)

            if not conv.tableClass:
                conv.tableClass = tableClass

            if name in ["SubTable", "ExtSubTable", "SubStruct"]:
                conv.lookupTypes = tableNamespace["lookupTypes"]
                # also create reverse mapping
                for t in conv.lookupTypes.values():
                    for cls in t.values():
                        convertersByName[cls.__name__] = Table(name, repeat, aux, cls)
            if name == "FeatureParams":
                conv.featureParamTypes = tableNamespace["featureParamTypes"]
                conv.defaultFeatureParams = tableNamespace["FeatureParams"]
                for cls in conv.featureParamTypes.values():
                    convertersByName[cls.__name__] = Table(name, repeat, aux, cls)
            converters.append(conv)
            assert name not in convertersByName, name
            convertersByName[name] = conv
        return converters, convertersByName

    class BaseConverter(object):
        """Base class for converter objects. Apart from the constructor, this
        is an abstract class."""

        def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
            self.name = name
            self.repeat = repeat
            self.aux = aux
            if self.aux and not self.repeat:
                self.aux = compile(self.aux, "<string>", "eval")
            self.tableClass = tableClass
            self.isCount = name.endswith("Count") or name in [
                "DesignAxisRecordSize",
                "ValueRecordSize",
            ]
            self.isLookupType = name.endswith("LookupType") or name == "MorphType"
            self.isPropagated = name in [
                "ClassCount",
                "Class2Count",
                "FeatureTag",
                "SettingsCount",
                "VarRegionCount",
                "MappingCount",
                "RegionAxisCount",
                "DesignAxisCount",
                "DesignAxisRecordSize",
                "AxisValueCount",
                "ValueRecordSize",
                "AxisCount",
                "BaseGlyphRecordCount",
                "LayerRecordCount",
                "AxisIndicesList",
            ]
            self.description = description

        def readArray(self, reader, font, tableDict, count):
            """Read an array of values from the reader."""
            lazy = font.lazy and count > 8
            if lazy:
                recordSize = self.getRecordSize(reader)
                if recordSize is NotImplemented:
                    lazy = False
            if not lazy:
                l = []
                for i in range(count):
                    l.append(self.read(reader, font, tableDict))
                return l
            else:

                def get_read_item():
                    reader_copy = reader.copy()
                    pos = reader.pos

                    def read_item(i):
                        reader_copy.seek(pos + i * recordSize)
                        return self.read(reader_copy, font, {})

                    return read_item

                read_item = get_read_item()
                l = LazyList(read_item for i in range(count))
                reader.advance(count * recordSize)

                return l

        def getRecordSize(self, reader):
            if hasattr(self, "staticSize"):
                return self.staticSize
            return NotImplemented

        def read(self, reader, font, tableDict):
            """Read a value from the reader."""
            raise NotImplementedError(self)

        def writeArray(self, writer, font, tableDict, values):
            try:
                for i, value in enumerate(values):
                    self.write(writer, font, tableDict, value, i)
            except Exception as e:
                e.args = e.args + (i,)
                raise

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            """Write a value to the writer."""
            raise NotImplementedError(self)

        def xmlRead(self, attrs, content, font):
            """Read a value from XML."""
            raise NotImplementedError(self)

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            """Write a value to XML."""
            raise NotImplementedError(self)

        varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)")

        def getVarIndexOffset(self) -> Optional[int]:
            """If description has `VarIndexBase + {offset}`, return the offset else None."""
            m = self.varIndexBasePlusOffsetRE.search(self.description)
            if not m:
                return None
            return int(m.group(1))

    class SimpleValue(BaseConverter):

        @staticmethod
        def toString(value):
            return value

        @staticmethod
        def fromString(value):
            return value

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.simpletag(name, attrs + [("value", self.toString(value))])
            xmlWriter.newline()

        def xmlRead(self, attrs, content, font):
            return self.fromString(attrs["value"])

    class OptionalValue(SimpleValue):
        DEFAULT = None

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            if value != self.DEFAULT:
                attrs.append(("value", self.toString(value)))
            xmlWriter.simpletag(name, attrs)
            xmlWriter.newline()

        def xmlRead(self, attrs, content, font):
            if "value" in attrs:
                return self.fromString(attrs["value"])
            return self.DEFAULT

    class IntValue(SimpleValue):

        @staticmethod
        def fromString(value):
            return int(value, 0)

    class Long(IntValue):
        staticSize = 4

        def read(self, reader, font, tableDict):
            return reader.readLong()

        def readArray(self, reader, font, tableDict, count):
            return reader.readLongArray(count)

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeLong(value)

        def writeArray(self, writer, font, tableDict, values):
            writer.writeLongArray(values)

    class ULong(IntValue):
        staticSize = 4

        def read(self, reader, font, tableDict):
            return reader.readULong()

        def readArray(self, reader, font, tableDict, count):
            return reader.readULongArray(count)

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeULong(value)

        def writeArray(self, writer, font, tableDict, values):
            writer.writeULongArray(values)

    class Flags32(ULong):

        @staticmethod
        def toString(value):
            return "0x%08X" % value

    class VarIndex(OptionalValue, ULong):
        DEFAULT = NO_VARIATION_INDEX

    class Short(IntValue):
        staticSize = 2

        def read(self, reader, font, tableDict):
            return reader.readShort()

        def readArray(self, reader, font, tableDict, count):
            return reader.readShortArray(count)

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeShort(value)

        def writeArray(self, writer, font, tableDict, values):
            writer.writeShortArray(values)

    class UShort(IntValue):
        staticSize = 2

        def read(self, reader, font, tableDict):
            return reader.readUShort()

        def readArray(self, reader, font, tableDict, count):
            return reader.readUShortArray(count)

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeUShort(value)

        def writeArray(self, writer, font, tableDict, values):
            writer.writeUShortArray(values)

    class Int8(IntValue):
        staticSize = 1

        def read(self, reader, font, tableDict):
            return reader.readInt8()

        def readArray(self, reader, font, tableDict, count):
            return reader.readInt8Array(count)

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeInt8(value)

        def writeArray(self, writer, font, tableDict, values):
            writer.writeInt8Array(values)

    class UInt8(IntValue):
        staticSize = 1

        def read(self, reader, font, tableDict):
            return reader.readUInt8()

        def readArray(self, reader, font, tableDict, count):
            return reader.readUInt8Array(count)

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeUInt8(value)

        def writeArray(self, writer, font, tableDict, values):
            writer.writeUInt8Array(values)

    class UInt24(IntValue):
        staticSize = 3

        def read(self, reader, font, tableDict):
            return reader.readUInt24()

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeUInt24(value)

    class ComputedInt(IntValue):

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            if value is not None:
                xmlWriter.comment("%s=%s" % (name, value))
                xmlWriter.newline()

    class ComputedUInt8(ComputedInt, UInt8):
        pass

    class ComputedUShort(ComputedInt, UShort):
        pass

    class ComputedULong(ComputedInt, ULong):
        pass

    class Tag(SimpleValue):
        staticSize = 4

        def read(self, reader, font, tableDict):
            return reader.readTag()

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeTag(value)

    class GlyphID(SimpleValue):
        staticSize = 2
        typecode = "H"

        def readArray(self, reader, font, tableDict, count):
            return font.getGlyphNameMany(
                reader.readArray(self.typecode, self.staticSize, count)
            )

        def read(self, reader, font, tableDict):
            return font.getGlyphName(reader.readValue(self.typecode, self.staticSize))

        def writeArray(self, writer, font, tableDict, values):
            writer.writeArray(self.typecode, font.getGlyphIDMany(values))

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeValue(self.typecode, font.getGlyphID(value))

    class GlyphID32(GlyphID):
        staticSize = 4
        typecode = "L"

    class NameID(UShort):

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.simpletag(name, attrs + [("value", value)])
            if font and value:
                nameTable = font.get("name")
                if nameTable:
                    name = nameTable.getDebugName(value)
                    xmlWriter.write("  ")
                    if name:
                        xmlWriter.comment(name)
                    else:
                        xmlWriter.comment("missing from name table")
                        log.warning("name id %d missing from name table" % value)
            xmlWriter.newline()

    class STATFlags(UShort):

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.simpletag(name, attrs + [("value", value)])
            flags = []
            if value & 0x01:
                flags.append("OlderSiblingFontAttribute")
            if value & 0x02:
                flags.append("ElidableAxisValueName")
            if flags:
                xmlWriter.write("  ")
                xmlWriter.comment(" ".join(flags))
            xmlWriter.newline()

    class FloatValue(SimpleValue):

        @staticmethod
        def fromString(value):
            return float(value)

    class DeciPoints(FloatValue):
        staticSize = 2

        def read(self, reader, font, tableDict):
            return reader.readUShort() / 10

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.writeUShort(round(value * 10))

    class BaseFixedValue(FloatValue):
        staticSize = NotImplemented
        precisionBits = NotImplemented
        readerMethod = NotImplemented
        writerMethod = NotImplemented

        def read(self, reader, font, tableDict):
            return self.fromInt(getattr(reader, self.readerMethod)())

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            getattr(writer, self.writerMethod)(self.toInt(value))

        @classmethod
        def fromInt(cls, value):
            return fi2fl(value, cls.precisionBits)

        @classmethod
        def toInt(cls, value):
            return fl2fi(value, cls.precisionBits)

        @classmethod
        def fromString(cls, value):
            return str2fl(value, cls.precisionBits)

        @classmethod
        def toString(cls, value):
            return fl2str(value, cls.precisionBits)

    class Fixed(BaseFixedValue):
        staticSize = 4
        precisionBits = 16
        readerMethod = "readLong"
        writerMethod = "writeLong"

    class F2Dot14(BaseFixedValue):
        staticSize = 2
        precisionBits = 14
        readerMethod = "readShort"
        writerMethod = "writeShort"

    class Angle(F2Dot14):
        # angles are specified in degrees, and encoded as F2Dot14 fractions of half
        # circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc.
        bias = 0.0
        factor = 1.0 / (1 << 14) * 180  # 0.010986328125

        @classmethod
        def fromInt(cls, value):
            return (super().fromInt(value) + cls.bias) * 180

        @classmethod
        def toInt(cls, value):
            return super().toInt((value / 180) - cls.bias)

        @classmethod
        def fromString(cls, value):
            # quantize to nearest multiples of minimum fixed-precision angle
            return otRound(float(value) / cls.factor) * cls.factor

        @classmethod
        def toString(cls, value):
            return nearestMultipleShortestRepr(value, cls.factor)

    class BiasedAngle(Angle):
        # A bias of 1.0 is used in the representation of start and end angles
        # of COLRv1 PaintSweepGradients to allow for encoding +360deg
        bias = 1.0

    class Version(SimpleValue):
        staticSize = 4

        def read(self, reader, font, tableDict):
            value = reader.readLong()
            return value

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            value = fi2ve(value)
            writer.writeLong(value)

        @staticmethod
        def fromString(value):
            return ve2fi(value)

        @staticmethod
        def toString(value):
            return "0x%08x" % value

        @staticmethod
        def fromFloat(v):
            return fl2fi(v, 16)

    class Char64(SimpleValue):
        """An ASCII string with up to 64 characters.

        Unused character positions are filled with 0x00 bytes.
        Used in Apple AAT fonts in the `gcid` table.
        """

        staticSize = 64

        def read(self, reader, font, tableDict):
            data = reader.readData(self.staticSize)
            zeroPos = data.find(b"\0")
            if zeroPos >= 0:
                data = data[:zeroPos]
            s = tostr(data, encoding="ascii", errors="replace")
            if s != tostr(data, encoding="ascii", errors="ignore"):
                log.warning('replaced non-ASCII characters in "%s"' % s)
            return s

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            data = tobytes(value, encoding="ascii", errors="replace")
            if data != tobytes(value, encoding="ascii", errors="ignore"):
                log.warning('replacing non-ASCII characters in "%s"' % value)
            if len(data) > self.staticSize:
                log.warning(
                    'truncating overlong "%s" to %d bytes' % (value, self.staticSize)
                )
            data = (data + b"\0" * self.staticSize)[: self.staticSize]
            writer.writeData(data)

    class Struct(BaseConverter):

        def getRecordSize(self, reader):
            return self.tableClass and self.tableClass.getRecordSize(reader)

        def read(self, reader, font, tableDict):
            table = self.tableClass()
            table.decompile(reader, font)
            return table

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            value.compile(writer, font)

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            if value is None:
                if attrs:
                    # If there are attributes (probably index), then
                    # don't drop this even if it's NULL.  It will mess
                    # up the array indices of the containing element.
                    xmlWriter.simpletag(name, attrs + [("empty", 1)])
                    xmlWriter.newline()
                else:
                    pass  # NULL table, ignore
            else:
                value.toXML(xmlWriter, font, attrs, name=name)

        def xmlRead(self, attrs, content, font):
            if "empty" in attrs and safeEval(attrs["empty"]):
                return None
            table = self.tableClass()
            Format = attrs.get("Format")
            if Format is not None:
                table.Format = int(Format)

            noPostRead = not hasattr(table, "postRead")
            if noPostRead:
                # TODO Cache table.hasPropagated.
                cleanPropagation = False
                for conv in table.getConverters():
                    if conv.isPropagated:
                        cleanPropagation = True
                        if not hasattr(font, "_propagator"):
                            font._propagator = {}
                        propagator = font._propagator
                        assert conv.name not in propagator, (conv.name, propagator)
                        setattr(table, conv.name, None)
                        propagator[conv.name] = CountReference(table.__dict__, conv.name)

            for element in content:
                if isinstance(element, tuple):
                    name, attrs, content = element
                    table.fromXML(name, attrs, content, font)
                else:
                    pass

            table.populateDefaults(propagator=getattr(font, "_propagator", None))

            if noPostRead:
                if cleanPropagation:
                    for conv in table.getConverters():
                        if conv.isPropagated:
                            propagator = font._propagator
                            del propagator[conv.name]
                            if not propagator:
                                del font._propagator

            return table

        def __repr__(self):
            return "Struct of " + repr(self.tableClass)

    class StructWithLength(Struct):

        def read(self, reader, font, tableDict):
            pos = reader.pos
            table = self.tableClass()
            table.decompile(reader, font)
            reader.seek(pos + table.StructLength)
            return table

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            for convIndex, conv in enumerate(value.getConverters()):
                if conv.name == "StructLength":
                    break
            lengthIndex = len(writer.items) + convIndex
            if isinstance(value, FormatSwitchingBaseTable):
                lengthIndex += 1  # implicit Format field
            deadbeef = {1: 0xDE, 2: 0xDEAD, 4: 0xDEADBEEF}[conv.staticSize]

            before = writer.getDataLength()
            value.StructLength = deadbeef
            value.compile(writer, font)
            length = writer.getDataLength() - before
            lengthWriter = writer.getSubWriter()
            conv.write(lengthWriter, font, tableDict, length)
            assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef"[: conv.staticSize]
            writer.items[lengthIndex] = lengthWriter.getAllData()

    class Table(Struct):
        staticSize = 2

        def readOffset(self, reader):
            return reader.readUShort()

        def writeNullOffset(self, writer):
            writer.writeUShort(0)

        def read(self, reader, font, tableDict):
            offset = self.readOffset(reader)
            if offset == 0:
                return None
            table = self.tableClass()
            reader = reader.getSubReader(offset)
            if font.lazy:
                table.reader = reader
                table.font = font
            else:
                table.decompile(reader, font)
            return table

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            if value is None:
                self.writeNullOffset(writer)
            else:
                subWriter = writer.getSubWriter()
                subWriter.name = self.name
                if repeatIndex is not None:
                    subWriter.repeatIndex = repeatIndex
                writer.writeSubTable(subWriter, offsetSize=self.staticSize)
                value.compile(subWriter, font)

    class LTable(Table):
        staticSize = 4

        def readOffset(self, reader):
            return reader.readULong()

        def writeNullOffset(self, writer):
            writer.writeULong(0)

    # Table pointed to by a 24-bit, 3-byte long offset

    class Table24(Table):
        staticSize = 3

        def readOffset(self, reader):
            return reader.readUInt24()

        def writeNullOffset(self, writer):
            writer.writeUInt24(0)

    # TODO Clean / merge the SubTable and SubStruct

    class SubStruct(Struct):

        def getConverter(self, tableType, lookupType):
            tableClass = self.lookupTypes[tableType][lookupType]
            return self.__class__(self.name, self.repeat, self.aux, tableClass)

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            super(SubStruct, self).xmlWrite(xmlWriter, font, value, None, attrs)

    class SubTable(Table):

        def getConverter(self, tableType, lookupType):
            tableClass = self.lookupTypes[tableType][lookupType]
            return self.__class__(self.name, self.repeat, self.aux, tableClass)

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            super(SubTable, self).xmlWrite(xmlWriter, font, value, None, attrs)

    class ExtSubTable(LTable, SubTable):

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer.Extension = True  # actually, mere presence of the field flags it as an Ext Subtable writer.
            Table.write(self, writer, font, tableDict, value, repeatIndex)

    class FeatureParams(Table):

        def getConverter(self, featureTag):
            tableClass = self.featureParamTypes.get(featureTag, self.defaultFeatureParams)
            return self.__class__(self.name, self.repeat, self.aux, tableClass)

    class ValueFormat(IntValue):
        staticSize = 2

        def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
            BaseConverter.__init__(
                self, name, repeat, aux, tableClass, description=description
            )
            self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1")

        def read(self, reader, font, tableDict):
            format = reader.readUShort()
            reader[self.which] = ValueRecordFactory(format)
            return format

        def write(self, writer, font, tableDict, format, repeatIndex=None):
            writer.writeUShort(format)
            writer[self.which] = ValueRecordFactory(format)

    class ValueRecord(ValueFormat):

        def getRecordSize(self, reader):
            return 2 * len(reader[self.which])

        def read(self, reader, font, tableDict):
            return reader[self.which].readValueRecord(reader, font)

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            writer[self.which].writeValueRecord(writer, font, value)

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            if value is None:
                pass  # NULL table, ignore
            else:
                value.toXML(xmlWriter, font, self.name, attrs)

        def xmlRead(self, attrs, content, font):
            from .otBase import ValueRecord

            value = ValueRecord()
            value.fromXML(None, attrs, content, font)
            return value

    class AATLookup(BaseConverter):
        BIN_SEARCH_HEADER_SIZE = 10

        def __init__(self, name, repeat, aux, tableClass, *, description=""):
            BaseConverter.__init__(
                self, name, repeat, aux, tableClass, description=description
            )
            if issubclass(self.tableClass, SimpleValue):
                self.converter = self.tableClass(name="Value", repeat=None, aux=None)
            else:
                self.converter = Table(
                    name="Value", repeat=None, aux=None, tableClass=self.tableClass
                )

        def read(self, reader, font, tableDict):
            format = reader.readUShort()
            if format == 0:
                return self.readFormat0(reader, font)
            elif format == 2:
                return self.readFormat2(reader, font)
            elif format == 4:
                return self.readFormat4(reader, font)
            elif format == 6:
                return self.readFormat6(reader, font)
            elif format == 8:
                return self.readFormat8(reader, font)
            else:
                assert False, "unsupported lookup format: %d" % format

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            values = list(
                sorted([(font.getGlyphID(glyph), val) for glyph, val in value.items()])
            )
            # TODO: Also implement format 4.
            formats = list(
                sorted(
                    filter(
                        None,
                        [
                            self.buildFormat0(writer, font, values),
                            self.buildFormat2(writer, font, values),
                            self.buildFormat6(writer, font, values),
                            self.buildFormat8(writer, font, values),
                        ],
                    )
                )
            )
            # We use the format ID as secondary sort key to make the output
            # deterministic when multiple formats have same encoded size.
            dataSize, lookupFormat, writeMethod = formats[0]
            pos = writer.getDataLength()
            writeMethod()
            actualSize = writer.getDataLength() - pos
            assert (
                actualSize == dataSize
            ), "AATLookup format %d claimed to write %d bytes, but wrote %d" % (
                lookupFormat,
                dataSize,
                actualSize,
            )

        @staticmethod
        def writeBinSearchHeader(writer, numUnits, unitSize):
            writer.writeUShort(unitSize)
            writer.writeUShort(numUnits)
            searchRange, entrySelector, rangeShift = getSearchRange(
                n=numUnits, itemSize=unitSize
            )
            writer.writeUShort(searchRange)
            writer.writeUShort(entrySelector)
            writer.writeUShort(rangeShift)

        def buildFormat0(self, writer, font, values):
            numGlyphs = len(font.getGlyphOrder())
            if len(values) != numGlyphs:
                return None
            valueSize = self.converter.staticSize
            return (
                2 + numGlyphs * valueSize,
                0,
                lambda: self.writeFormat0(writer, font, values),
            )

        def writeFormat0(self, writer, font, values):
            writer.writeUShort(0)
            for glyphID_, value in values:
                self.converter.write(
                    writer, font, tableDict=None, value=value, repeatIndex=None
                )

        def buildFormat2(self, writer, font, values):
            segStart, segValue = values[0]
            segEnd = segStart
            segments = []
            for glyphID, curValue in values[1:]:
                if glyphID != segEnd + 1 or curValue != segValue:
                    segments.append((segStart, segEnd, segValue))
                    segStart = segEnd = glyphID
                    segValue = curValue
                else:
                    segEnd = glyphID
            segments.append((segStart, segEnd, segValue))
            valueSize = self.converter.staticSize
            numUnits, unitSize = len(segments) + 1, valueSize + 4
            return (
                2 + self.BIN_SEARCH_HEADER_SIZE + numUnits * unitSize,
                2,
                lambda: self.writeFormat2(writer, font, segments),
            )

        def writeFormat2(self, writer, font, segments):
            writer.writeUShort(2)
            valueSize = self.converter.staticSize
            numUnits, unitSize = len(segments), valueSize + 4
            self.writeBinSearchHeader(writer, numUnits, unitSize)
            for firstGlyph, lastGlyph, value in segments:
                writer.writeUShort(lastGlyph)
                writer.writeUShort(firstGlyph)
                self.converter.write(
                    writer, font, tableDict=None, value=value, repeatIndex=None
                )
            writer.writeUShort(0xFFFF)
            writer.writeUShort(0xFFFF)
            writer.writeData(b"\x00" * valueSize)

        def buildFormat6(self, writer, font, values):
            valueSize = self.converter.staticSize
            numUnits, unitSize = len(values), valueSize + 2
            return (
                2 + self.BIN_SEARCH_HEADER_SIZE + (numUnits + 1) * unitSize,
                6,
                lambda: self.writeFormat6(writer, font, values),
            )

        def writeFormat6(self, writer, font, values):
            writer.writeUShort(6)
            valueSize = self.converter.staticSize
            numUnits, unitSize = len(values), valueSize + 2
            self.writeBinSearchHeader(writer, numUnits, unitSize)
            for glyphID, value in values:
                writer.writeUShort(glyphID)
                self.converter.write(
                    writer, font, tableDict=None, value=value, repeatIndex=None
                )
            writer.writeUShort(0xFFFF)
            writer.writeData(b"\x00" * valueSize)

        def buildFormat8(self, writer, font, values):
            minGlyphID, maxGlyphID = values[0][0], values[-1][0]
            if len(values) != maxGlyphID - minGlyphID + 1:
                return None
            valueSize = self.converter.staticSize
            return (
                6 + len(values) * valueSize,
                8,
                lambda: self.writeFormat8(writer, font, values),
            )

        def writeFormat8(self, writer, font, values):
            firstGlyphID = values[0][0]
            writer.writeUShort(8)
            writer.writeUShort(firstGlyphID)
            writer.writeUShort(len(values))
            for _, value in values:
                self.converter.write(
                    writer, font, tableDict=None, value=value, repeatIndex=None
                )

        def readFormat0(self, reader, font):
            numGlyphs = len(font.getGlyphOrder())
            data = self.converter.readArray(reader, font, tableDict=None, count=numGlyphs)
            return {font.getGlyphName(k): value for k, value in enumerate(data)}

        def readFormat2(self, reader, font):
            mapping = {}
            pos = reader.pos - 2  # start of table is at UShort for format
            unitSize, numUnits = reader.readUShort(), reader.readUShort()
            assert unitSize >= 4 + self.converter.staticSize, unitSize
            for i in range(numUnits):
                reader.seek(pos + i * unitSize + 12)
                last = reader.readUShort()
                first = reader.readUShort()
                value = self.converter.read(reader, font, tableDict=None)
                if last != 0xFFFF:
                    for k in range(first, last + 1):
                        mapping[font.getGlyphName(k)] = value
            return mapping

        def readFormat4(self, reader, font):
            mapping = {}
            pos = reader.pos - 2  # start of table is at UShort for format
            unitSize = reader.readUShort()
            assert unitSize >= 6, unitSize
            for i in range(reader.readUShort()):
                reader.seek(pos + i * unitSize + 12)
                last = reader.readUShort()
                first = reader.readUShort()
                offset = reader.readUShort()
                if last != 0xFFFF:
                    dataReader = reader.getSubReader(0)  # relative to current position
                    dataReader.seek(pos + offset)  # relative to start of table
                    data = self.converter.readArray(
                        dataReader, font, tableDict=None, count=last - first + 1
                    )
                    for k, v in enumerate(data):
                        mapping[font.getGlyphName(first + k)] = v
            return mapping

        def readFormat6(self, reader, font):
            mapping = {}
            pos = reader.pos - 2  # start of table is at UShort for format
            unitSize = reader.readUShort()
            assert unitSize >= 2 + self.converter.staticSize, unitSize
            for i in range(reader.readUShort()):
                reader.seek(pos + i * unitSize + 12)
                glyphID = reader.readUShort()
                value = self.converter.read(reader, font, tableDict=None)
                if glyphID != 0xFFFF:
                    mapping[font.getGlyphName(glyphID)] = value
            return mapping

        def readFormat8(self, reader, font):
            first = reader.readUShort()
            count = reader.readUShort()
            data = self.converter.readArray(reader, font, tableDict=None, count=count)
            return {font.getGlyphName(first + k): value for (k, value) in enumerate(data)}

        def xmlRead(self, attrs, content, font):
            value = {}
            for element in content:
                if isinstance(element, tuple):
                    name, a, eltContent = element
                    if name == "Lookup":
                        value[a["glyph"]] = self.converter.xmlRead(a, eltContent, font)
            return value

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.begintag(name, attrs)
            xmlWriter.newline()
            for glyph, value in sorted(value.items()):
                self.converter.xmlWrite(
                    xmlWriter, font, value=value, name="Lookup", attrs=[("glyph", glyph)]
                )
            xmlWriter.endtag(name)
            xmlWriter.newline()

    # The AAT 'ankr' table has an unusual structure: An offset to an AATLookup
    # followed by an offset to a glyph data table. Other than usual, the
    # offsets in the AATLookup are not relative to the beginning of
    # the beginning of the 'ankr' table, but relative to the glyph data table.
    # So, to find the anchor data for a glyph, one needs to add the offset
    # to the data table to the offset found in the AATLookup, and then use
    # the sum of these two offsets to find the actual data.

    class AATLookupWithDataOffset(BaseConverter):

        def read(self, reader, font, tableDict):
            lookupOffset = reader.readULong()
            dataOffset = reader.readULong()
            lookupReader = reader.getSubReader(lookupOffset)
            lookup = AATLookup("DataOffsets", None, None, UShort)
            offsets = lookup.read(lookupReader, font, tableDict)
            result = {}
            for glyph, offset in offsets.items():
                dataReader = reader.getSubReader(offset + dataOffset)
                item = self.tableClass()
                item.decompile(dataReader, font)
                result[glyph] = item
            return result

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            # We do not work with OTTableWriter sub-writers because
            # the offsets in our AATLookup are relative to our data
            # table, for which we need to provide an offset value itself.
            # It might have been possible to somehow make a kludge for
            # performing this indirect offset computation directly inside
            # OTTableWriter. But this would have made the internal logic
            # of OTTableWriter even more complex than it already is,
            # so we decided to roll our own offset computation for the
            # contents of the AATLookup and associated data table.
            offsetByGlyph, offsetByData, dataLen = {}, {}, 0
            compiledData = []
            for glyph in sorted(value, key=font.getGlyphID):
                subWriter = OTTableWriter()
                value[glyph].compile(subWriter, font)
                data = subWriter.getAllData()
                offset = offsetByData.get(data, None)
                if offset == None:
                    offset = dataLen
                    dataLen = dataLen + len(data)
                    offsetByData[data] = offset
                    compiledData.append(data)
                offsetByGlyph[glyph] = offset
            # For calculating the offsets to our AATLookup and data table,
            # we can use the regular OTTableWriter infrastructure.
            lookupWriter = writer.getSubWriter()
            lookup = AATLookup("DataOffsets", None, None, UShort)
            lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None)

            dataWriter = writer.getSubWriter()
            writer.writeSubTable(lookupWriter, offsetSize=4)
            writer.writeSubTable(dataWriter, offsetSize=4)
            for d in compiledData:
                dataWriter.writeData(d)

        def xmlRead(self, attrs, content, font):
            lookup = AATLookup("DataOffsets", None, None, self.tableClass)
            return lookup.xmlRead(attrs, content, font)

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            lookup = AATLookup("DataOffsets", None, None, self.tableClass)
            lookup.xmlWrite(xmlWriter, font, value, name, attrs)

    class MorxSubtableConverter(BaseConverter):
        _PROCESSING_ORDERS = {
            # bits 30 and 28 of morx.CoverageFlags; see morx spec
            (False, False): "LayoutOrder",
            (True, False): "ReversedLayoutOrder",
            (False, True): "LogicalOrder",
            (True, True): "ReversedLogicalOrder",
        }

        _PROCESSING_ORDERS_REVERSED = {val: key for key, val in _PROCESSING_ORDERS.items()}

        def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
            BaseConverter.__init__(
                self, name, repeat, aux, tableClass, description=description
            )

        def _setTextDirectionFromCoverageFlags(self, flags, subtable):
            if (flags & 0x20) != 0:
                subtable.TextDirection = "Any"
            elif (flags & 0x80) != 0:
                subtable.TextDirection = "Vertical"
            else:
                subtable.TextDirection = "Horizontal"

        def read(self, reader, font, tableDict):
            pos = reader.pos
            m = MorxSubtable()
            m.StructLength = reader.readULong()
            flags = reader.readUInt8()
            orderKey = ((flags & 0x40) != 0, (flags & 0x10) != 0)
            m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey]
            self._setTextDirectionFromCoverageFlags(flags, m)
            m.Reserved = reader.readUShort()
            m.Reserved |= (flags & 0xF) << 16
            m.MorphType = reader.readUInt8()
            m.SubFeatureFlags = reader.readULong()
            tableClass = lookupTypes["morx"].get(m.MorphType)
            if tableClass is None:
                assert False, "unsupported 'morx' lookup type %s" % m.MorphType
            # To decode AAT ligatures, we need to know the subtable size.
            # The easiest way to pass this along is to create a new reader
            # that works on just the subtable as its data.
            headerLength = reader.pos - pos
            data = reader.data[reader.pos : reader.pos + m.StructLength - headerLength]
            assert len(data) == m.StructLength - headerLength
            subReader = OTTableReader(data=data, tableTag=reader.tableTag)
            m.SubStruct = tableClass()
            m.SubStruct.decompile(subReader, font)
            reader.seek(pos + m.StructLength)
            return m

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.begintag(name, attrs)
            xmlWriter.newline()
            xmlWriter.comment("StructLength=%d" % value.StructLength)
            xmlWriter.newline()
            xmlWriter.simpletag("TextDirection", value=value.TextDirection)
            xmlWriter.newline()
            xmlWriter.simpletag("ProcessingOrder", value=value.ProcessingOrder)
            xmlWriter.newline()
            if value.Reserved != 0:
                xmlWriter.simpletag("Reserved", value="0x%04x" % value.Reserved)
                xmlWriter.newline()
            xmlWriter.comment("MorphType=%d" % value.MorphType)
            xmlWriter.newline()
            xmlWriter.simpletag("SubFeatureFlags", value="0x%08x" % value.SubFeatureFlags)
            xmlWriter.newline()
            value.SubStruct.toXML(xmlWriter, font)
            xmlWriter.endtag(name)
            xmlWriter.newline()

        def xmlRead(self, attrs, content, font):
            m = MorxSubtable()
            covFlags = 0
            m.Reserved = 0
            for eltName, eltAttrs, eltContent in filter(istuple, content):
                if eltName == "CoverageFlags":
                    # Only in XML from old versions of fonttools.
                    covFlags = safeEval(eltAttrs["value"])
                    orderKey = ((covFlags & 0x40) != 0, (covFlags & 0x10) != 0)
                    m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey]
                    self._setTextDirectionFromCoverageFlags(covFlags, m)
                elif eltName == "ProcessingOrder":
                    m.ProcessingOrder = eltAttrs["value"]
                    assert m.ProcessingOrder in self._PROCESSING_ORDERS_REVERSED, (
                        "unknown ProcessingOrder: %s" % m.ProcessingOrder
                    )
                elif eltName == "TextDirection":
                    m.TextDirection = eltAttrs["value"]
                    assert m.TextDirection in {"Horizontal", "Vertical", "Any"}, (
                        "unknown TextDirection %s" % m.TextDirection
                    )
                elif eltName == "Reserved":
                    m.Reserved = safeEval(eltAttrs["value"])
                elif eltName == "SubFeatureFlags":
                    m.SubFeatureFlags = safeEval(eltAttrs["value"])
                elif eltName.endswith("Morph"):
                    m.fromXML(eltName, eltAttrs, eltContent, font)
                else:
                    assert False, eltName
            m.Reserved = (covFlags & 0xF) << 16 | m.Reserved
            return m

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            covFlags = (value.Reserved & 0x000F0000) >> 16
            reverseOrder, logicalOrder = self._PROCESSING_ORDERS_REVERSED[
                value.ProcessingOrder
            ]
            covFlags |= 0x80 if value.TextDirection == "Vertical" else 0
            covFlags |= 0x40 if reverseOrder else 0
            covFlags |= 0x20 if value.TextDirection == "Any" else 0
            covFlags |= 0x10 if logicalOrder else 0
            value.CoverageFlags = covFlags
            lengthIndex = len(writer.items)
            before = writer.getDataLength()
            value.StructLength = 0xDEADBEEF
            # The high nibble of value.Reserved is actuallly encoded
            # into coverageFlags, so we need to clear it here.
            origReserved = value.Reserved  # including high nibble
            value.Reserved = value.Reserved & 0xFFFF  # without high nibble
            value.compile(writer, font)
            value.Reserved = origReserved  # restore original value
            assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef"
            length = writer.getDataLength() - before
            writer.items[lengthIndex] = struct.pack(">L", length)

    # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader
    # TODO: Untangle the implementation of the various lookup-specific formats.

    class STXHeader(BaseConverter):
        def __init__(self, name, repeat, aux, tableClass, *, description=""):
            BaseConverter.__init__(
                self, name, repeat, aux, tableClass, description=description
            )
            assert issubclass(self.tableClass, AATAction)
            self.classLookup = AATLookup("GlyphClasses", None, None, UShort)
            if issubclass(self.tableClass, ContextualMorphAction):
                self.perGlyphLookup = AATLookup("PerGlyphLookup", None, None, GlyphID)
            else:
                self.perGlyphLookup = None

        def read(self, reader, font, tableDict):
            table = AATStateTable()
            pos = reader.pos
            classTableReader = reader.getSubReader(0)
            stateArrayReader = reader.getSubReader(0)
            entryTableReader = reader.getSubReader(0)
            actionReader = None
            ligaturesReader = None
            table.GlyphClassCount = reader.readULong()
            classTableReader.seek(pos + reader.readULong())
            stateArrayReader.seek(pos + reader.readULong())
            entryTableReader.seek(pos + reader.readULong())
            if self.perGlyphLookup is not None:
                perGlyphTableReader = reader.getSubReader(0)
                perGlyphTableReader.seek(pos + reader.readULong())
            if issubclass(self.tableClass, LigatureMorphAction):
                actionReader = reader.getSubReader(0)
                actionReader.seek(pos + reader.readULong())
                ligComponentReader = reader.getSubReader(0)
                ligComponentReader.seek(pos + reader.readULong())
                ligaturesReader = reader.getSubReader(0)
                ligaturesReader.seek(pos + reader.readULong())
                numLigComponents = (ligaturesReader.pos - ligComponentReader.pos) // 2
                assert numLigComponents >= 0
                table.LigComponents = ligComponentReader.readUShortArray(numLigComponents)
                table.Ligatures = self._readLigatures(ligaturesReader, font)
            elif issubclass(self.tableClass, InsertionMorphAction):
                actionReader = reader.getSubReader(0)
                actionReader.seek(pos + reader.readULong())
            table.GlyphClasses = self.classLookup.read(classTableReader, font, tableDict)
            numStates = int(
                (entryTableReader.pos - stateArrayReader.pos) / (table.GlyphClassCount * 2)
            )
            for stateIndex in range(numStates):
                state = AATState()
                table.States.append(state)
                for glyphClass in range(table.GlyphClassCount):
                    entryIndex = stateArrayReader.readUShort()
                    state.Transitions[glyphClass] = self._readTransition(
                        entryTableReader, entryIndex, font, actionReader
                    )
            if self.perGlyphLookup is not None:
                table.PerGlyphLookups = self._readPerGlyphLookups(
                    table, perGlyphTableReader, font
                )
            return table

        def _readTransition(self, reader, entryIndex, font, actionReader):
            transition = self.tableClass()
            entryReader = reader.getSubReader(
                reader.pos + entryIndex * transition.staticSize
            )
            transition.decompile(entryReader, font, actionReader)
            return transition

        def _readLigatures(self, reader, font):
            limit = len(reader.data)
            numLigatureGlyphs = (limit - reader.pos) // 2
            return font.getGlyphNameMany(reader.readUShortArray(numLigatureGlyphs))

        def _countPerGlyphLookups(self, table):
            # Somewhat annoyingly, the morx table does not encode
            # the size of the per-glyph table. So we need to find
            # the maximum value that MorphActions use as index
            # into this table.
            numLookups = 0
            for state in table.States:
                for t in state.Transitions.values():
                    if isinstance(t, ContextualMorphAction):
                        if t.MarkIndex != 0xFFFF:
                            numLookups = max(numLookups, t.MarkIndex + 1)
                        if t.CurrentIndex != 0xFFFF:
                            numLookups = max(numLookups, t.CurrentIndex + 1)
            return numLookups

        def _readPerGlyphLookups(self, table, reader, font):
            pos = reader.pos
            lookups = []
            for _ in range(self._countPerGlyphLookups(table)):
                lookupReader = reader.getSubReader(0)
                lookupReader.seek(pos + reader.readULong())
                lookups.append(self.perGlyphLookup.read(lookupReader, font, {}))
            return lookups

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            glyphClassWriter = OTTableWriter()
            self.classLookup.write(
                glyphClassWriter, font, tableDict, value.GlyphClasses, repeatIndex=None
            )
            glyphClassData = pad(glyphClassWriter.getAllData(), 2)
            glyphClassCount = max(value.GlyphClasses.values()) + 1
            glyphClassTableOffset = 16  # size of STXHeader
            if self.perGlyphLookup is not None:
                glyphClassTableOffset += 4

            glyphClassTableOffset += self.tableClass.actionHeaderSize
            actionData, actionIndex = self.tableClass.compileActions(font, value.States)
            stateArrayData, entryTableData = self._compileStates(
                font, value.States, glyphClassCount, actionIndex
            )
            stateArrayOffset = glyphClassTableOffset + len(glyphClassData)
            entryTableOffset = stateArrayOffset + len(stateArrayData)
            perGlyphOffset = entryTableOffset + len(entryTableData)
            perGlyphData = pad(self._compilePerGlyphLookups(value, font), 4)
            if actionData is not None:
                actionOffset = entryTableOffset + len(entryTableData)
            else:
                actionOffset = None

            ligaturesOffset, ligComponentsOffset = None, None
            ligComponentsData = self._compileLigComponents(value, font)
            ligaturesData = self._compileLigatures(value, font)
            if ligComponentsData is not None:
                assert len(perGlyphData) == 0
                ligComponentsOffset = actionOffset + len(actionData)
                ligaturesOffset = ligComponentsOffset + len(ligComponentsData)

            writer.writeULong(glyphClassCount)
            writer.writeULong(glyphClassTableOffset)
            writer.writeULong(stateArrayOffset)
            writer.writeULong(entryTableOffset)
            if self.perGlyphLookup is not None:
                writer.writeULong(perGlyphOffset)
            if actionOffset is not None:
                writer.writeULong(actionOffset)
            if ligComponentsOffset is not None:
                writer.writeULong(ligComponentsOffset)
                writer.writeULong(ligaturesOffset)
            writer.writeData(glyphClassData)
            writer.writeData(stateArrayData)
            writer.writeData(entryTableData)
            writer.writeData(perGlyphData)
            if actionData is not None:
                writer.writeData(actionData)
            if ligComponentsData is not None:
                writer.writeData(ligComponentsData)
            if ligaturesData is not None:
                writer.writeData(ligaturesData)

        def _compileStates(self, font, states, glyphClassCount, actionIndex):
            stateArrayWriter = OTTableWriter()
            entries, entryIDs = [], {}
            for state in states:
                for glyphClass in range(glyphClassCount):
                    transition = state.Transitions[glyphClass]
                    entryWriter = OTTableWriter()
                    transition.compile(entryWriter, font, actionIndex)
                    entryData = entryWriter.getAllData()
                    assert (
                        len(entryData) == transition.staticSize
                    ), "%s has staticSize %d, " "but actually wrote %d bytes" % (
                        repr(transition),
                        transition.staticSize,
                        len(entryData),
                    )
                    entryIndex = entryIDs.get(entryData)
                    if entryIndex is None:
                        entryIndex = len(entries)
                        entryIDs[entryData] = entryIndex
                        entries.append(entryData)
                    stateArrayWriter.writeUShort(entryIndex)
            stateArrayData = pad(stateArrayWriter.getAllData(), 4)
            entryTableData = pad(bytesjoin(entries), 4)
            return stateArrayData, entryTableData

        def _compilePerGlyphLookups(self, table, font):
            if self.perGlyphLookup is None:
                return b""
            numLookups = self._countPerGlyphLookups(table)
            assert len(table.PerGlyphLookups) == numLookups, (
                "len(AATStateTable.PerGlyphLookups) is %d, "
                "but the actions inside the table refer to %d"
                % (len(table.PerGlyphLookups), numLookups)
            )
            writer = OTTableWriter()
            for lookup in table.PerGlyphLookups:
                lookupWriter = writer.getSubWriter()
                self.perGlyphLookup.write(lookupWriter, font, {}, lookup, None)
                writer.writeSubTable(lookupWriter, offsetSize=4)
            return writer.getAllData()

        def _compileLigComponents(self, table, font):
            if not hasattr(table, "LigComponents"):
                return None
            writer = OTTableWriter()
            for component in table.LigComponents:
                writer.writeUShort(component)
            return writer.getAllData()

        def _compileLigatures(self, table, font):
            if not hasattr(table, "Ligatures"):
                return None
            writer = OTTableWriter()
            for glyphName in table.Ligatures:
                writer.writeUShort(font.getGlyphID(glyphName))
            return writer.getAllData()

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.begintag(name, attrs)
            xmlWriter.newline()
            xmlWriter.comment("GlyphClassCount=%s" % value.GlyphClassCount)
            xmlWriter.newline()
            for g, klass in sorted(value.GlyphClasses.items()):
                xmlWriter.simpletag("GlyphClass", glyph=g, value=klass)
                xmlWriter.newline()
            for stateIndex, state in enumerate(value.States):
                xmlWriter.begintag("State", index=stateIndex)
                xmlWriter.newline()
                for glyphClass, trans in sorted(state.Transitions.items()):
                    trans.toXML(
                        xmlWriter,
                        font=font,
                        attrs={"onGlyphClass": glyphClass},
                        name="Transition",
                    )
                xmlWriter.endtag("State")
                xmlWriter.newline()
            for i, lookup in enumerate(value.PerGlyphLookups):
                xmlWriter.begintag("PerGlyphLookup", index=i)
                xmlWriter.newline()
                for glyph, val in sorted(lookup.items()):
                    xmlWriter.simpletag("Lookup", glyph=glyph, value=val)
                    xmlWriter.newline()
                xmlWriter.endtag("PerGlyphLookup")
                xmlWriter.newline()
            if hasattr(value, "LigComponents"):
                xmlWriter.begintag("LigComponents")
                xmlWriter.newline()
                for i, val in enumerate(getattr(value, "LigComponents")):
                    xmlWriter.simpletag("LigComponent", index=i, value=val)
                    xmlWriter.newline()
                xmlWriter.endtag("LigComponents")
                xmlWriter.newline()
            self._xmlWriteLigatures(xmlWriter, font, value, name, attrs)
            xmlWriter.endtag(name)
            xmlWriter.newline()

        def _xmlWriteLigatures(self, xmlWriter, font, value, name, attrs):
            if not hasattr(value, "Ligatures"):
                return
            xmlWriter.begintag("Ligatures")
            xmlWriter.newline()
            for i, g in enumerate(getattr(value, "Ligatures")):
                xmlWriter.simpletag("Ligature", index=i, glyph=g)
                xmlWriter.newline()
            xmlWriter.endtag("Ligatures")
            xmlWriter.newline()

        def xmlRead(self, attrs, content, font):
            table = AATStateTable()
            for eltName, eltAttrs, eltContent in filter(istuple, content):
                if eltName == "GlyphClass":
                    glyph = eltAttrs["glyph"]
                    value = eltAttrs["value"]
                    table.GlyphClasses[glyph] = safeEval(value)
                elif eltName == "State":
                    state = self._xmlReadState(eltAttrs, eltContent, font)
                    table.States.append(state)
                elif eltName == "PerGlyphLookup":
                    lookup = self.perGlyphLookup.xmlRead(eltAttrs, eltContent, font)
                    table.PerGlyphLookups.append(lookup)
                elif eltName == "LigComponents":
                    table.LigComponents = self._xmlReadLigComponents(
                        eltAttrs, eltContent, font
                    )
                elif eltName == "Ligatures":
                    table.Ligatures = self._xmlReadLigatures(eltAttrs, eltContent, font)
            table.GlyphClassCount = max(table.GlyphClasses.values()) + 1
            return table

        def _xmlReadState(self, attrs, content, font):
            state = AATState()
            for eltName, eltAttrs, eltContent in filter(istuple, content):
                if eltName == "Transition":
                    glyphClass = safeEval(eltAttrs["onGlyphClass"])
                    transition = self.tableClass()
                    transition.fromXML(eltName, eltAttrs, eltContent, font)
                    state.Transitions[glyphClass] = transition
            return state

        def _xmlReadLigComponents(self, attrs, content, font):
            ligComponents = []
            for eltName, eltAttrs, _eltContent in filter(istuple, content):
                if eltName == "LigComponent":
                    ligComponents.append(safeEval(eltAttrs["value"]))
            return ligComponents

        def _xmlReadLigatures(self, attrs, content, font):
            ligs = []
            for eltName, eltAttrs, _eltContent in filter(istuple, content):
                if eltName == "Ligature":
                    ligs.append(eltAttrs["glyph"])
            return ligs

    class CIDGlyphMap(BaseConverter):

        def read(self, reader, font, tableDict):
            numCIDs = reader.readUShort()
            result = {}
            for cid, glyphID in enumerate(reader.readUShortArray(numCIDs)):
                if glyphID != 0xFFFF:
                    result[cid] = font.getGlyphName(glyphID)
            return result

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            items = {cid: font.getGlyphID(glyph) for cid, glyph in value.items()}
            count = max(items) + 1 if items else 0
            writer.writeUShort(count)
            for cid in range(count):
                writer.writeUShort(items.get(cid, 0xFFFF))

        def xmlRead(self, attrs, content, font):
            result = {}
            for eName, eAttrs, _eContent in filter(istuple, content):
                if eName == "CID":
                    result[safeEval(eAttrs["cid"])] = eAttrs["glyph"].strip()
            return result

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.begintag(name, attrs)
            xmlWriter.newline()
            for cid, glyph in sorted(value.items()):
                if glyph is not None and glyph != 0xFFFF:
                    xmlWriter.simpletag("CID", cid=cid, glyph=glyph)
                    xmlWriter.newline()
            xmlWriter.endtag(name)
            xmlWriter.newline()

    class GlyphCIDMap(BaseConverter):

        def read(self, reader, font, tableDict):
            glyphOrder = font.getGlyphOrder()
            count = reader.readUShort()
            cids = reader.readUShortArray(count)
            if count > len(glyphOrder):
                log.warning(
                    "GlyphCIDMap has %d elements, "
                    "but the font has only %d glyphs; "
                    "ignoring the rest" % (count, len(glyphOrder))
                )
            result = {}
            for glyphID in range(min(len(cids), len(glyphOrder))):
                cid = cids[glyphID]
                if cid != 0xFFFF:
                    result[glyphOrder[glyphID]] = cid
            return result

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            items = {
                font.getGlyphID(g): cid
                for g, cid in value.items()
                if cid is not None and cid != 0xFFFF
            }
            count = max(items) + 1 if items else 0
            writer.writeUShort(count)
            for glyphID in range(count):
                writer.writeUShort(items.get(glyphID, 0xFFFF))

        def xmlRead(self, attrs, content, font):
            result = {}
            for eName, eAttrs, _eContent in filter(istuple, content):
                if eName == "CID":
                    result[eAttrs["glyph"]] = safeEval(eAttrs["value"])
            return result

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.begintag(name, attrs)
            xmlWriter.newline()
            for glyph, cid in sorted(value.items()):
                if cid is not None and cid != 0xFFFF:
                    xmlWriter.simpletag("CID", glyph=glyph, value=cid)
                    xmlWriter.newline()
            xmlWriter.endtag(name)
            xmlWriter.newline()

    class DeltaValue(BaseConverter):

        def read(self, reader, font, tableDict):
            StartSize = tableDict["StartSize"]
            EndSize = tableDict["EndSize"]
            DeltaFormat = tableDict["DeltaFormat"]
            assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat"
            nItems = EndSize - StartSize + 1
            nBits = 1 << DeltaFormat
            minusOffset = 1 << nBits
            mask = (1 << nBits) - 1
            signMask = 1 << (nBits - 1)

            DeltaValue = []
            tmp, shift = 0, 0
            for i in range(nItems):
                if shift == 0:
                    tmp, shift = reader.readUShort(), 16
                shift = shift - nBits
                value = (tmp >> shift) & mask
                if value & signMask:
                    value = value - minusOffset
                DeltaValue.append(value)
            return DeltaValue

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            StartSize = tableDict["StartSize"]
            EndSize = tableDict["EndSize"]
            DeltaFormat = tableDict["DeltaFormat"]
            DeltaValue = value
            assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat"
            nItems = EndSize - StartSize + 1
            nBits = 1 << DeltaFormat
            assert len(DeltaValue) == nItems
            mask = (1 << nBits) - 1

            tmp, shift = 0, 16
            for value in DeltaValue:
                shift = shift - nBits
                tmp = tmp | ((value & mask) << shift)
                if shift == 0:
                    writer.writeUShort(tmp)
                    tmp, shift = 0, 16
            if shift != 16:
                writer.writeUShort(tmp)

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.simpletag(name, attrs + [("value", value)])
            xmlWriter.newline()

        def xmlRead(self, attrs, content, font):
            return safeEval(attrs["value"])

    class VarIdxMapValue(BaseConverter):

        def read(self, reader, font, tableDict):
            fmt = tableDict["EntryFormat"]
            nItems = tableDict["MappingCount"]

            innerBits = 1 + (fmt & 0x000F)
            innerMask = (1 << innerBits) - 1
            outerMask = 0xFFFFFFFF - innerMask
            outerShift = 16 - innerBits

            entrySize = 1 + ((fmt & 0x0030) >> 4)
            readArray = {
                1: reader.readUInt8Array,
                2: reader.readUShortArray,
                3: reader.readUInt24Array,
                4: reader.readULongArray,
            }[entrySize]

            return [
                (((raw & outerMask) << outerShift) | (raw & innerMask))
                for raw in readArray(nItems)
            ]

        def write(self, writer, font, tableDict, value, repeatIndex=None):
            fmt = tableDict["EntryFormat"]
            mapping = value
            writer["MappingCount"].setValue(len(mapping))

            innerBits = 1 + (fmt & 0x000F)
            innerMask = (1 << innerBits) - 1
            outerShift = 16 - innerBits

            entrySize = 1 + ((fmt & 0x0030) >> 4)
            writeArray = {
                1: writer.writeUInt8Array,
                2: writer.writeUShortArray,
                3: writer.writeUInt24Array,
                4: writer.writeULongArray,
            }[entrySize]

            writeArray(
                [
                    (((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask))
                    for idx in mapping
                ]
            )

    class VarDataValue(BaseConverter):

        def read(self, reader, font, tableDict):
            values = []

            regionCount = tableDict["VarRegionCount"]
            wordCount = tableDict["NumShorts"]

            # https://github.com/fonttools/fonttools/issues/2279
            longWords = bool(wordCount & 0x8000)
            wordCount = wordCount & 0x7FFF

            if longWords:
                readBigArray, readSmallArray = reader.readLongArray, reader.readShortArray
            else:
                readBigArray, readSmallArray = reader.readShortArray, reader.readInt8Array

            n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
            values.extend(readBigArray(n1))
            values.extend(readSmallArray(n2 - n1))
            if n2 > regionCount:  # Padding
                del values[regionCount:]

            return values

        def write(self, writer, font, tableDict, values, repeatIndex=None):
            regionCount = tableDict["VarRegionCount"]
            wordCount = tableDict["NumShorts"]

            # https://github.com/fonttools/fonttools/issues/2279
            longWords = bool(wordCount & 0x8000)
            wordCount = wordCount & 0x7FFF

            (writeBigArray, writeSmallArray) = {
                False: (writer.writeShortArray, writer.writeInt8Array),
                True: (writer.writeLongArray, writer.writeShortArray),
            }[longWords]

            n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount)
            writeBigArray(values[:n1])
            writeSmallArray(values[n1:regionCount])
            if n2 > regionCount:  # Padding
                writer.writeSmallArray([0] * (n2 - regionCount))

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.simpletag(name, attrs + [("value", value)])
            xmlWriter.newline()

        def xmlRead(self, attrs, content, font):
            return safeEval(attrs["value"])

    class TupleValues:

        def read(self, data, font):
            return TupleVariation.decompileDeltas_(None, data)[0]

        def write(self, writer, font, tableDict, values, repeatIndex=None):
            optimizeSpeed = font.cfg[OPTIMIZE_FONT_SPEED]
            return bytes(
                TupleVariation.compileDeltaValues_(values, optimizeSize=not optimizeSpeed)
            )

        def xmlRead(self, attrs, content, font):
            return safeEval(attrs["value"])

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.simpletag(name, attrs + [("value", value)])
            xmlWriter.newline()

    class CFF2Index(BaseConverter):
        def __init__(
            self,
            name,
            repeat,
            aux,
            tableClass=None,
            *,
            itemClass=None,
            itemConverterClass=None,
            description="",
        ):
            BaseConverter.__init__(
                self, name, repeat, aux, tableClass, description=description
            )
            self._itemClass = itemClass
            self._converter = (
                itemConverterClass() if itemConverterClass is not None else None
            )

        def read(self, reader, font, tableDict):
            count = reader.readULong()
            if count == 0:
                return []
            offSize = reader.readUInt8()

            def getReadArray(reader, offSize):
                return {
                    1: reader.readUInt8Array,
                    2: reader.readUShortArray,
                    3: reader.readUInt24Array,
                    4: reader.readULongArray,
                }[offSize]

            readArray = getReadArray(reader, offSize)

            lazy = font.lazy is not False and count > 8
            if not lazy:
                offsets = readArray(count + 1)
                items = []
                lastOffset = offsets.pop(0)
                reader.readData(lastOffset - 1)  # In case first offset is not 1

                for offset in offsets:
                    assert lastOffset <= offset
                    item = reader.readData(offset - lastOffset)

                    if self._itemClass is not None:
                        obj = self._itemClass()
                        obj.decompile(item, font, reader.localState)
                        item = obj
                    elif self._converter is not None:
                        item = self._converter.read(item, font)

                    items.append(item)
                    lastOffset = offset
                return items
            else:

                def get_read_item():
                    reader_copy = reader.copy()
                    offset_pos = reader.pos
                    data_pos = offset_pos + (count + 1) * offSize - 1
                    readArray = getReadArray(reader_copy, offSize)

                    def read_item(i):
                        reader_copy.seek(offset_pos + i * offSize)
                        offsets = readArray(2)
                        reader_copy.seek(data_pos + offsets[0])
                        item = reader_copy.readData(offsets[1] - offsets[0])

                        if self._itemClass is not None:
                            obj = self._itemClass()
                            obj.decompile(item, font, reader_copy.localState)
                            item = obj
                        elif self._converter is not None:
                            item = self._converter.read(item, font)
                        return item

                    return read_item

                read_item = get_read_item()
                l = LazyList([read_item] * count)

                # TODO: Advance reader

                return l

        def write(self, writer, font, tableDict, values, repeatIndex=None):
            items = values

            writer.writeULong(len(items))
            if not len(items):
                return

            if self._itemClass is not None:
                items = [item.compile(font) for item in items]
            elif self._converter is not None:
                items = [
                    self._converter.write(writer, font, tableDict, item, i)
                    for i, item in enumerate(items)
                ]

            offsets = [len(item) for item in items]
            offsets = list(accumulate(offsets, initial=1))

            lastOffset = offsets[-1]
            offSize = (
                1
                if lastOffset < 0x100
                else 2 if lastOffset < 0x10000 else 3 if lastOffset < 0x1000000 else 4
            )
            writer.writeUInt8(offSize)

            writeArray = {
                1: writer.writeUInt8Array,
                2: writer.writeUShortArray,
                3: writer.writeUInt24Array,
                4: writer.writeULongArray,
            }[offSize]

            writeArray(offsets)
            for item in items:
                writer.writeData(item)

        def xmlRead(self, attrs, content, font):
            if self._itemClass is not None:
                obj = self._itemClass()
                obj.fromXML(None, attrs, content, font)
                return obj
            elif self._converter is not None:
                return self._converter.xmlRead(attrs, content, font)
            else:
                raise NotImplementedError()

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            if self._itemClass is not None:
                for i, item in enumerate(value):
                    item.toXML(xmlWriter, font, [("index", i)], name)
            elif self._converter is not None:
                for i, item in enumerate(value):
                    self._converter.xmlWrite(
                        xmlWriter, font, item, name, attrs + [("index", i)]
                    )
            else:
                raise NotImplementedError()

    class LookupFlag(UShort):

        def xmlWrite(self, xmlWriter, font, value, name, attrs):
            xmlWriter.simpletag(name, attrs + [("value", value)])
            flags = []
            if value & 0x01:
                flags.append("rightToLeft")
            if value & 0x02:
                flags.append("ignoreBaseGlyphs")
            if value & 0x04:
                flags.append("ignoreLigatures")
            if value & 0x08:
                flags.append("ignoreMarks")
            if value & 0x10:
                flags.append("useMarkFilteringSet")
            if value & 0xFF00:
                flags.append("markAttachmentType[%i]" % (value >> 8))
            if flags:
                xmlWriter.comment(" ".join(flags))
            xmlWriter.newline()

    class _UInt8Enum(UInt8):
        enumClass = NotImplemented

        def read(self, reader, font, tableDict):
            return self.enumClass(super().read(reader, font, tableDict))

        @classmethod
        def fromString(cls, value):
            return getattr(cls.enumClass, value.upper())

        @classmethod
        def toString(cls, value):
            return cls.enumClass(value).name.lower()

    class ExtendMode(_UInt8Enum):
        enumClass = _ExtendMode

    class CompositeMode(_UInt8Enum):
        enumClass = _CompositeMode

    converterMapping = {
        # type		class
        "int8": Int8,
        "int16": Short,
        "int32": Long,
        "uint8": UInt8,
        "uint16": UShort,
        "uint24": UInt24,
        "uint32": ULong,
        "char64": Char64,
        "Flags32": Flags32,
        "VarIndex": VarIndex,
        "Version": Version,
        "Tag": Tag,
        "GlyphID": GlyphID,
        "GlyphID32": GlyphID32,
        "NameID": NameID,
        "DeciPoints": DeciPoints,
        "Fixed": Fixed,
        "F2Dot14": F2Dot14,
        "Angle": Angle,
        "BiasedAngle": BiasedAngle,
        "struct": Struct,
        "Offset": Table,
        "LOffset": LTable,
        "Offset24": Table24,
        "ValueRecord": ValueRecord,
        "DeltaValue": DeltaValue,
        "VarIdxMapValue": VarIdxMapValue,
        "VarDataValue": VarDataValue,
        "LookupFlag": LookupFlag,
        "ExtendMode": ExtendMode,
        "CompositeMode": CompositeMode,
        "STATFlags": STATFlags,
        "TupleList": partial(CFF2Index, itemConverterClass=TupleValues),
        "VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph),
        # AAT
        "CIDGlyphMap": CIDGlyphMap,
        "GlyphCIDMap": GlyphCIDMap,
        "MortChain": StructWithLength,
        "MortSubtable": StructWithLength,
        "MorxChain": StructWithLength,
        "MorxSubtable": MorxSubtableConverter,
        # "Template" types
        "AATLookup": lambda C: partial(AATLookup, tableClass=C),
        "AATLookupWithDataOffset": lambda C: partial(AATLookupWithDataOffset, tableClass=C),
        "STXHeader": lambda C: partial(STXHeader, tableClass=C),
        "OffsetTo": lambda C: partial(Table, tableClass=C),
        "LOffsetTo": lambda C: partial(LTable, tableClass=C),
        "LOffset24To": lambda C: partial(Table24, tableClass=C),
    }