xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/tables/otTables.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1# coding: utf-8
2"""fontTools.ttLib.tables.otTables -- A collection of classes representing the various
3OpenType subtables.
4
5Most are constructed upon import from data in otData.py, all are populated with
6converter objects from otConverters.py.
7"""
8import copy
9from enum import IntEnum
10from functools import reduce
11from math import radians
12import itertools
13from collections import defaultdict, namedtuple
14from fontTools.ttLib.tables.otTraverse import dfs_base_table
15from fontTools.misc.arrayTools import quantizeRect
16from fontTools.misc.roundTools import otRound
17from fontTools.misc.transform import Transform, Identity
18from fontTools.misc.textTools import bytesjoin, pad, safeEval
19from fontTools.pens.boundsPen import ControlBoundsPen
20from fontTools.pens.transformPen import TransformPen
21from .otBase import (
22    BaseTable,
23    FormatSwitchingBaseTable,
24    ValueRecord,
25    CountReference,
26    getFormatSwitchingBaseTableClass,
27)
28from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
29import logging
30import struct
31from typing import TYPE_CHECKING, Iterator, List, Optional, Set
32
33if TYPE_CHECKING:
34    from fontTools.ttLib.ttGlyphSet import _TTGlyphSet
35
36
37log = logging.getLogger(__name__)
38
39
40class AATStateTable(object):
41    def __init__(self):
42        self.GlyphClasses = {}  # GlyphID --> GlyphClass
43        self.States = []  # List of AATState, indexed by state number
44        self.PerGlyphLookups = []  # [{GlyphID:GlyphID}, ...]
45
46
47class AATState(object):
48    def __init__(self):
49        self.Transitions = {}  # GlyphClass --> AATAction
50
51
52class AATAction(object):
53    _FLAGS = None
54
55    @staticmethod
56    def compileActions(font, states):
57        return (None, None)
58
59    def _writeFlagsToXML(self, xmlWriter):
60        flags = [f for f in self._FLAGS if self.__dict__[f]]
61        if flags:
62            xmlWriter.simpletag("Flags", value=",".join(flags))
63            xmlWriter.newline()
64        if self.ReservedFlags != 0:
65            xmlWriter.simpletag("ReservedFlags", value="0x%04X" % self.ReservedFlags)
66            xmlWriter.newline()
67
68    def _setFlag(self, flag):
69        assert flag in self._FLAGS, "unsupported flag %s" % flag
70        self.__dict__[flag] = True
71
72
73class RearrangementMorphAction(AATAction):
74    staticSize = 4
75    actionHeaderSize = 0
76    _FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"]
77
78    _VERBS = {
79        0: "no change",
80        1: "Ax ⇒ xA",
81        2: "xD ⇒ Dx",
82        3: "AxD ⇒ DxA",
83        4: "ABx ⇒ xAB",
84        5: "ABx ⇒ xBA",
85        6: "xCD ⇒ CDx",
86        7: "xCD ⇒ DCx",
87        8: "AxCD ⇒ CDxA",
88        9: "AxCD ⇒ DCxA",
89        10: "ABxD ⇒ DxAB",
90        11: "ABxD ⇒ DxBA",
91        12: "ABxCD ⇒ CDxAB",
92        13: "ABxCD ⇒ CDxBA",
93        14: "ABxCD ⇒ DCxAB",
94        15: "ABxCD ⇒ DCxBA",
95    }
96
97    def __init__(self):
98        self.NewState = 0
99        self.Verb = 0
100        self.MarkFirst = False
101        self.DontAdvance = False
102        self.MarkLast = False
103        self.ReservedFlags = 0
104
105    def compile(self, writer, font, actionIndex):
106        assert actionIndex is None
107        writer.writeUShort(self.NewState)
108        assert self.Verb >= 0 and self.Verb <= 15, self.Verb
109        flags = self.Verb | self.ReservedFlags
110        if self.MarkFirst:
111            flags |= 0x8000
112        if self.DontAdvance:
113            flags |= 0x4000
114        if self.MarkLast:
115            flags |= 0x2000
116        writer.writeUShort(flags)
117
118    def decompile(self, reader, font, actionReader):
119        assert actionReader is None
120        self.NewState = reader.readUShort()
121        flags = reader.readUShort()
122        self.Verb = flags & 0xF
123        self.MarkFirst = bool(flags & 0x8000)
124        self.DontAdvance = bool(flags & 0x4000)
125        self.MarkLast = bool(flags & 0x2000)
126        self.ReservedFlags = flags & 0x1FF0
127
128    def toXML(self, xmlWriter, font, attrs, name):
129        xmlWriter.begintag(name, **attrs)
130        xmlWriter.newline()
131        xmlWriter.simpletag("NewState", value=self.NewState)
132        xmlWriter.newline()
133        self._writeFlagsToXML(xmlWriter)
134        xmlWriter.simpletag("Verb", value=self.Verb)
135        verbComment = self._VERBS.get(self.Verb)
136        if verbComment is not None:
137            xmlWriter.comment(verbComment)
138        xmlWriter.newline()
139        xmlWriter.endtag(name)
140        xmlWriter.newline()
141
142    def fromXML(self, name, attrs, content, font):
143        self.NewState = self.Verb = self.ReservedFlags = 0
144        self.MarkFirst = self.DontAdvance = self.MarkLast = False
145        content = [t for t in content if isinstance(t, tuple)]
146        for eltName, eltAttrs, eltContent in content:
147            if eltName == "NewState":
148                self.NewState = safeEval(eltAttrs["value"])
149            elif eltName == "Verb":
150                self.Verb = safeEval(eltAttrs["value"])
151            elif eltName == "ReservedFlags":
152                self.ReservedFlags = safeEval(eltAttrs["value"])
153            elif eltName == "Flags":
154                for flag in eltAttrs["value"].split(","):
155                    self._setFlag(flag.strip())
156
157
158class ContextualMorphAction(AATAction):
159    staticSize = 8
160    actionHeaderSize = 0
161    _FLAGS = ["SetMark", "DontAdvance"]
162
163    def __init__(self):
164        self.NewState = 0
165        self.SetMark, self.DontAdvance = False, False
166        self.ReservedFlags = 0
167        self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
168
169    def compile(self, writer, font, actionIndex):
170        assert actionIndex is None
171        writer.writeUShort(self.NewState)
172        flags = self.ReservedFlags
173        if self.SetMark:
174            flags |= 0x8000
175        if self.DontAdvance:
176            flags |= 0x4000
177        writer.writeUShort(flags)
178        writer.writeUShort(self.MarkIndex)
179        writer.writeUShort(self.CurrentIndex)
180
181    def decompile(self, reader, font, actionReader):
182        assert actionReader is None
183        self.NewState = reader.readUShort()
184        flags = reader.readUShort()
185        self.SetMark = bool(flags & 0x8000)
186        self.DontAdvance = bool(flags & 0x4000)
187        self.ReservedFlags = flags & 0x3FFF
188        self.MarkIndex = reader.readUShort()
189        self.CurrentIndex = reader.readUShort()
190
191    def toXML(self, xmlWriter, font, attrs, name):
192        xmlWriter.begintag(name, **attrs)
193        xmlWriter.newline()
194        xmlWriter.simpletag("NewState", value=self.NewState)
195        xmlWriter.newline()
196        self._writeFlagsToXML(xmlWriter)
197        xmlWriter.simpletag("MarkIndex", value=self.MarkIndex)
198        xmlWriter.newline()
199        xmlWriter.simpletag("CurrentIndex", value=self.CurrentIndex)
200        xmlWriter.newline()
201        xmlWriter.endtag(name)
202        xmlWriter.newline()
203
204    def fromXML(self, name, attrs, content, font):
205        self.NewState = self.ReservedFlags = 0
206        self.SetMark = self.DontAdvance = False
207        self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
208        content = [t for t in content if isinstance(t, tuple)]
209        for eltName, eltAttrs, eltContent in content:
210            if eltName == "NewState":
211                self.NewState = safeEval(eltAttrs["value"])
212            elif eltName == "Flags":
213                for flag in eltAttrs["value"].split(","):
214                    self._setFlag(flag.strip())
215            elif eltName == "ReservedFlags":
216                self.ReservedFlags = safeEval(eltAttrs["value"])
217            elif eltName == "MarkIndex":
218                self.MarkIndex = safeEval(eltAttrs["value"])
219            elif eltName == "CurrentIndex":
220                self.CurrentIndex = safeEval(eltAttrs["value"])
221
222
223class LigAction(object):
224    def __init__(self):
225        self.Store = False
226        # GlyphIndexDelta is a (possibly negative) delta that gets
227        # added to the glyph ID at the top of the AAT runtime
228        # execution stack. It is *not* a byte offset into the
229        # morx table. The result of the addition, which is performed
230        # at run time by the shaping engine, is an index into
231        # the ligature components table. See 'morx' specification.
232        # In the AAT specification, this field is called Offset;
233        # but its meaning is quite different from other offsets
234        # in either AAT or OpenType, so we use a different name.
235        self.GlyphIndexDelta = 0
236
237
238class LigatureMorphAction(AATAction):
239    staticSize = 6
240
241    # 4 bytes for each of {action,ligComponents,ligatures}Offset
242    actionHeaderSize = 12
243
244    _FLAGS = ["SetComponent", "DontAdvance"]
245
246    def __init__(self):
247        self.NewState = 0
248        self.SetComponent, self.DontAdvance = False, False
249        self.ReservedFlags = 0
250        self.Actions = []
251
252    def compile(self, writer, font, actionIndex):
253        assert actionIndex is not None
254        writer.writeUShort(self.NewState)
255        flags = self.ReservedFlags
256        if self.SetComponent:
257            flags |= 0x8000
258        if self.DontAdvance:
259            flags |= 0x4000
260        if len(self.Actions) > 0:
261            flags |= 0x2000
262        writer.writeUShort(flags)
263        if len(self.Actions) > 0:
264            actions = self.compileLigActions()
265            writer.writeUShort(actionIndex[actions])
266        else:
267            writer.writeUShort(0)
268
269    def decompile(self, reader, font, actionReader):
270        assert actionReader is not None
271        self.NewState = reader.readUShort()
272        flags = reader.readUShort()
273        self.SetComponent = bool(flags & 0x8000)
274        self.DontAdvance = bool(flags & 0x4000)
275        performAction = bool(flags & 0x2000)
276        # As of 2017-09-12, the 'morx' specification says that
277        # the reserved bitmask in ligature subtables is 0x3FFF.
278        # However, the specification also defines a flag 0x2000,
279        # so the reserved value should actually be 0x1FFF.
280        # TODO: Report this specification bug to Apple.
281        self.ReservedFlags = flags & 0x1FFF
282        actionIndex = reader.readUShort()
283        if performAction:
284            self.Actions = self._decompileLigActions(actionReader, actionIndex)
285        else:
286            self.Actions = []
287
288    @staticmethod
289    def compileActions(font, states):
290        result, actions, actionIndex = b"", set(), {}
291        for state in states:
292            for _glyphClass, trans in state.Transitions.items():
293                actions.add(trans.compileLigActions())
294        # Sort the compiled actions in decreasing order of
295        # length, so that the longer sequence come before the
296        # shorter ones.  For each compiled action ABCD, its
297        # suffixes BCD, CD, and D do not be encoded separately
298        # (in case they occur); instead, we can just store an
299        # index that points into the middle of the longer
300        # sequence. Every compiled AAT ligature sequence is
301        # terminated with an end-of-sequence flag, which can
302        # only be set on the last element of the sequence.
303        # Therefore, it is sufficient to consider just the
304        # suffixes.
305        for a in sorted(actions, key=lambda x: (-len(x), x)):
306            if a not in actionIndex:
307                for i in range(0, len(a), 4):
308                    suffix = a[i:]
309                    suffixIndex = (len(result) + i) // 4
310                    actionIndex.setdefault(suffix, suffixIndex)
311                result += a
312        result = pad(result, 4)
313        return (result, actionIndex)
314
315    def compileLigActions(self):
316        result = []
317        for i, action in enumerate(self.Actions):
318            last = i == len(self.Actions) - 1
319            value = action.GlyphIndexDelta & 0x3FFFFFFF
320            value |= 0x80000000 if last else 0
321            value |= 0x40000000 if action.Store else 0
322            result.append(struct.pack(">L", value))
323        return bytesjoin(result)
324
325    def _decompileLigActions(self, actionReader, actionIndex):
326        actions = []
327        last = False
328        reader = actionReader.getSubReader(actionReader.pos + actionIndex * 4)
329        while not last:
330            value = reader.readULong()
331            last = bool(value & 0x80000000)
332            action = LigAction()
333            actions.append(action)
334            action.Store = bool(value & 0x40000000)
335            delta = value & 0x3FFFFFFF
336            if delta >= 0x20000000:  # sign-extend 30-bit value
337                delta = -0x40000000 + delta
338            action.GlyphIndexDelta = delta
339        return actions
340
341    def fromXML(self, name, attrs, content, font):
342        self.NewState = self.ReservedFlags = 0
343        self.SetComponent = self.DontAdvance = False
344        self.ReservedFlags = 0
345        self.Actions = []
346        content = [t for t in content if isinstance(t, tuple)]
347        for eltName, eltAttrs, eltContent in content:
348            if eltName == "NewState":
349                self.NewState = safeEval(eltAttrs["value"])
350            elif eltName == "Flags":
351                for flag in eltAttrs["value"].split(","):
352                    self._setFlag(flag.strip())
353            elif eltName == "ReservedFlags":
354                self.ReservedFlags = safeEval(eltAttrs["value"])
355            elif eltName == "Action":
356                action = LigAction()
357                flags = eltAttrs.get("Flags", "").split(",")
358                flags = [f.strip() for f in flags]
359                action.Store = "Store" in flags
360                action.GlyphIndexDelta = safeEval(eltAttrs["GlyphIndexDelta"])
361                self.Actions.append(action)
362
363    def toXML(self, xmlWriter, font, attrs, name):
364        xmlWriter.begintag(name, **attrs)
365        xmlWriter.newline()
366        xmlWriter.simpletag("NewState", value=self.NewState)
367        xmlWriter.newline()
368        self._writeFlagsToXML(xmlWriter)
369        for action in self.Actions:
370            attribs = [("GlyphIndexDelta", action.GlyphIndexDelta)]
371            if action.Store:
372                attribs.append(("Flags", "Store"))
373            xmlWriter.simpletag("Action", attribs)
374            xmlWriter.newline()
375        xmlWriter.endtag(name)
376        xmlWriter.newline()
377
378
379class InsertionMorphAction(AATAction):
380    staticSize = 8
381    actionHeaderSize = 4  # 4 bytes for actionOffset
382    _FLAGS = [
383        "SetMark",
384        "DontAdvance",
385        "CurrentIsKashidaLike",
386        "MarkedIsKashidaLike",
387        "CurrentInsertBefore",
388        "MarkedInsertBefore",
389    ]
390
391    def __init__(self):
392        self.NewState = 0
393        for flag in self._FLAGS:
394            setattr(self, flag, False)
395        self.ReservedFlags = 0
396        self.CurrentInsertionAction, self.MarkedInsertionAction = [], []
397
398    def compile(self, writer, font, actionIndex):
399        assert actionIndex is not None
400        writer.writeUShort(self.NewState)
401        flags = self.ReservedFlags
402        if self.SetMark:
403            flags |= 0x8000
404        if self.DontAdvance:
405            flags |= 0x4000
406        if self.CurrentIsKashidaLike:
407            flags |= 0x2000
408        if self.MarkedIsKashidaLike:
409            flags |= 0x1000
410        if self.CurrentInsertBefore:
411            flags |= 0x0800
412        if self.MarkedInsertBefore:
413            flags |= 0x0400
414        flags |= len(self.CurrentInsertionAction) << 5
415        flags |= len(self.MarkedInsertionAction)
416        writer.writeUShort(flags)
417        if len(self.CurrentInsertionAction) > 0:
418            currentIndex = actionIndex[tuple(self.CurrentInsertionAction)]
419        else:
420            currentIndex = 0xFFFF
421        writer.writeUShort(currentIndex)
422        if len(self.MarkedInsertionAction) > 0:
423            markedIndex = actionIndex[tuple(self.MarkedInsertionAction)]
424        else:
425            markedIndex = 0xFFFF
426        writer.writeUShort(markedIndex)
427
428    def decompile(self, reader, font, actionReader):
429        assert actionReader is not None
430        self.NewState = reader.readUShort()
431        flags = reader.readUShort()
432        self.SetMark = bool(flags & 0x8000)
433        self.DontAdvance = bool(flags & 0x4000)
434        self.CurrentIsKashidaLike = bool(flags & 0x2000)
435        self.MarkedIsKashidaLike = bool(flags & 0x1000)
436        self.CurrentInsertBefore = bool(flags & 0x0800)
437        self.MarkedInsertBefore = bool(flags & 0x0400)
438        self.CurrentInsertionAction = self._decompileInsertionAction(
439            actionReader, font, index=reader.readUShort(), count=((flags & 0x03E0) >> 5)
440        )
441        self.MarkedInsertionAction = self._decompileInsertionAction(
442            actionReader, font, index=reader.readUShort(), count=(flags & 0x001F)
443        )
444
445    def _decompileInsertionAction(self, actionReader, font, index, count):
446        if index == 0xFFFF or count == 0:
447            return []
448        reader = actionReader.getSubReader(actionReader.pos + index * 2)
449        return font.getGlyphNameMany(reader.readUShortArray(count))
450
451    def toXML(self, xmlWriter, font, attrs, name):
452        xmlWriter.begintag(name, **attrs)
453        xmlWriter.newline()
454        xmlWriter.simpletag("NewState", value=self.NewState)
455        xmlWriter.newline()
456        self._writeFlagsToXML(xmlWriter)
457        for g in self.CurrentInsertionAction:
458            xmlWriter.simpletag("CurrentInsertionAction", glyph=g)
459            xmlWriter.newline()
460        for g in self.MarkedInsertionAction:
461            xmlWriter.simpletag("MarkedInsertionAction", glyph=g)
462            xmlWriter.newline()
463        xmlWriter.endtag(name)
464        xmlWriter.newline()
465
466    def fromXML(self, name, attrs, content, font):
467        self.__init__()
468        content = [t for t in content if isinstance(t, tuple)]
469        for eltName, eltAttrs, eltContent in content:
470            if eltName == "NewState":
471                self.NewState = safeEval(eltAttrs["value"])
472            elif eltName == "Flags":
473                for flag in eltAttrs["value"].split(","):
474                    self._setFlag(flag.strip())
475            elif eltName == "CurrentInsertionAction":
476                self.CurrentInsertionAction.append(eltAttrs["glyph"])
477            elif eltName == "MarkedInsertionAction":
478                self.MarkedInsertionAction.append(eltAttrs["glyph"])
479            else:
480                assert False, eltName
481
482    @staticmethod
483    def compileActions(font, states):
484        actions, actionIndex, result = set(), {}, b""
485        for state in states:
486            for _glyphClass, trans in state.Transitions.items():
487                if trans.CurrentInsertionAction is not None:
488                    actions.add(tuple(trans.CurrentInsertionAction))
489                if trans.MarkedInsertionAction is not None:
490                    actions.add(tuple(trans.MarkedInsertionAction))
491        # Sort the compiled actions in decreasing order of
492        # length, so that the longer sequence come before the
493        # shorter ones.
494        for action in sorted(actions, key=lambda x: (-len(x), x)):
495            # We insert all sub-sequences of the action glyph sequence
496            # into actionIndex. For example, if one action triggers on
497            # glyph sequence [A, B, C, D, E] and another action triggers
498            # on [C, D], we return result=[A, B, C, D, E] (as list of
499            # encoded glyph IDs), and actionIndex={('A','B','C','D','E'): 0,
500            # ('C','D'): 2}.
501            if action in actionIndex:
502                continue
503            for start in range(0, len(action)):
504                startIndex = (len(result) // 2) + start
505                for limit in range(start, len(action)):
506                    glyphs = action[start : limit + 1]
507                    actionIndex.setdefault(glyphs, startIndex)
508            for glyph in action:
509                glyphID = font.getGlyphID(glyph)
510                result += struct.pack(">H", glyphID)
511        return result, actionIndex
512
513
514class FeatureParams(BaseTable):
515    def compile(self, writer, font):
516        assert (
517            featureParamTypes.get(writer["FeatureTag"]) == self.__class__
518        ), "Wrong FeatureParams type for feature '%s': %s" % (
519            writer["FeatureTag"],
520            self.__class__.__name__,
521        )
522        BaseTable.compile(self, writer, font)
523
524    def toXML(self, xmlWriter, font, attrs=None, name=None):
525        BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__)
526
527
528class FeatureParamsSize(FeatureParams):
529    pass
530
531
532class FeatureParamsStylisticSet(FeatureParams):
533    pass
534
535
536class FeatureParamsCharacterVariants(FeatureParams):
537    pass
538
539
540class Coverage(FormatSwitchingBaseTable):
541    # manual implementation to get rid of glyphID dependencies
542
543    def populateDefaults(self, propagator=None):
544        if not hasattr(self, "glyphs"):
545            self.glyphs = []
546
547    def postRead(self, rawTable, font):
548        if self.Format == 1:
549            self.glyphs = rawTable["GlyphArray"]
550        elif self.Format == 2:
551            glyphs = self.glyphs = []
552            ranges = rawTable["RangeRecord"]
553            # Some SIL fonts have coverage entries that don't have sorted
554            # StartCoverageIndex.  If it is so, fixup and warn.  We undo
555            # this when writing font out.
556            sorted_ranges = sorted(ranges, key=lambda a: a.StartCoverageIndex)
557            if ranges != sorted_ranges:
558                log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.")
559                ranges = sorted_ranges
560            del sorted_ranges
561            for r in ranges:
562                start = r.Start
563                end = r.End
564                startID = font.getGlyphID(start)
565                endID = font.getGlyphID(end) + 1
566                glyphs.extend(font.getGlyphNameMany(range(startID, endID)))
567        else:
568            self.glyphs = []
569            log.warning("Unknown Coverage format: %s", self.Format)
570        del self.Format  # Don't need this anymore
571
572    def preWrite(self, font):
573        glyphs = getattr(self, "glyphs", None)
574        if glyphs is None:
575            glyphs = self.glyphs = []
576        format = 1
577        rawTable = {"GlyphArray": glyphs}
578        if glyphs:
579            # find out whether Format 2 is more compact or not
580            glyphIDs = font.getGlyphIDMany(glyphs)
581            brokenOrder = sorted(glyphIDs) != glyphIDs
582
583            last = glyphIDs[0]
584            ranges = [[last]]
585            for glyphID in glyphIDs[1:]:
586                if glyphID != last + 1:
587                    ranges[-1].append(last)
588                    ranges.append([glyphID])
589                last = glyphID
590            ranges[-1].append(last)
591
592            if brokenOrder or len(ranges) * 3 < len(glyphs):  # 3 words vs. 1 word
593                # Format 2 is more compact
594                index = 0
595                for i in range(len(ranges)):
596                    start, end = ranges[i]
597                    r = RangeRecord()
598                    r.StartID = start
599                    r.Start = font.getGlyphName(start)
600                    r.End = font.getGlyphName(end)
601                    r.StartCoverageIndex = index
602                    ranges[i] = r
603                    index = index + end - start + 1
604                if brokenOrder:
605                    log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.")
606                    ranges.sort(key=lambda a: a.StartID)
607                for r in ranges:
608                    del r.StartID
609                format = 2
610                rawTable = {"RangeRecord": ranges}
611            # else:
612            # 	fallthrough; Format 1 is more compact
613        self.Format = format
614        return rawTable
615
616    def toXML2(self, xmlWriter, font):
617        for glyphName in getattr(self, "glyphs", []):
618            xmlWriter.simpletag("Glyph", value=glyphName)
619            xmlWriter.newline()
620
621    def fromXML(self, name, attrs, content, font):
622        glyphs = getattr(self, "glyphs", None)
623        if glyphs is None:
624            glyphs = []
625            self.glyphs = glyphs
626        glyphs.append(attrs["value"])
627
628
629# The special 0xFFFFFFFF delta-set index is used to indicate that there
630# is no variation data in the ItemVariationStore for a given variable field
631NO_VARIATION_INDEX = 0xFFFFFFFF
632
633
634class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
635    def populateDefaults(self, propagator=None):
636        if not hasattr(self, "mapping"):
637            self.mapping = []
638
639    def postRead(self, rawTable, font):
640        assert (rawTable["EntryFormat"] & 0xFFC0) == 0
641        self.mapping = rawTable["mapping"]
642
643    @staticmethod
644    def getEntryFormat(mapping):
645        ored = 0
646        for idx in mapping:
647            ored |= idx
648
649        inner = ored & 0xFFFF
650        innerBits = 0
651        while inner:
652            innerBits += 1
653            inner >>= 1
654        innerBits = max(innerBits, 1)
655        assert innerBits <= 16
656
657        ored = (ored >> (16 - innerBits)) | (ored & ((1 << innerBits) - 1))
658        if ored <= 0x000000FF:
659            entrySize = 1
660        elif ored <= 0x0000FFFF:
661            entrySize = 2
662        elif ored <= 0x00FFFFFF:
663            entrySize = 3
664        else:
665            entrySize = 4
666
667        return ((entrySize - 1) << 4) | (innerBits - 1)
668
669    def preWrite(self, font):
670        mapping = getattr(self, "mapping", None)
671        if mapping is None:
672            mapping = self.mapping = []
673        self.Format = 1 if len(mapping) > 0xFFFF else 0
674        rawTable = self.__dict__.copy()
675        rawTable["MappingCount"] = len(mapping)
676        rawTable["EntryFormat"] = self.getEntryFormat(mapping)
677        return rawTable
678
679    def toXML2(self, xmlWriter, font):
680        # Make xml dump less verbose, by omitting no-op entries like:
681        #   <Map index="..." outer="65535" inner="65535"/>
682        xmlWriter.comment("Omitted values default to 0xFFFF/0xFFFF (no variations)")
683        xmlWriter.newline()
684        for i, value in enumerate(getattr(self, "mapping", [])):
685            attrs = [("index", i)]
686            if value != NO_VARIATION_INDEX:
687                attrs.extend(
688                    [
689                        ("outer", value >> 16),
690                        ("inner", value & 0xFFFF),
691                    ]
692                )
693            xmlWriter.simpletag("Map", attrs)
694            xmlWriter.newline()
695
696    def fromXML(self, name, attrs, content, font):
697        mapping = getattr(self, "mapping", None)
698        if mapping is None:
699            self.mapping = mapping = []
700        index = safeEval(attrs["index"])
701        outer = safeEval(attrs.get("outer", "0xFFFF"))
702        inner = safeEval(attrs.get("inner", "0xFFFF"))
703        assert inner <= 0xFFFF
704        mapping.insert(index, (outer << 16) | inner)
705
706
707class VarIdxMap(BaseTable):
708    def populateDefaults(self, propagator=None):
709        if not hasattr(self, "mapping"):
710            self.mapping = {}
711
712    def postRead(self, rawTable, font):
713        assert (rawTable["EntryFormat"] & 0xFFC0) == 0
714        glyphOrder = font.getGlyphOrder()
715        mapList = rawTable["mapping"]
716        mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList)))
717        self.mapping = dict(zip(glyphOrder, mapList))
718
719    def preWrite(self, font):
720        mapping = getattr(self, "mapping", None)
721        if mapping is None:
722            mapping = self.mapping = {}
723
724        glyphOrder = font.getGlyphOrder()
725        mapping = [mapping[g] for g in glyphOrder]
726        while len(mapping) > 1 and mapping[-2] == mapping[-1]:
727            del mapping[-1]
728
729        rawTable = {"mapping": mapping}
730        rawTable["MappingCount"] = len(mapping)
731        rawTable["EntryFormat"] = DeltaSetIndexMap.getEntryFormat(mapping)
732        return rawTable
733
734    def toXML2(self, xmlWriter, font):
735        for glyph, value in sorted(getattr(self, "mapping", {}).items()):
736            attrs = (
737                ("glyph", glyph),
738                ("outer", value >> 16),
739                ("inner", value & 0xFFFF),
740            )
741            xmlWriter.simpletag("Map", attrs)
742            xmlWriter.newline()
743
744    def fromXML(self, name, attrs, content, font):
745        mapping = getattr(self, "mapping", None)
746        if mapping is None:
747            mapping = {}
748            self.mapping = mapping
749        try:
750            glyph = attrs["glyph"]
751        except:  # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836
752            glyph = font.getGlyphOrder()[attrs["index"]]
753        outer = safeEval(attrs["outer"])
754        inner = safeEval(attrs["inner"])
755        assert inner <= 0xFFFF
756        mapping[glyph] = (outer << 16) | inner
757
758
759class VarRegionList(BaseTable):
760    def preWrite(self, font):
761        # The OT spec says VarStore.VarRegionList.RegionAxisCount should always
762        # be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule
763        # even when the VarRegionList is empty. We can't treat RegionAxisCount
764        # like a normal propagated count (== len(Region[i].VarRegionAxis)),
765        # otherwise it would default to 0 if VarRegionList is empty.
766        # Thus, we force it to always be equal to fvar.axisCount.
767        # https://github.com/khaledhosny/ots/pull/192
768        fvarTable = font.get("fvar")
769        if fvarTable:
770            self.RegionAxisCount = len(fvarTable.axes)
771        return {
772            **self.__dict__,
773            "RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount"),
774        }
775
776
777class SingleSubst(FormatSwitchingBaseTable):
778    def populateDefaults(self, propagator=None):
779        if not hasattr(self, "mapping"):
780            self.mapping = {}
781
782    def postRead(self, rawTable, font):
783        mapping = {}
784        input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
785        if self.Format == 1:
786            delta = rawTable["DeltaGlyphID"]
787            inputGIDS = font.getGlyphIDMany(input)
788            outGIDS = [(glyphID + delta) % 65536 for glyphID in inputGIDS]
789            outNames = font.getGlyphNameMany(outGIDS)
790            for inp, out in zip(input, outNames):
791                mapping[inp] = out
792        elif self.Format == 2:
793            assert (
794                len(input) == rawTable["GlyphCount"]
795            ), "invalid SingleSubstFormat2 table"
796            subst = rawTable["Substitute"]
797            for inp, sub in zip(input, subst):
798                mapping[inp] = sub
799        else:
800            assert 0, "unknown format: %s" % self.Format
801        self.mapping = mapping
802        del self.Format  # Don't need this anymore
803
804    def preWrite(self, font):
805        mapping = getattr(self, "mapping", None)
806        if mapping is None:
807            mapping = self.mapping = {}
808        items = list(mapping.items())
809        getGlyphID = font.getGlyphID
810        gidItems = [(getGlyphID(a), getGlyphID(b)) for a, b in items]
811        sortableItems = sorted(zip(gidItems, items))
812
813        # figure out format
814        format = 2
815        delta = None
816        for inID, outID in gidItems:
817            if delta is None:
818                delta = (outID - inID) % 65536
819
820            if (inID + delta) % 65536 != outID:
821                break
822        else:
823            if delta is None:
824                # the mapping is empty, better use format 2
825                format = 2
826            else:
827                format = 1
828
829        rawTable = {}
830        self.Format = format
831        cov = Coverage()
832        input = [item[1][0] for item in sortableItems]
833        subst = [item[1][1] for item in sortableItems]
834        cov.glyphs = input
835        rawTable["Coverage"] = cov
836        if format == 1:
837            assert delta is not None
838            rawTable["DeltaGlyphID"] = delta
839        else:
840            rawTable["Substitute"] = subst
841        return rawTable
842
843    def toXML2(self, xmlWriter, font):
844        items = sorted(self.mapping.items())
845        for inGlyph, outGlyph in items:
846            xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", outGlyph)])
847            xmlWriter.newline()
848
849    def fromXML(self, name, attrs, content, font):
850        mapping = getattr(self, "mapping", None)
851        if mapping is None:
852            mapping = {}
853            self.mapping = mapping
854        mapping[attrs["in"]] = attrs["out"]
855
856
857class MultipleSubst(FormatSwitchingBaseTable):
858    def populateDefaults(self, propagator=None):
859        if not hasattr(self, "mapping"):
860            self.mapping = {}
861
862    def postRead(self, rawTable, font):
863        mapping = {}
864        if self.Format == 1:
865            glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"])
866            subst = [s.Substitute for s in rawTable["Sequence"]]
867            mapping = dict(zip(glyphs, subst))
868        else:
869            assert 0, "unknown format: %s" % self.Format
870        self.mapping = mapping
871        del self.Format  # Don't need this anymore
872
873    def preWrite(self, font):
874        mapping = getattr(self, "mapping", None)
875        if mapping is None:
876            mapping = self.mapping = {}
877        cov = Coverage()
878        cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID)
879        self.Format = 1
880        rawTable = {
881            "Coverage": cov,
882            "Sequence": [self.makeSequence_(mapping[glyph]) for glyph in cov.glyphs],
883        }
884        return rawTable
885
886    def toXML2(self, xmlWriter, font):
887        items = sorted(self.mapping.items())
888        for inGlyph, outGlyphs in items:
889            out = ",".join(outGlyphs)
890            xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", out)])
891            xmlWriter.newline()
892
893    def fromXML(self, name, attrs, content, font):
894        mapping = getattr(self, "mapping", None)
895        if mapping is None:
896            mapping = {}
897            self.mapping = mapping
898
899        # TTX v3.0 and earlier.
900        if name == "Coverage":
901            self.old_coverage_ = []
902            for element in content:
903                if not isinstance(element, tuple):
904                    continue
905                element_name, element_attrs, _ = element
906                if element_name == "Glyph":
907                    self.old_coverage_.append(element_attrs["value"])
908            return
909        if name == "Sequence":
910            index = int(attrs.get("index", len(mapping)))
911            glyph = self.old_coverage_[index]
912            glyph_mapping = mapping[glyph] = []
913            for element in content:
914                if not isinstance(element, tuple):
915                    continue
916                element_name, element_attrs, _ = element
917                if element_name == "Substitute":
918                    glyph_mapping.append(element_attrs["value"])
919            return
920
921            # TTX v3.1 and later.
922        outGlyphs = attrs["out"].split(",") if attrs["out"] else []
923        mapping[attrs["in"]] = [g.strip() for g in outGlyphs]
924
925    @staticmethod
926    def makeSequence_(g):
927        seq = Sequence()
928        seq.Substitute = g
929        return seq
930
931
932class ClassDef(FormatSwitchingBaseTable):
933    def populateDefaults(self, propagator=None):
934        if not hasattr(self, "classDefs"):
935            self.classDefs = {}
936
937    def postRead(self, rawTable, font):
938        classDefs = {}
939
940        if self.Format == 1:
941            start = rawTable["StartGlyph"]
942            classList = rawTable["ClassValueArray"]
943            startID = font.getGlyphID(start)
944            endID = startID + len(classList)
945            glyphNames = font.getGlyphNameMany(range(startID, endID))
946            for glyphName, cls in zip(glyphNames, classList):
947                if cls:
948                    classDefs[glyphName] = cls
949
950        elif self.Format == 2:
951            records = rawTable["ClassRangeRecord"]
952            for rec in records:
953                cls = rec.Class
954                if not cls:
955                    continue
956                start = rec.Start
957                end = rec.End
958                startID = font.getGlyphID(start)
959                endID = font.getGlyphID(end) + 1
960                glyphNames = font.getGlyphNameMany(range(startID, endID))
961                for glyphName in glyphNames:
962                    classDefs[glyphName] = cls
963        else:
964            log.warning("Unknown ClassDef format: %s", self.Format)
965        self.classDefs = classDefs
966        del self.Format  # Don't need this anymore
967
968    def _getClassRanges(self, font):
969        classDefs = getattr(self, "classDefs", None)
970        if classDefs is None:
971            self.classDefs = {}
972            return
973        getGlyphID = font.getGlyphID
974        items = []
975        for glyphName, cls in classDefs.items():
976            if not cls:
977                continue
978            items.append((getGlyphID(glyphName), glyphName, cls))
979        if items:
980            items.sort()
981            last, lastName, lastCls = items[0]
982            ranges = [[lastCls, last, lastName]]
983            for glyphID, glyphName, cls in items[1:]:
984                if glyphID != last + 1 or cls != lastCls:
985                    ranges[-1].extend([last, lastName])
986                    ranges.append([cls, glyphID, glyphName])
987                last = glyphID
988                lastName = glyphName
989                lastCls = cls
990            ranges[-1].extend([last, lastName])
991            return ranges
992
993    def preWrite(self, font):
994        format = 2
995        rawTable = {"ClassRangeRecord": []}
996        ranges = self._getClassRanges(font)
997        if ranges:
998            startGlyph = ranges[0][1]
999            endGlyph = ranges[-1][3]
1000            glyphCount = endGlyph - startGlyph + 1
1001            if len(ranges) * 3 < glyphCount + 1:
1002                # Format 2 is more compact
1003                for i in range(len(ranges)):
1004                    cls, start, startName, end, endName = ranges[i]
1005                    rec = ClassRangeRecord()
1006                    rec.Start = startName
1007                    rec.End = endName
1008                    rec.Class = cls
1009                    ranges[i] = rec
1010                format = 2
1011                rawTable = {"ClassRangeRecord": ranges}
1012            else:
1013                # Format 1 is more compact
1014                startGlyphName = ranges[0][2]
1015                classes = [0] * glyphCount
1016                for cls, start, startName, end, endName in ranges:
1017                    for g in range(start - startGlyph, end - startGlyph + 1):
1018                        classes[g] = cls
1019                format = 1
1020                rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes}
1021        self.Format = format
1022        return rawTable
1023
1024    def toXML2(self, xmlWriter, font):
1025        items = sorted(self.classDefs.items())
1026        for glyphName, cls in items:
1027            xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)])
1028            xmlWriter.newline()
1029
1030    def fromXML(self, name, attrs, content, font):
1031        classDefs = getattr(self, "classDefs", None)
1032        if classDefs is None:
1033            classDefs = {}
1034            self.classDefs = classDefs
1035        classDefs[attrs["glyph"]] = int(attrs["class"])
1036
1037
1038class AlternateSubst(FormatSwitchingBaseTable):
1039    def populateDefaults(self, propagator=None):
1040        if not hasattr(self, "alternates"):
1041            self.alternates = {}
1042
1043    def postRead(self, rawTable, font):
1044        alternates = {}
1045        if self.Format == 1:
1046            input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1047            alts = rawTable["AlternateSet"]
1048            assert len(input) == len(alts)
1049            for inp, alt in zip(input, alts):
1050                alternates[inp] = alt.Alternate
1051        else:
1052            assert 0, "unknown format: %s" % self.Format
1053        self.alternates = alternates
1054        del self.Format  # Don't need this anymore
1055
1056    def preWrite(self, font):
1057        self.Format = 1
1058        alternates = getattr(self, "alternates", None)
1059        if alternates is None:
1060            alternates = self.alternates = {}
1061        items = list(alternates.items())
1062        for i in range(len(items)):
1063            glyphName, set = items[i]
1064            items[i] = font.getGlyphID(glyphName), glyphName, set
1065        items.sort()
1066        cov = Coverage()
1067        cov.glyphs = [item[1] for item in items]
1068        alternates = []
1069        setList = [item[-1] for item in items]
1070        for set in setList:
1071            alts = AlternateSet()
1072            alts.Alternate = set
1073            alternates.append(alts)
1074        # a special case to deal with the fact that several hundred Adobe Japan1-5
1075        # CJK fonts will overflow an offset if the coverage table isn't pushed to the end.
1076        # Also useful in that when splitting a sub-table because of an offset overflow
1077        # I don't need to calculate the change in the subtable offset due to the change in the coverage table size.
1078        # Allows packing more rules in subtable.
1079        self.sortCoverageLast = 1
1080        return {"Coverage": cov, "AlternateSet": alternates}
1081
1082    def toXML2(self, xmlWriter, font):
1083        items = sorted(self.alternates.items())
1084        for glyphName, alternates in items:
1085            xmlWriter.begintag("AlternateSet", glyph=glyphName)
1086            xmlWriter.newline()
1087            for alt in alternates:
1088                xmlWriter.simpletag("Alternate", glyph=alt)
1089                xmlWriter.newline()
1090            xmlWriter.endtag("AlternateSet")
1091            xmlWriter.newline()
1092
1093    def fromXML(self, name, attrs, content, font):
1094        alternates = getattr(self, "alternates", None)
1095        if alternates is None:
1096            alternates = {}
1097            self.alternates = alternates
1098        glyphName = attrs["glyph"]
1099        set = []
1100        alternates[glyphName] = set
1101        for element in content:
1102            if not isinstance(element, tuple):
1103                continue
1104            name, attrs, content = element
1105            set.append(attrs["glyph"])
1106
1107
1108class LigatureSubst(FormatSwitchingBaseTable):
1109    def populateDefaults(self, propagator=None):
1110        if not hasattr(self, "ligatures"):
1111            self.ligatures = {}
1112
1113    def postRead(self, rawTable, font):
1114        ligatures = {}
1115        if self.Format == 1:
1116            input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
1117            ligSets = rawTable["LigatureSet"]
1118            assert len(input) == len(ligSets)
1119            for i in range(len(input)):
1120                ligatures[input[i]] = ligSets[i].Ligature
1121        else:
1122            assert 0, "unknown format: %s" % self.Format
1123        self.ligatures = ligatures
1124        del self.Format  # Don't need this anymore
1125
1126    @staticmethod
1127    def _getLigatureSortKey(components):
1128        # Computes a key for ordering ligatures in a GSUB Type-4 lookup.
1129
1130        # When building the OpenType lookup, we need to make sure that
1131        # the longest sequence of components is listed first, so we
1132        # use the negative length as the key for sorting.
1133        # Note, we no longer need to worry about deterministic order because the
1134        # ligature mapping `dict` remembers the insertion order, and this in
1135        # turn depends on the order in which the ligatures are written in the FEA.
1136        # Since python sort algorithm is stable, the ligatures of equal length
1137        # will keep the relative order in which they appear in the feature file.
1138        # For example, given the following ligatures (all starting with 'f' and
1139        # thus belonging to the same LigatureSet):
1140        #
1141        #   feature liga {
1142        #     sub f i by f_i;
1143        #     sub f f f by f_f_f;
1144        #     sub f f by f_f;
1145        #     sub f f i by f_f_i;
1146        #   } liga;
1147        #
1148        # this should sort to: f_f_f, f_f_i, f_i, f_f
1149        # This is also what fea-rs does, see:
1150        # https://github.com/adobe-type-tools/afdko/issues/1727
1151        # https://github.com/fonttools/fonttools/issues/3428
1152        # https://github.com/googlefonts/fontc/pull/680
1153        return -len(components)
1154
1155    def preWrite(self, font):
1156        self.Format = 1
1157        ligatures = getattr(self, "ligatures", None)
1158        if ligatures is None:
1159            ligatures = self.ligatures = {}
1160
1161        if ligatures and isinstance(next(iter(ligatures)), tuple):
1162            # New high-level API in v3.1 and later.  Note that we just support compiling this
1163            # for now.  We don't load to this API, and don't do XML with it.
1164
1165            # ligatures is map from components-sequence to lig-glyph
1166            newLigatures = dict()
1167            for comps in sorted(ligatures.keys(), key=self._getLigatureSortKey):
1168                ligature = Ligature()
1169                ligature.Component = comps[1:]
1170                ligature.CompCount = len(comps)
1171                ligature.LigGlyph = ligatures[comps]
1172                newLigatures.setdefault(comps[0], []).append(ligature)
1173            ligatures = newLigatures
1174
1175        items = list(ligatures.items())
1176        for i in range(len(items)):
1177            glyphName, set = items[i]
1178            items[i] = font.getGlyphID(glyphName), glyphName, set
1179        items.sort()
1180        cov = Coverage()
1181        cov.glyphs = [item[1] for item in items]
1182
1183        ligSets = []
1184        setList = [item[-1] for item in items]
1185        for set in setList:
1186            ligSet = LigatureSet()
1187            ligs = ligSet.Ligature = []
1188            for lig in set:
1189                ligs.append(lig)
1190            ligSets.append(ligSet)
1191        # Useful in that when splitting a sub-table because of an offset overflow
1192        # I don't need to calculate the change in subtabl offset due to the coverage table size.
1193        # Allows packing more rules in subtable.
1194        self.sortCoverageLast = 1
1195        return {"Coverage": cov, "LigatureSet": ligSets}
1196
1197    def toXML2(self, xmlWriter, font):
1198        items = sorted(self.ligatures.items())
1199        for glyphName, ligSets in items:
1200            xmlWriter.begintag("LigatureSet", glyph=glyphName)
1201            xmlWriter.newline()
1202            for lig in ligSets:
1203                xmlWriter.simpletag(
1204                    "Ligature", glyph=lig.LigGlyph, components=",".join(lig.Component)
1205                )
1206                xmlWriter.newline()
1207            xmlWriter.endtag("LigatureSet")
1208            xmlWriter.newline()
1209
1210    def fromXML(self, name, attrs, content, font):
1211        ligatures = getattr(self, "ligatures", None)
1212        if ligatures is None:
1213            ligatures = {}
1214            self.ligatures = ligatures
1215        glyphName = attrs["glyph"]
1216        ligs = []
1217        ligatures[glyphName] = ligs
1218        for element in content:
1219            if not isinstance(element, tuple):
1220                continue
1221            name, attrs, content = element
1222            lig = Ligature()
1223            lig.LigGlyph = attrs["glyph"]
1224            components = attrs["components"]
1225            lig.Component = components.split(",") if components else []
1226            lig.CompCount = len(lig.Component)
1227            ligs.append(lig)
1228
1229
1230class COLR(BaseTable):
1231    def decompile(self, reader, font):
1232        # COLRv0 is exceptional in that LayerRecordCount appears *after* the
1233        # LayerRecordArray it counts, but the parser logic expects Count fields
1234        # to always precede the arrays. Here we work around this by parsing the
1235        # LayerRecordCount before the rest of the table, and storing it in
1236        # the reader's local state.
1237        subReader = reader.getSubReader(offset=0)
1238        for conv in self.getConverters():
1239            if conv.name != "LayerRecordCount":
1240                subReader.advance(conv.staticSize)
1241                continue
1242            reader[conv.name] = conv.read(subReader, font, tableDict={})
1243            break
1244        else:
1245            raise AssertionError("LayerRecordCount converter not found")
1246        return BaseTable.decompile(self, reader, font)
1247
1248    def preWrite(self, font):
1249        # The writer similarly assumes Count values precede the things counted,
1250        # thus here we pre-initialize a CountReference; the actual count value
1251        # will be set to the lenght of the array by the time this is assembled.
1252        self.LayerRecordCount = None
1253        return {
1254            **self.__dict__,
1255            "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount"),
1256        }
1257
1258    def computeClipBoxes(self, glyphSet: "_TTGlyphSet", quantization: int = 1):
1259        if self.Version == 0:
1260            return
1261
1262        clips = {}
1263        for rec in self.BaseGlyphList.BaseGlyphPaintRecord:
1264            try:
1265                clipBox = rec.Paint.computeClipBox(self, glyphSet, quantization)
1266            except Exception as e:
1267                from fontTools.ttLib import TTLibError
1268
1269                raise TTLibError(
1270                    f"Failed to compute COLR ClipBox for {rec.BaseGlyph!r}"
1271                ) from e
1272
1273            if clipBox is not None:
1274                clips[rec.BaseGlyph] = clipBox
1275
1276        hasClipList = hasattr(self, "ClipList") and self.ClipList is not None
1277        if not clips:
1278            if hasClipList:
1279                self.ClipList = None
1280        else:
1281            if not hasClipList:
1282                self.ClipList = ClipList()
1283                self.ClipList.Format = 1
1284            self.ClipList.clips = clips
1285
1286
1287class LookupList(BaseTable):
1288    @property
1289    def table(self):
1290        for l in self.Lookup:
1291            for st in l.SubTable:
1292                if type(st).__name__.endswith("Subst"):
1293                    return "GSUB"
1294                if type(st).__name__.endswith("Pos"):
1295                    return "GPOS"
1296        raise ValueError
1297
1298    def toXML2(self, xmlWriter, font):
1299        if (
1300            not font
1301            or "Debg" not in font
1302            or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data
1303        ):
1304            return super().toXML2(xmlWriter, font)
1305        debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table]
1306        for conv in self.getConverters():
1307            if conv.repeat:
1308                value = getattr(self, conv.name, [])
1309                for lookupIndex, item in enumerate(value):
1310                    if str(lookupIndex) in debugData:
1311                        info = LookupDebugInfo(*debugData[str(lookupIndex)])
1312                        tag = info.location
1313                        if info.name:
1314                            tag = f"{info.name}: {tag}"
1315                        if info.feature:
1316                            script, language, feature = info.feature
1317                            tag = f"{tag} in {feature} ({script}/{language})"
1318                        xmlWriter.comment(tag)
1319                        xmlWriter.newline()
1320
1321                    conv.xmlWrite(
1322                        xmlWriter, font, item, conv.name, [("index", lookupIndex)]
1323                    )
1324            else:
1325                if conv.aux and not eval(conv.aux, None, vars(self)):
1326                    continue
1327                value = getattr(
1328                    self, conv.name, None
1329                )  # TODO Handle defaults instead of defaulting to None!
1330                conv.xmlWrite(xmlWriter, font, value, conv.name, [])
1331
1332
1333class BaseGlyphRecordArray(BaseTable):
1334    def preWrite(self, font):
1335        self.BaseGlyphRecord = sorted(
1336            self.BaseGlyphRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
1337        )
1338        return self.__dict__.copy()
1339
1340
1341class BaseGlyphList(BaseTable):
1342    def preWrite(self, font):
1343        self.BaseGlyphPaintRecord = sorted(
1344            self.BaseGlyphPaintRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
1345        )
1346        return self.__dict__.copy()
1347
1348
1349class ClipBoxFormat(IntEnum):
1350    Static = 1
1351    Variable = 2
1352
1353    def is_variable(self):
1354        return self is self.Variable
1355
1356    def as_variable(self):
1357        return self.Variable
1358
1359
1360class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
1361    formatEnum = ClipBoxFormat
1362
1363    def as_tuple(self):
1364        return tuple(getattr(self, conv.name) for conv in self.getConverters())
1365
1366    def __repr__(self):
1367        return f"{self.__class__.__name__}{self.as_tuple()}"
1368
1369
1370class ClipList(getFormatSwitchingBaseTableClass("uint8")):
1371    def populateDefaults(self, propagator=None):
1372        if not hasattr(self, "clips"):
1373            self.clips = {}
1374
1375    def postRead(self, rawTable, font):
1376        clips = {}
1377        glyphOrder = font.getGlyphOrder()
1378        for i, rec in enumerate(rawTable["ClipRecord"]):
1379            if rec.StartGlyphID > rec.EndGlyphID:
1380                log.warning(
1381                    "invalid ClipRecord[%i].StartGlyphID (%i) > "
1382                    "EndGlyphID (%i); skipped",
1383                    i,
1384                    rec.StartGlyphID,
1385                    rec.EndGlyphID,
1386                )
1387                continue
1388            redefinedGlyphs = []
1389            missingGlyphs = []
1390            for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1):
1391                try:
1392                    glyph = glyphOrder[glyphID]
1393                except IndexError:
1394                    missingGlyphs.append(glyphID)
1395                    continue
1396                if glyph not in clips:
1397                    clips[glyph] = copy.copy(rec.ClipBox)
1398                else:
1399                    redefinedGlyphs.append(glyphID)
1400            if redefinedGlyphs:
1401                log.warning(
1402                    "ClipRecord[%i] overlaps previous records; "
1403                    "ignoring redefined clip boxes for the "
1404                    "following glyph ID range: [%i-%i]",
1405                    i,
1406                    min(redefinedGlyphs),
1407                    max(redefinedGlyphs),
1408                )
1409            if missingGlyphs:
1410                log.warning(
1411                    "ClipRecord[%i] range references missing " "glyph IDs: [%i-%i]",
1412                    i,
1413                    min(missingGlyphs),
1414                    max(missingGlyphs),
1415                )
1416        self.clips = clips
1417
1418    def groups(self):
1419        glyphsByClip = defaultdict(list)
1420        uniqueClips = {}
1421        for glyphName, clipBox in self.clips.items():
1422            key = clipBox.as_tuple()
1423            glyphsByClip[key].append(glyphName)
1424            if key not in uniqueClips:
1425                uniqueClips[key] = clipBox
1426        return {
1427            frozenset(glyphs): uniqueClips[key] for key, glyphs in glyphsByClip.items()
1428        }
1429
1430    def preWrite(self, font):
1431        if not hasattr(self, "clips"):
1432            self.clips = {}
1433        clipBoxRanges = {}
1434        glyphMap = font.getReverseGlyphMap()
1435        for glyphs, clipBox in self.groups().items():
1436            glyphIDs = sorted(
1437                glyphMap[glyphName] for glyphName in glyphs if glyphName in glyphMap
1438            )
1439            if not glyphIDs:
1440                continue
1441            last = glyphIDs[0]
1442            ranges = [[last]]
1443            for glyphID in glyphIDs[1:]:
1444                if glyphID != last + 1:
1445                    ranges[-1].append(last)
1446                    ranges.append([glyphID])
1447                last = glyphID
1448            ranges[-1].append(last)
1449            for start, end in ranges:
1450                assert (start, end) not in clipBoxRanges
1451                clipBoxRanges[(start, end)] = clipBox
1452
1453        clipRecords = []
1454        for (start, end), clipBox in sorted(clipBoxRanges.items()):
1455            record = ClipRecord()
1456            record.StartGlyphID = start
1457            record.EndGlyphID = end
1458            record.ClipBox = clipBox
1459            clipRecords.append(record)
1460        rawTable = {
1461            "ClipCount": len(clipRecords),
1462            "ClipRecord": clipRecords,
1463        }
1464        return rawTable
1465
1466    def toXML(self, xmlWriter, font, attrs=None, name=None):
1467        tableName = name if name else self.__class__.__name__
1468        if attrs is None:
1469            attrs = []
1470        if hasattr(self, "Format"):
1471            attrs.append(("Format", self.Format))
1472        xmlWriter.begintag(tableName, attrs)
1473        xmlWriter.newline()
1474        # sort clips alphabetically to ensure deterministic XML dump
1475        for glyphs, clipBox in sorted(
1476            self.groups().items(), key=lambda item: min(item[0])
1477        ):
1478            xmlWriter.begintag("Clip")
1479            xmlWriter.newline()
1480            for glyphName in sorted(glyphs):
1481                xmlWriter.simpletag("Glyph", value=glyphName)
1482                xmlWriter.newline()
1483            xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)])
1484            xmlWriter.newline()
1485            clipBox.toXML2(xmlWriter, font)
1486            xmlWriter.endtag("ClipBox")
1487            xmlWriter.newline()
1488            xmlWriter.endtag("Clip")
1489            xmlWriter.newline()
1490        xmlWriter.endtag(tableName)
1491        xmlWriter.newline()
1492
1493    def fromXML(self, name, attrs, content, font):
1494        clips = getattr(self, "clips", None)
1495        if clips is None:
1496            self.clips = clips = {}
1497        assert name == "Clip"
1498        glyphs = []
1499        clipBox = None
1500        for elem in content:
1501            if not isinstance(elem, tuple):
1502                continue
1503            name, attrs, content = elem
1504            if name == "Glyph":
1505                glyphs.append(attrs["value"])
1506            elif name == "ClipBox":
1507                clipBox = ClipBox()
1508                clipBox.Format = safeEval(attrs["Format"])
1509                for elem in content:
1510                    if not isinstance(elem, tuple):
1511                        continue
1512                    name, attrs, content = elem
1513                    clipBox.fromXML(name, attrs, content, font)
1514        if clipBox:
1515            for glyphName in glyphs:
1516                clips[glyphName] = clipBox
1517
1518
1519class ExtendMode(IntEnum):
1520    PAD = 0
1521    REPEAT = 1
1522    REFLECT = 2
1523
1524
1525# Porter-Duff modes for COLRv1 PaintComposite:
1526# https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration
1527class CompositeMode(IntEnum):
1528    CLEAR = 0
1529    SRC = 1
1530    DEST = 2
1531    SRC_OVER = 3
1532    DEST_OVER = 4
1533    SRC_IN = 5
1534    DEST_IN = 6
1535    SRC_OUT = 7
1536    DEST_OUT = 8
1537    SRC_ATOP = 9
1538    DEST_ATOP = 10
1539    XOR = 11
1540    PLUS = 12
1541    SCREEN = 13
1542    OVERLAY = 14
1543    DARKEN = 15
1544    LIGHTEN = 16
1545    COLOR_DODGE = 17
1546    COLOR_BURN = 18
1547    HARD_LIGHT = 19
1548    SOFT_LIGHT = 20
1549    DIFFERENCE = 21
1550    EXCLUSION = 22
1551    MULTIPLY = 23
1552    HSL_HUE = 24
1553    HSL_SATURATION = 25
1554    HSL_COLOR = 26
1555    HSL_LUMINOSITY = 27
1556
1557
1558class PaintFormat(IntEnum):
1559    PaintColrLayers = 1
1560    PaintSolid = 2
1561    PaintVarSolid = 3
1562    PaintLinearGradient = 4
1563    PaintVarLinearGradient = 5
1564    PaintRadialGradient = 6
1565    PaintVarRadialGradient = 7
1566    PaintSweepGradient = 8
1567    PaintVarSweepGradient = 9
1568    PaintGlyph = 10
1569    PaintColrGlyph = 11
1570    PaintTransform = 12
1571    PaintVarTransform = 13
1572    PaintTranslate = 14
1573    PaintVarTranslate = 15
1574    PaintScale = 16
1575    PaintVarScale = 17
1576    PaintScaleAroundCenter = 18
1577    PaintVarScaleAroundCenter = 19
1578    PaintScaleUniform = 20
1579    PaintVarScaleUniform = 21
1580    PaintScaleUniformAroundCenter = 22
1581    PaintVarScaleUniformAroundCenter = 23
1582    PaintRotate = 24
1583    PaintVarRotate = 25
1584    PaintRotateAroundCenter = 26
1585    PaintVarRotateAroundCenter = 27
1586    PaintSkew = 28
1587    PaintVarSkew = 29
1588    PaintSkewAroundCenter = 30
1589    PaintVarSkewAroundCenter = 31
1590    PaintComposite = 32
1591
1592    def is_variable(self):
1593        return self.name.startswith("PaintVar")
1594
1595    def as_variable(self):
1596        if self.is_variable():
1597            return self
1598        try:
1599            return PaintFormat.__members__[f"PaintVar{self.name[5:]}"]
1600        except KeyError:
1601            return None
1602
1603
1604class Paint(getFormatSwitchingBaseTableClass("uint8")):
1605    formatEnum = PaintFormat
1606
1607    def getFormatName(self):
1608        try:
1609            return self.formatEnum(self.Format).name
1610        except ValueError:
1611            raise NotImplementedError(f"Unknown Paint format: {self.Format}")
1612
1613    def toXML(self, xmlWriter, font, attrs=None, name=None):
1614        tableName = name if name else self.__class__.__name__
1615        if attrs is None:
1616            attrs = []
1617        attrs.append(("Format", self.Format))
1618        xmlWriter.begintag(tableName, attrs)
1619        xmlWriter.comment(self.getFormatName())
1620        xmlWriter.newline()
1621        self.toXML2(xmlWriter, font)
1622        xmlWriter.endtag(tableName)
1623        xmlWriter.newline()
1624
1625    def iterPaintSubTables(self, colr: COLR) -> Iterator[BaseTable.SubTableEntry]:
1626        if self.Format == PaintFormat.PaintColrLayers:
1627            # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists
1628            layers = []
1629            if colr.LayerList is not None:
1630                layers = colr.LayerList.Paint
1631            yield from (
1632                BaseTable.SubTableEntry(name="Layers", value=v, index=i)
1633                for i, v in enumerate(
1634                    layers[self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers]
1635                )
1636            )
1637            return
1638
1639        if self.Format == PaintFormat.PaintColrGlyph:
1640            for record in colr.BaseGlyphList.BaseGlyphPaintRecord:
1641                if record.BaseGlyph == self.Glyph:
1642                    yield BaseTable.SubTableEntry(name="BaseGlyph", value=record.Paint)
1643                    return
1644            else:
1645                raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList")
1646
1647        for conv in self.getConverters():
1648            if conv.tableClass is not None and issubclass(conv.tableClass, type(self)):
1649                value = getattr(self, conv.name)
1650                yield BaseTable.SubTableEntry(name=conv.name, value=value)
1651
1652    def getChildren(self, colr) -> List["Paint"]:
1653        # this is kept for backward compatibility (e.g. it's used by the subsetter)
1654        return [p.value for p in self.iterPaintSubTables(colr)]
1655
1656    def traverse(self, colr: COLR, callback):
1657        """Depth-first traversal of graph rooted at self, callback on each node."""
1658        if not callable(callback):
1659            raise TypeError("callback must be callable")
1660
1661        for path in dfs_base_table(
1662            self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
1663        ):
1664            paint = path[-1].value
1665            callback(paint)
1666
1667    def getTransform(self) -> Transform:
1668        if self.Format == PaintFormat.PaintTransform:
1669            t = self.Transform
1670            return Transform(t.xx, t.yx, t.xy, t.yy, t.dx, t.dy)
1671        elif self.Format == PaintFormat.PaintTranslate:
1672            return Identity.translate(self.dx, self.dy)
1673        elif self.Format == PaintFormat.PaintScale:
1674            return Identity.scale(self.scaleX, self.scaleY)
1675        elif self.Format == PaintFormat.PaintScaleAroundCenter:
1676            return (
1677                Identity.translate(self.centerX, self.centerY)
1678                .scale(self.scaleX, self.scaleY)
1679                .translate(-self.centerX, -self.centerY)
1680            )
1681        elif self.Format == PaintFormat.PaintScaleUniform:
1682            return Identity.scale(self.scale)
1683        elif self.Format == PaintFormat.PaintScaleUniformAroundCenter:
1684            return (
1685                Identity.translate(self.centerX, self.centerY)
1686                .scale(self.scale)
1687                .translate(-self.centerX, -self.centerY)
1688            )
1689        elif self.Format == PaintFormat.PaintRotate:
1690            return Identity.rotate(radians(self.angle))
1691        elif self.Format == PaintFormat.PaintRotateAroundCenter:
1692            return (
1693                Identity.translate(self.centerX, self.centerY)
1694                .rotate(radians(self.angle))
1695                .translate(-self.centerX, -self.centerY)
1696            )
1697        elif self.Format == PaintFormat.PaintSkew:
1698            return Identity.skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
1699        elif self.Format == PaintFormat.PaintSkewAroundCenter:
1700            return (
1701                Identity.translate(self.centerX, self.centerY)
1702                .skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
1703                .translate(-self.centerX, -self.centerY)
1704            )
1705        if PaintFormat(self.Format).is_variable():
1706            raise NotImplementedError(f"Variable Paints not supported: {self.Format}")
1707
1708        return Identity
1709
1710    def computeClipBox(
1711        self, colr: COLR, glyphSet: "_TTGlyphSet", quantization: int = 1
1712    ) -> Optional[ClipBox]:
1713        pen = ControlBoundsPen(glyphSet)
1714        for path in dfs_base_table(
1715            self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
1716        ):
1717            paint = path[-1].value
1718            if paint.Format == PaintFormat.PaintGlyph:
1719                transformation = reduce(
1720                    Transform.transform,
1721                    (st.value.getTransform() for st in path),
1722                    Identity,
1723                )
1724                glyphSet[paint.Glyph].draw(TransformPen(pen, transformation))
1725
1726        if pen.bounds is None:
1727            return None
1728
1729        cb = ClipBox()
1730        cb.Format = int(ClipBoxFormat.Static)
1731        cb.xMin, cb.yMin, cb.xMax, cb.yMax = quantizeRect(pen.bounds, quantization)
1732        return cb
1733
1734
1735# For each subtable format there is a class. However, we don't really distinguish
1736# between "field name" and "format name": often these are the same. Yet there's
1737# a whole bunch of fields with different names. The following dict is a mapping
1738# from "format name" to "field name". _buildClasses() uses this to create a
1739# subclass for each alternate field name.
1740#
1741_equivalents = {
1742    "MarkArray": ("Mark1Array",),
1743    "LangSys": ("DefaultLangSys",),
1744    "Coverage": (
1745        "MarkCoverage",
1746        "BaseCoverage",
1747        "LigatureCoverage",
1748        "Mark1Coverage",
1749        "Mark2Coverage",
1750        "BacktrackCoverage",
1751        "InputCoverage",
1752        "LookAheadCoverage",
1753        "VertGlyphCoverage",
1754        "HorizGlyphCoverage",
1755        "TopAccentCoverage",
1756        "ExtendedShapeCoverage",
1757        "MathKernCoverage",
1758    ),
1759    "ClassDef": (
1760        "ClassDef1",
1761        "ClassDef2",
1762        "BacktrackClassDef",
1763        "InputClassDef",
1764        "LookAheadClassDef",
1765        "GlyphClassDef",
1766        "MarkAttachClassDef",
1767    ),
1768    "Anchor": (
1769        "EntryAnchor",
1770        "ExitAnchor",
1771        "BaseAnchor",
1772        "LigatureAnchor",
1773        "Mark2Anchor",
1774        "MarkAnchor",
1775    ),
1776    "Device": (
1777        "XPlaDevice",
1778        "YPlaDevice",
1779        "XAdvDevice",
1780        "YAdvDevice",
1781        "XDeviceTable",
1782        "YDeviceTable",
1783        "DeviceTable",
1784    ),
1785    "Axis": (
1786        "HorizAxis",
1787        "VertAxis",
1788    ),
1789    "MinMax": ("DefaultMinMax",),
1790    "BaseCoord": (
1791        "MinCoord",
1792        "MaxCoord",
1793    ),
1794    "JstfLangSys": ("DefJstfLangSys",),
1795    "JstfGSUBModList": (
1796        "ShrinkageEnableGSUB",
1797        "ShrinkageDisableGSUB",
1798        "ExtensionEnableGSUB",
1799        "ExtensionDisableGSUB",
1800    ),
1801    "JstfGPOSModList": (
1802        "ShrinkageEnableGPOS",
1803        "ShrinkageDisableGPOS",
1804        "ExtensionEnableGPOS",
1805        "ExtensionDisableGPOS",
1806    ),
1807    "JstfMax": (
1808        "ShrinkageJstfMax",
1809        "ExtensionJstfMax",
1810    ),
1811    "MathKern": (
1812        "TopRightMathKern",
1813        "TopLeftMathKern",
1814        "BottomRightMathKern",
1815        "BottomLeftMathKern",
1816    ),
1817    "MathGlyphConstruction": ("VertGlyphConstruction", "HorizGlyphConstruction"),
1818}
1819
1820#
1821# OverFlow logic, to automatically create ExtensionLookups
1822# XXX This should probably move to otBase.py
1823#
1824
1825
1826def fixLookupOverFlows(ttf, overflowRecord):
1827    """Either the offset from the LookupList to a lookup overflowed, or
1828    an offset from a lookup to a subtable overflowed.
1829    The table layout is:
1830    GPSO/GUSB
1831            Script List
1832            Feature List
1833            LookUpList
1834                    Lookup[0] and contents
1835                            SubTable offset list
1836                                    SubTable[0] and contents
1837                                    ...
1838                                    SubTable[n] and contents
1839                    ...
1840                    Lookup[n] and contents
1841                            SubTable offset list
1842                                    SubTable[0] and contents
1843                                    ...
1844                                    SubTable[n] and contents
1845    If the offset to a lookup overflowed (SubTableIndex is None)
1846            we must promote the *previous*	lookup to an Extension type.
1847    If the offset from a lookup to subtable overflowed, then we must promote it
1848            to an Extension Lookup type.
1849    """
1850    ok = 0
1851    lookupIndex = overflowRecord.LookupListIndex
1852    if overflowRecord.SubTableIndex is None:
1853        lookupIndex = lookupIndex - 1
1854    if lookupIndex < 0:
1855        return ok
1856    if overflowRecord.tableType == "GSUB":
1857        extType = 7
1858    elif overflowRecord.tableType == "GPOS":
1859        extType = 9
1860
1861    lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup
1862    lookup = lookups[lookupIndex]
1863    # If the previous lookup is an extType, look further back. Very unlikely, but possible.
1864    while lookup.SubTable[0].__class__.LookupType == extType:
1865        lookupIndex = lookupIndex - 1
1866        if lookupIndex < 0:
1867            return ok
1868        lookup = lookups[lookupIndex]
1869
1870    for lookupIndex in range(lookupIndex, len(lookups)):
1871        lookup = lookups[lookupIndex]
1872        if lookup.LookupType != extType:
1873            lookup.LookupType = extType
1874            for si in range(len(lookup.SubTable)):
1875                subTable = lookup.SubTable[si]
1876                extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
1877                extSubTable = extSubTableClass()
1878                extSubTable.Format = 1
1879                extSubTable.ExtSubTable = subTable
1880                lookup.SubTable[si] = extSubTable
1881    ok = 1
1882    return ok
1883
1884
1885def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
1886    ok = 1
1887    oldMapping = sorted(oldSubTable.mapping.items())
1888    oldLen = len(oldMapping)
1889
1890    if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
1891        # Coverage table is written last. Overflow is to or within the
1892        # the coverage table. We will just cut the subtable in half.
1893        newLen = oldLen // 2
1894
1895    elif overflowRecord.itemName == "Sequence":
1896        # We just need to back up by two items from the overflowed
1897        # Sequence index to make sure the offset to the Coverage table
1898        # doesn't overflow.
1899        newLen = overflowRecord.itemIndex - 1
1900
1901    newSubTable.mapping = {}
1902    for i in range(newLen, oldLen):
1903        item = oldMapping[i]
1904        key = item[0]
1905        newSubTable.mapping[key] = item[1]
1906        del oldSubTable.mapping[key]
1907
1908    return ok
1909
1910
1911def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
1912    ok = 1
1913    if hasattr(oldSubTable, "sortCoverageLast"):
1914        newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast
1915
1916    oldAlts = sorted(oldSubTable.alternates.items())
1917    oldLen = len(oldAlts)
1918
1919    if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
1920        # Coverage table is written last. overflow is to or within the
1921        # the coverage table. We will just cut the subtable in half.
1922        newLen = oldLen // 2
1923
1924    elif overflowRecord.itemName == "AlternateSet":
1925        # We just need to back up by two items
1926        # from the overflowed AlternateSet index to make sure the offset
1927        # to the Coverage table doesn't overflow.
1928        newLen = overflowRecord.itemIndex - 1
1929
1930    newSubTable.alternates = {}
1931    for i in range(newLen, oldLen):
1932        item = oldAlts[i]
1933        key = item[0]
1934        newSubTable.alternates[key] = item[1]
1935        del oldSubTable.alternates[key]
1936
1937    return ok
1938
1939
1940def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord):
1941    ok = 1
1942    oldLigs = sorted(oldSubTable.ligatures.items())
1943    oldLen = len(oldLigs)
1944
1945    if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
1946        # Coverage table is written last. overflow is to or within the
1947        # the coverage table. We will just cut the subtable in half.
1948        newLen = oldLen // 2
1949
1950    elif overflowRecord.itemName == "LigatureSet":
1951        # We just need to back up by two items
1952        # from the overflowed AlternateSet index to make sure the offset
1953        # to the Coverage table doesn't overflow.
1954        newLen = overflowRecord.itemIndex - 1
1955
1956    newSubTable.ligatures = {}
1957    for i in range(newLen, oldLen):
1958        item = oldLigs[i]
1959        key = item[0]
1960        newSubTable.ligatures[key] = item[1]
1961        del oldSubTable.ligatures[key]
1962
1963    return ok
1964
1965
1966def splitPairPos(oldSubTable, newSubTable, overflowRecord):
1967    st = oldSubTable
1968    ok = False
1969    newSubTable.Format = oldSubTable.Format
1970    if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1:
1971        for name in "ValueFormat1", "ValueFormat2":
1972            setattr(newSubTable, name, getattr(oldSubTable, name))
1973
1974        # Move top half of coverage to new subtable
1975
1976        newSubTable.Coverage = oldSubTable.Coverage.__class__()
1977
1978        coverage = oldSubTable.Coverage.glyphs
1979        records = oldSubTable.PairSet
1980
1981        oldCount = len(oldSubTable.PairSet) // 2
1982
1983        oldSubTable.Coverage.glyphs = coverage[:oldCount]
1984        oldSubTable.PairSet = records[:oldCount]
1985
1986        newSubTable.Coverage.glyphs = coverage[oldCount:]
1987        newSubTable.PairSet = records[oldCount:]
1988
1989        oldSubTable.PairSetCount = len(oldSubTable.PairSet)
1990        newSubTable.PairSetCount = len(newSubTable.PairSet)
1991
1992        ok = True
1993
1994    elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1:
1995        if not hasattr(oldSubTable, "Class2Count"):
1996            oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record)
1997        for name in "Class2Count", "ClassDef2", "ValueFormat1", "ValueFormat2":
1998            setattr(newSubTable, name, getattr(oldSubTable, name))
1999
2000        # The two subtables will still have the same ClassDef2 and the table
2001        # sharing will still cause the sharing to overflow.  As such, disable
2002        # sharing on the one that is serialized second (that's oldSubTable).
2003        oldSubTable.DontShare = True
2004
2005        # Move top half of class numbers to new subtable
2006
2007        newSubTable.Coverage = oldSubTable.Coverage.__class__()
2008        newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__()
2009
2010        coverage = oldSubTable.Coverage.glyphs
2011        classDefs = oldSubTable.ClassDef1.classDefs
2012        records = oldSubTable.Class1Record
2013
2014        oldCount = len(oldSubTable.Class1Record) // 2
2015        newGlyphs = set(k for k, v in classDefs.items() if v >= oldCount)
2016
2017        oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs]
2018        oldSubTable.ClassDef1.classDefs = {
2019            k: v for k, v in classDefs.items() if v < oldCount
2020        }
2021        oldSubTable.Class1Record = records[:oldCount]
2022
2023        newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs]
2024        newSubTable.ClassDef1.classDefs = {
2025            k: (v - oldCount) for k, v in classDefs.items() if v > oldCount
2026        }
2027        newSubTable.Class1Record = records[oldCount:]
2028
2029        oldSubTable.Class1Count = len(oldSubTable.Class1Record)
2030        newSubTable.Class1Count = len(newSubTable.Class1Record)
2031
2032        ok = True
2033
2034    return ok
2035
2036
2037def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord):
2038    # split half of the mark classes to the new subtable
2039    classCount = oldSubTable.ClassCount
2040    if classCount < 2:
2041        # oh well, not much left to split...
2042        return False
2043
2044    oldClassCount = classCount // 2
2045    newClassCount = classCount - oldClassCount
2046
2047    oldMarkCoverage, oldMarkRecords = [], []
2048    newMarkCoverage, newMarkRecords = [], []
2049    for glyphName, markRecord in zip(
2050        oldSubTable.MarkCoverage.glyphs, oldSubTable.MarkArray.MarkRecord
2051    ):
2052        if markRecord.Class < oldClassCount:
2053            oldMarkCoverage.append(glyphName)
2054            oldMarkRecords.append(markRecord)
2055        else:
2056            markRecord.Class -= oldClassCount
2057            newMarkCoverage.append(glyphName)
2058            newMarkRecords.append(markRecord)
2059
2060    oldBaseRecords, newBaseRecords = [], []
2061    for rec in oldSubTable.BaseArray.BaseRecord:
2062        oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__()
2063        oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount]
2064        newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:]
2065        oldBaseRecords.append(oldBaseRecord)
2066        newBaseRecords.append(newBaseRecord)
2067
2068    newSubTable.Format = oldSubTable.Format
2069
2070    oldSubTable.MarkCoverage.glyphs = oldMarkCoverage
2071    newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__()
2072    newSubTable.MarkCoverage.glyphs = newMarkCoverage
2073
2074    # share the same BaseCoverage in both halves
2075    newSubTable.BaseCoverage = oldSubTable.BaseCoverage
2076
2077    oldSubTable.ClassCount = oldClassCount
2078    newSubTable.ClassCount = newClassCount
2079
2080    oldSubTable.MarkArray.MarkRecord = oldMarkRecords
2081    newSubTable.MarkArray = oldSubTable.MarkArray.__class__()
2082    newSubTable.MarkArray.MarkRecord = newMarkRecords
2083
2084    oldSubTable.MarkArray.MarkCount = len(oldMarkRecords)
2085    newSubTable.MarkArray.MarkCount = len(newMarkRecords)
2086
2087    oldSubTable.BaseArray.BaseRecord = oldBaseRecords
2088    newSubTable.BaseArray = oldSubTable.BaseArray.__class__()
2089    newSubTable.BaseArray.BaseRecord = newBaseRecords
2090
2091    oldSubTable.BaseArray.BaseCount = len(oldBaseRecords)
2092    newSubTable.BaseArray.BaseCount = len(newBaseRecords)
2093
2094    return True
2095
2096
2097splitTable = {
2098    "GSUB": {
2099        # 					1: splitSingleSubst,
2100        2: splitMultipleSubst,
2101        3: splitAlternateSubst,
2102        4: splitLigatureSubst,
2103        # 					5: splitContextSubst,
2104        # 					6: splitChainContextSubst,
2105        # 					7: splitExtensionSubst,
2106        # 					8: splitReverseChainSingleSubst,
2107    },
2108    "GPOS": {
2109        # 					1: splitSinglePos,
2110        2: splitPairPos,
2111        # 					3: splitCursivePos,
2112        4: splitMarkBasePos,
2113        # 					5: splitMarkLigPos,
2114        # 					6: splitMarkMarkPos,
2115        # 					7: splitContextPos,
2116        # 					8: splitChainContextPos,
2117        # 					9: splitExtensionPos,
2118    },
2119}
2120
2121
2122def fixSubTableOverFlows(ttf, overflowRecord):
2123    """
2124    An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts.
2125    """
2126    table = ttf[overflowRecord.tableType].table
2127    lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex]
2128    subIndex = overflowRecord.SubTableIndex
2129    subtable = lookup.SubTable[subIndex]
2130
2131    # First, try not sharing anything for this subtable...
2132    if not hasattr(subtable, "DontShare"):
2133        subtable.DontShare = True
2134        return True
2135
2136    if hasattr(subtable, "ExtSubTable"):
2137        # We split the subtable of the Extension table, and add a new Extension table
2138        # to contain the new subtable.
2139
2140        subTableType = subtable.ExtSubTable.__class__.LookupType
2141        extSubTable = subtable
2142        subtable = extSubTable.ExtSubTable
2143        newExtSubTableClass = lookupTypes[overflowRecord.tableType][
2144            extSubTable.__class__.LookupType
2145        ]
2146        newExtSubTable = newExtSubTableClass()
2147        newExtSubTable.Format = extSubTable.Format
2148        toInsert = newExtSubTable
2149
2150        newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
2151        newSubTable = newSubTableClass()
2152        newExtSubTable.ExtSubTable = newSubTable
2153    else:
2154        subTableType = subtable.__class__.LookupType
2155        newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
2156        newSubTable = newSubTableClass()
2157        toInsert = newSubTable
2158
2159    if hasattr(lookup, "SubTableCount"):  # may not be defined yet.
2160        lookup.SubTableCount = lookup.SubTableCount + 1
2161
2162    try:
2163        splitFunc = splitTable[overflowRecord.tableType][subTableType]
2164    except KeyError:
2165        log.error(
2166            "Don't know how to split %s lookup type %s",
2167            overflowRecord.tableType,
2168            subTableType,
2169        )
2170        return False
2171
2172    ok = splitFunc(subtable, newSubTable, overflowRecord)
2173    if ok:
2174        lookup.SubTable.insert(subIndex + 1, toInsert)
2175    return ok
2176
2177
2178# End of OverFlow logic
2179
2180
2181def _buildClasses():
2182    import re
2183    from .otData import otData
2184
2185    formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$")
2186    namespace = globals()
2187
2188    # populate module with classes
2189    for name, table in otData:
2190        baseClass = BaseTable
2191        m = formatPat.match(name)
2192        if m:
2193            # XxxFormatN subtable, we only add the "base" table
2194            name = m.group(1)
2195            # the first row of a format-switching otData table describes the Format;
2196            # the first column defines the type of the Format field.
2197            # Currently this can be either 'uint16' or 'uint8'.
2198            formatType = table[0][0]
2199            baseClass = getFormatSwitchingBaseTableClass(formatType)
2200        if name not in namespace:
2201            # the class doesn't exist yet, so the base implementation is used.
2202            cls = type(name, (baseClass,), {})
2203            if name in ("GSUB", "GPOS"):
2204                cls.DontShare = True
2205            namespace[name] = cls
2206
2207    # link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.)
2208    for name, _ in otData:
2209        if name.startswith("Var") and len(name) > 3 and name[3:] in namespace:
2210            varType = namespace[name]
2211            noVarType = namespace[name[3:]]
2212            varType.NoVarType = noVarType
2213            noVarType.VarType = varType
2214
2215    for base, alts in _equivalents.items():
2216        base = namespace[base]
2217        for alt in alts:
2218            namespace[alt] = base
2219
2220    global lookupTypes
2221    lookupTypes = {
2222        "GSUB": {
2223            1: SingleSubst,
2224            2: MultipleSubst,
2225            3: AlternateSubst,
2226            4: LigatureSubst,
2227            5: ContextSubst,
2228            6: ChainContextSubst,
2229            7: ExtensionSubst,
2230            8: ReverseChainSingleSubst,
2231        },
2232        "GPOS": {
2233            1: SinglePos,
2234            2: PairPos,
2235            3: CursivePos,
2236            4: MarkBasePos,
2237            5: MarkLigPos,
2238            6: MarkMarkPos,
2239            7: ContextPos,
2240            8: ChainContextPos,
2241            9: ExtensionPos,
2242        },
2243        "mort": {
2244            4: NoncontextualMorph,
2245        },
2246        "morx": {
2247            0: RearrangementMorph,
2248            1: ContextualMorph,
2249            2: LigatureMorph,
2250            # 3: Reserved,
2251            4: NoncontextualMorph,
2252            5: InsertionMorph,
2253        },
2254    }
2255    lookupTypes["JSTF"] = lookupTypes["GPOS"]  # JSTF contains GPOS
2256    for lookupEnum in lookupTypes.values():
2257        for enum, cls in lookupEnum.items():
2258            cls.LookupType = enum
2259
2260    global featureParamTypes
2261    featureParamTypes = {
2262        "size": FeatureParamsSize,
2263    }
2264    for i in range(1, 20 + 1):
2265        featureParamTypes["ss%02d" % i] = FeatureParamsStylisticSet
2266    for i in range(1, 99 + 1):
2267        featureParamTypes["cv%02d" % i] = FeatureParamsCharacterVariants
2268
2269    # add converters to classes
2270    from .otConverters import buildConverters
2271
2272    for name, table in otData:
2273        m = formatPat.match(name)
2274        if m:
2275            # XxxFormatN subtable, add converter to "base" table
2276            name, format = m.groups()
2277            format = int(format)
2278            cls = namespace[name]
2279            if not hasattr(cls, "converters"):
2280                cls.converters = {}
2281                cls.convertersByName = {}
2282            converters, convertersByName = buildConverters(table[1:], namespace)
2283            cls.converters[format] = converters
2284            cls.convertersByName[format] = convertersByName
2285            # XXX Add staticSize?
2286        else:
2287            cls = namespace[name]
2288            cls.converters, cls.convertersByName = buildConverters(table, namespace)
2289            # XXX Add staticSize?
2290
2291
2292_buildClasses()
2293
2294
2295def _getGlyphsFromCoverageTable(coverage):
2296    if coverage is None:
2297        # empty coverage table
2298        return []
2299    else:
2300        return coverage.glyphs
2301