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