xref: /aosp_15_r20/external/fonttools/Lib/fontTools/otlLib/builder.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from collections import namedtuple, OrderedDict
2import os
3from fontTools.misc.fixedTools import fixedToFloat
4from fontTools.misc.roundTools import otRound
5from fontTools import ttLib
6from fontTools.ttLib.tables import otTables as ot
7from fontTools.ttLib.tables.otBase import (
8    ValueRecord,
9    valueRecordFormatDict,
10    OTLOffsetOverflowError,
11    OTTableWriter,
12    CountReference,
13)
14from fontTools.ttLib.tables import otBase
15from fontTools.feaLib.ast import STATNameStatement
16from fontTools.otlLib.optimize.gpos import (
17    _compression_level_from_env,
18    compact_lookup,
19)
20from fontTools.otlLib.error import OpenTypeLibError
21from functools import reduce
22import logging
23import copy
24
25
26log = logging.getLogger(__name__)
27
28
29def buildCoverage(glyphs, glyphMap):
30    """Builds a coverage table.
31
32    Coverage tables (as defined in the `OpenType spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#coverage-table>`__)
33    are used in all OpenType Layout lookups apart from the Extension type, and
34    define the glyphs involved in a layout subtable. This allows shaping engines
35    to compare the glyph stream with the coverage table and quickly determine
36    whether a subtable should be involved in a shaping operation.
37
38    This function takes a list of glyphs and a glyphname-to-ID map, and
39    returns a ``Coverage`` object representing the coverage table.
40
41    Example::
42
43        glyphMap = font.getReverseGlyphMap()
44        glyphs = [ "A", "B", "C" ]
45        coverage = buildCoverage(glyphs, glyphMap)
46
47    Args:
48        glyphs: a sequence of glyph names.
49        glyphMap: a glyph name to ID map, typically returned from
50            ``font.getReverseGlyphMap()``.
51
52    Returns:
53        An ``otTables.Coverage`` object or ``None`` if there are no glyphs
54        supplied.
55    """
56
57    if not glyphs:
58        return None
59    self = ot.Coverage()
60    try:
61        self.glyphs = sorted(set(glyphs), key=glyphMap.__getitem__)
62    except KeyError as e:
63        raise ValueError(f"Could not find glyph {e} in font") from e
64
65    return self
66
67
68LOOKUP_FLAG_RIGHT_TO_LEFT = 0x0001
69LOOKUP_FLAG_IGNORE_BASE_GLYPHS = 0x0002
70LOOKUP_FLAG_IGNORE_LIGATURES = 0x0004
71LOOKUP_FLAG_IGNORE_MARKS = 0x0008
72LOOKUP_FLAG_USE_MARK_FILTERING_SET = 0x0010
73
74
75def buildLookup(subtables, flags=0, markFilterSet=None):
76    """Turns a collection of rules into a lookup.
77
78    A Lookup (as defined in the `OpenType Spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#lookupTbl>`__)
79    wraps the individual rules in a layout operation (substitution or
80    positioning) in a data structure expressing their overall lookup type -
81    for example, single substitution, mark-to-base attachment, and so on -
82    as well as the lookup flags and any mark filtering sets. You may import
83    the following constants to express lookup flags:
84
85    - ``LOOKUP_FLAG_RIGHT_TO_LEFT``
86    - ``LOOKUP_FLAG_IGNORE_BASE_GLYPHS``
87    - ``LOOKUP_FLAG_IGNORE_LIGATURES``
88    - ``LOOKUP_FLAG_IGNORE_MARKS``
89    - ``LOOKUP_FLAG_USE_MARK_FILTERING_SET``
90
91    Args:
92        subtables: A list of layout subtable objects (e.g.
93            ``MultipleSubst``, ``PairPos``, etc.) or ``None``.
94        flags (int): This lookup's flags.
95        markFilterSet: Either ``None`` if no mark filtering set is used, or
96            an integer representing the filtering set to be used for this
97            lookup. If a mark filtering set is provided,
98            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
99            flags.
100
101    Returns:
102        An ``otTables.Lookup`` object or ``None`` if there are no subtables
103        supplied.
104    """
105    if subtables is None:
106        return None
107    subtables = [st for st in subtables if st is not None]
108    if not subtables:
109        return None
110    assert all(
111        t.LookupType == subtables[0].LookupType for t in subtables
112    ), "all subtables must have the same LookupType; got %s" % repr(
113        [t.LookupType for t in subtables]
114    )
115    self = ot.Lookup()
116    self.LookupType = subtables[0].LookupType
117    self.LookupFlag = flags
118    self.SubTable = subtables
119    self.SubTableCount = len(self.SubTable)
120    if markFilterSet is not None:
121        self.LookupFlag |= LOOKUP_FLAG_USE_MARK_FILTERING_SET
122        assert isinstance(markFilterSet, int), markFilterSet
123        self.MarkFilteringSet = markFilterSet
124    else:
125        assert (self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET) == 0, (
126            "if markFilterSet is None, flags must not set "
127            "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags
128        )
129    return self
130
131
132class LookupBuilder(object):
133    SUBTABLE_BREAK_ = "SUBTABLE_BREAK"
134
135    def __init__(self, font, location, table, lookup_type):
136        self.font = font
137        self.glyphMap = font.getReverseGlyphMap()
138        self.location = location
139        self.table, self.lookup_type = table, lookup_type
140        self.lookupflag = 0
141        self.markFilterSet = None
142        self.lookup_index = None  # assigned when making final tables
143        assert table in ("GPOS", "GSUB")
144
145    def equals(self, other):
146        return (
147            isinstance(other, self.__class__)
148            and self.table == other.table
149            and self.lookupflag == other.lookupflag
150            and self.markFilterSet == other.markFilterSet
151        )
152
153    def inferGlyphClasses(self):
154        """Infers glyph glasses for the GDEF table, such as {"cedilla":3}."""
155        return {}
156
157    def getAlternateGlyphs(self):
158        """Helper for building 'aalt' features."""
159        return {}
160
161    def buildLookup_(self, subtables):
162        return buildLookup(subtables, self.lookupflag, self.markFilterSet)
163
164    def buildMarkClasses_(self, marks):
165        """{"cedilla": ("BOTTOM", ast.Anchor), ...} --> {"BOTTOM":0, "TOP":1}
166
167        Helper for MarkBasePostBuilder, MarkLigPosBuilder, and
168        MarkMarkPosBuilder. Seems to return the same numeric IDs
169        for mark classes as the AFDKO makeotf tool.
170        """
171        ids = {}
172        for mark in sorted(marks.keys(), key=self.font.getGlyphID):
173            markClassName, _markAnchor = marks[mark]
174            if markClassName not in ids:
175                ids[markClassName] = len(ids)
176        return ids
177
178    def setBacktrackCoverage_(self, prefix, subtable):
179        subtable.BacktrackGlyphCount = len(prefix)
180        subtable.BacktrackCoverage = []
181        for p in reversed(prefix):
182            coverage = buildCoverage(p, self.glyphMap)
183            subtable.BacktrackCoverage.append(coverage)
184
185    def setLookAheadCoverage_(self, suffix, subtable):
186        subtable.LookAheadGlyphCount = len(suffix)
187        subtable.LookAheadCoverage = []
188        for s in suffix:
189            coverage = buildCoverage(s, self.glyphMap)
190            subtable.LookAheadCoverage.append(coverage)
191
192    def setInputCoverage_(self, glyphs, subtable):
193        subtable.InputGlyphCount = len(glyphs)
194        subtable.InputCoverage = []
195        for g in glyphs:
196            coverage = buildCoverage(g, self.glyphMap)
197            subtable.InputCoverage.append(coverage)
198
199    def setCoverage_(self, glyphs, subtable):
200        subtable.GlyphCount = len(glyphs)
201        subtable.Coverage = []
202        for g in glyphs:
203            coverage = buildCoverage(g, self.glyphMap)
204            subtable.Coverage.append(coverage)
205
206    def build_subst_subtables(self, mapping, klass):
207        substitutions = [{}]
208        for key in mapping:
209            if key[0] == self.SUBTABLE_BREAK_:
210                substitutions.append({})
211            else:
212                substitutions[-1][key] = mapping[key]
213        subtables = [klass(s) for s in substitutions]
214        return subtables
215
216    def add_subtable_break(self, location):
217        """Add an explicit subtable break.
218
219        Args:
220            location: A string or tuple representing the location in the
221                original source which produced this break, or ``None`` if
222                no location is provided.
223        """
224        log.warning(
225            OpenTypeLibError(
226                'unsupported "subtable" statement for lookup type', location
227            )
228        )
229
230
231class AlternateSubstBuilder(LookupBuilder):
232    """Builds an Alternate Substitution (GSUB3) lookup.
233
234    Users are expected to manually add alternate glyph substitutions to
235    the ``alternates`` attribute after the object has been initialized,
236    e.g.::
237
238        builder.alternates["A"] = ["A.alt1", "A.alt2"]
239
240    Attributes:
241        font (``fontTools.TTLib.TTFont``): A font object.
242        location: A string or tuple representing the location in the original
243            source which produced this lookup.
244        alternates: An ordered dictionary of alternates, mapping glyph names
245            to a list of names of alternates.
246        lookupflag (int): The lookup's flag
247        markFilterSet: Either ``None`` if no mark filtering set is used, or
248            an integer representing the filtering set to be used for this
249            lookup. If a mark filtering set is provided,
250            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
251            flags.
252    """
253
254    def __init__(self, font, location):
255        LookupBuilder.__init__(self, font, location, "GSUB", 3)
256        self.alternates = OrderedDict()
257
258    def equals(self, other):
259        return LookupBuilder.equals(self, other) and self.alternates == other.alternates
260
261    def build(self):
262        """Build the lookup.
263
264        Returns:
265            An ``otTables.Lookup`` object representing the alternate
266            substitution lookup.
267        """
268        subtables = self.build_subst_subtables(
269            self.alternates, buildAlternateSubstSubtable
270        )
271        return self.buildLookup_(subtables)
272
273    def getAlternateGlyphs(self):
274        return self.alternates
275
276    def add_subtable_break(self, location):
277        self.alternates[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
278
279
280class ChainContextualRule(
281    namedtuple("ChainContextualRule", ["prefix", "glyphs", "suffix", "lookups"])
282):
283    @property
284    def is_subtable_break(self):
285        return self.prefix == LookupBuilder.SUBTABLE_BREAK_
286
287
288class ChainContextualRuleset:
289    def __init__(self):
290        self.rules = []
291
292    def addRule(self, rule):
293        self.rules.append(rule)
294
295    @property
296    def hasPrefixOrSuffix(self):
297        # Do we have any prefixes/suffixes? If this is False for all
298        # rulesets, we can express the whole lookup as GPOS5/GSUB7.
299        for rule in self.rules:
300            if len(rule.prefix) > 0 or len(rule.suffix) > 0:
301                return True
302        return False
303
304    @property
305    def hasAnyGlyphClasses(self):
306        # Do we use glyph classes anywhere in the rules? If this is False
307        # we can express this subtable as a Format 1.
308        for rule in self.rules:
309            for coverage in (rule.prefix, rule.glyphs, rule.suffix):
310                if any(len(x) > 1 for x in coverage):
311                    return True
312        return False
313
314    def format2ClassDefs(self):
315        PREFIX, GLYPHS, SUFFIX = 0, 1, 2
316        classDefBuilders = []
317        for ix in [PREFIX, GLYPHS, SUFFIX]:
318            context = []
319            for r in self.rules:
320                context.append(r[ix])
321            classes = self._classBuilderForContext(context)
322            if not classes:
323                return None
324            classDefBuilders.append(classes)
325        return classDefBuilders
326
327    def _classBuilderForContext(self, context):
328        classdefbuilder = ClassDefBuilder(useClass0=False)
329        for position in context:
330            for glyphset in position:
331                glyphs = set(glyphset)
332                if not classdefbuilder.canAdd(glyphs):
333                    return None
334                classdefbuilder.add(glyphs)
335        return classdefbuilder
336
337
338class ChainContextualBuilder(LookupBuilder):
339    def equals(self, other):
340        return LookupBuilder.equals(self, other) and self.rules == other.rules
341
342    def rulesets(self):
343        # Return a list of ChainContextRuleset objects, taking explicit
344        # subtable breaks into account
345        ruleset = [ChainContextualRuleset()]
346        for rule in self.rules:
347            if rule.is_subtable_break:
348                ruleset.append(ChainContextualRuleset())
349                continue
350            ruleset[-1].addRule(rule)
351        # Squish any empty subtables
352        return [x for x in ruleset if len(x.rules) > 0]
353
354    def getCompiledSize_(self, subtables):
355        if not subtables:
356            return 0
357        # We need to make a copy here because compiling
358        # modifies the subtable (finalizing formats etc.)
359        table = self.buildLookup_(copy.deepcopy(subtables))
360        w = OTTableWriter()
361        table.compile(w, self.font)
362        size = len(w.getAllData())
363        return size
364
365    def build(self):
366        """Build the lookup.
367
368        Returns:
369            An ``otTables.Lookup`` object representing the chained
370            contextual positioning lookup.
371        """
372        subtables = []
373
374        rulesets = self.rulesets()
375        chaining = any(ruleset.hasPrefixOrSuffix for ruleset in rulesets)
376
377        # https://github.com/fonttools/fonttools/issues/2539
378        #
379        # Unfortunately, as of 2022-03-07, Apple's CoreText renderer does not
380        # correctly process GPOS7 lookups, so for now we force contextual
381        # positioning lookups to be chaining (GPOS8).
382        #
383        # This seems to be fixed as of macOS 13.2, but we keep disabling this
384        # for now until we are no longer concerned about old macOS versions.
385        # But we allow people to opt-out of this with the config key below.
386        write_gpos7 = self.font.cfg.get("fontTools.otlLib.builder:WRITE_GPOS7")
387        # horrible separation of concerns breach
388        if not write_gpos7 and self.subtable_type == "Pos":
389            chaining = True
390
391        for ruleset in rulesets:
392            # Determine format strategy. We try to build formats 1, 2 and 3
393            # subtables and then work out which is best. candidates list holds
394            # the subtables in each format for this ruleset (including a dummy
395            # "format 0" to make the addressing match the format numbers).
396
397            # We can always build a format 3 lookup by accumulating each of
398            # the rules into a list, so start with that.
399            candidates = [None, None, None, []]
400            for rule in ruleset.rules:
401                candidates[3].append(self.buildFormat3Subtable(rule, chaining))
402
403            # Can we express the whole ruleset as a format 2 subtable?
404            classdefs = ruleset.format2ClassDefs()
405            if classdefs:
406                candidates[2] = [
407                    self.buildFormat2Subtable(ruleset, classdefs, chaining)
408                ]
409
410            if not ruleset.hasAnyGlyphClasses:
411                candidates[1] = [self.buildFormat1Subtable(ruleset, chaining)]
412
413            candidates_by_size = []
414            for i in [1, 2, 3]:
415                if candidates[i]:
416                    try:
417                        size = self.getCompiledSize_(candidates[i])
418                    except OTLOffsetOverflowError as e:
419                        log.warning(
420                            "Contextual format %i at %s overflowed (%s)"
421                            % (i, str(self.location), e)
422                        )
423                    else:
424                        candidates_by_size.append((size, candidates[i]))
425
426            if not candidates_by_size:
427                raise OpenTypeLibError("All candidates overflowed", self.location)
428
429            _min_size, winner = min(candidates_by_size, key=lambda x: x[0])
430            subtables.extend(winner)
431
432        # If we are not chaining, lookup type will be automatically fixed by
433        # buildLookup_
434        return self.buildLookup_(subtables)
435
436    def buildFormat1Subtable(self, ruleset, chaining=True):
437        st = self.newSubtable_(chaining=chaining)
438        st.Format = 1
439        st.populateDefaults()
440        coverage = set()
441        rulesetsByFirstGlyph = {}
442        ruleAttr = self.ruleAttr_(format=1, chaining=chaining)
443
444        for rule in ruleset.rules:
445            ruleAsSubtable = self.newRule_(format=1, chaining=chaining)
446
447            if chaining:
448                ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix)
449                ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix)
450                ruleAsSubtable.Backtrack = [list(x)[0] for x in reversed(rule.prefix)]
451                ruleAsSubtable.LookAhead = [list(x)[0] for x in rule.suffix]
452
453                ruleAsSubtable.InputGlyphCount = len(rule.glyphs)
454            else:
455                ruleAsSubtable.GlyphCount = len(rule.glyphs)
456
457            ruleAsSubtable.Input = [list(x)[0] for x in rule.glyphs[1:]]
458
459            self.buildLookupList(rule, ruleAsSubtable)
460
461            firstGlyph = list(rule.glyphs[0])[0]
462            if firstGlyph not in rulesetsByFirstGlyph:
463                coverage.add(firstGlyph)
464                rulesetsByFirstGlyph[firstGlyph] = []
465            rulesetsByFirstGlyph[firstGlyph].append(ruleAsSubtable)
466
467        st.Coverage = buildCoverage(coverage, self.glyphMap)
468        ruleSets = []
469        for g in st.Coverage.glyphs:
470            ruleSet = self.newRuleSet_(format=1, chaining=chaining)
471            setattr(ruleSet, ruleAttr, rulesetsByFirstGlyph[g])
472            setattr(ruleSet, f"{ruleAttr}Count", len(rulesetsByFirstGlyph[g]))
473            ruleSets.append(ruleSet)
474
475        setattr(st, self.ruleSetAttr_(format=1, chaining=chaining), ruleSets)
476        setattr(
477            st, self.ruleSetAttr_(format=1, chaining=chaining) + "Count", len(ruleSets)
478        )
479
480        return st
481
482    def buildFormat2Subtable(self, ruleset, classdefs, chaining=True):
483        st = self.newSubtable_(chaining=chaining)
484        st.Format = 2
485        st.populateDefaults()
486
487        if chaining:
488            (
489                st.BacktrackClassDef,
490                st.InputClassDef,
491                st.LookAheadClassDef,
492            ) = [c.build() for c in classdefs]
493        else:
494            st.ClassDef = classdefs[1].build()
495
496        inClasses = classdefs[1].classes()
497
498        classSets = []
499        for _ in inClasses:
500            classSet = self.newRuleSet_(format=2, chaining=chaining)
501            classSets.append(classSet)
502
503        coverage = set()
504        classRuleAttr = self.ruleAttr_(format=2, chaining=chaining)
505
506        for rule in ruleset.rules:
507            ruleAsSubtable = self.newRule_(format=2, chaining=chaining)
508            if chaining:
509                ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix)
510                ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix)
511                # The glyphs in the rule may be list, tuple, odict_keys...
512                # Order is not important anyway because they are guaranteed
513                # to be members of the same class.
514                ruleAsSubtable.Backtrack = [
515                    st.BacktrackClassDef.classDefs[list(x)[0]]
516                    for x in reversed(rule.prefix)
517                ]
518                ruleAsSubtable.LookAhead = [
519                    st.LookAheadClassDef.classDefs[list(x)[0]] for x in rule.suffix
520                ]
521
522                ruleAsSubtable.InputGlyphCount = len(rule.glyphs)
523                ruleAsSubtable.Input = [
524                    st.InputClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:]
525                ]
526                setForThisRule = classSets[
527                    st.InputClassDef.classDefs[list(rule.glyphs[0])[0]]
528                ]
529            else:
530                ruleAsSubtable.GlyphCount = len(rule.glyphs)
531                ruleAsSubtable.Class = [  # The spec calls this InputSequence
532                    st.ClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:]
533                ]
534                setForThisRule = classSets[
535                    st.ClassDef.classDefs[list(rule.glyphs[0])[0]]
536                ]
537
538            self.buildLookupList(rule, ruleAsSubtable)
539            coverage |= set(rule.glyphs[0])
540
541            getattr(setForThisRule, classRuleAttr).append(ruleAsSubtable)
542            setattr(
543                setForThisRule,
544                f"{classRuleAttr}Count",
545                getattr(setForThisRule, f"{classRuleAttr}Count") + 1,
546            )
547        setattr(st, self.ruleSetAttr_(format=2, chaining=chaining), classSets)
548        setattr(
549            st, self.ruleSetAttr_(format=2, chaining=chaining) + "Count", len(classSets)
550        )
551        st.Coverage = buildCoverage(coverage, self.glyphMap)
552        return st
553
554    def buildFormat3Subtable(self, rule, chaining=True):
555        st = self.newSubtable_(chaining=chaining)
556        st.Format = 3
557        if chaining:
558            self.setBacktrackCoverage_(rule.prefix, st)
559            self.setLookAheadCoverage_(rule.suffix, st)
560            self.setInputCoverage_(rule.glyphs, st)
561        else:
562            self.setCoverage_(rule.glyphs, st)
563        self.buildLookupList(rule, st)
564        return st
565
566    def buildLookupList(self, rule, st):
567        for sequenceIndex, lookupList in enumerate(rule.lookups):
568            if lookupList is not None:
569                if not isinstance(lookupList, list):
570                    # Can happen with synthesised lookups
571                    lookupList = [lookupList]
572                for l in lookupList:
573                    if l.lookup_index is None:
574                        if isinstance(self, ChainContextPosBuilder):
575                            other = "substitution"
576                        else:
577                            other = "positioning"
578                        raise OpenTypeLibError(
579                            "Missing index of the specified "
580                            f"lookup, might be a {other} lookup",
581                            self.location,
582                        )
583                    rec = self.newLookupRecord_(st)
584                    rec.SequenceIndex = sequenceIndex
585                    rec.LookupListIndex = l.lookup_index
586
587    def add_subtable_break(self, location):
588        self.rules.append(
589            ChainContextualRule(
590                self.SUBTABLE_BREAK_,
591                self.SUBTABLE_BREAK_,
592                self.SUBTABLE_BREAK_,
593                [self.SUBTABLE_BREAK_],
594            )
595        )
596
597    def newSubtable_(self, chaining=True):
598        subtablename = f"Context{self.subtable_type}"
599        if chaining:
600            subtablename = "Chain" + subtablename
601        st = getattr(ot, subtablename)()  # ot.ChainContextPos()/ot.ChainSubst()/etc.
602        setattr(st, f"{self.subtable_type}Count", 0)
603        setattr(st, f"{self.subtable_type}LookupRecord", [])
604        return st
605
606    # Format 1 and format 2 GSUB5/GSUB6/GPOS7/GPOS8 rulesets and rules form a family:
607    #
608    #       format 1 ruleset      format 1 rule      format 2 ruleset      format 2 rule
609    # GSUB5 SubRuleSet            SubRule            SubClassSet           SubClassRule
610    # GSUB6 ChainSubRuleSet       ChainSubRule       ChainSubClassSet      ChainSubClassRule
611    # GPOS7 PosRuleSet            PosRule            PosClassSet           PosClassRule
612    # GPOS8 ChainPosRuleSet       ChainPosRule       ChainPosClassSet      ChainPosClassRule
613    #
614    # The following functions generate the attribute names and subtables according
615    # to this naming convention.
616    def ruleSetAttr_(self, format=1, chaining=True):
617        if format == 1:
618            formatType = "Rule"
619        elif format == 2:
620            formatType = "Class"
621        else:
622            raise AssertionError(formatType)
623        subtablename = f"{self.subtable_type[0:3]}{formatType}Set"  # Sub, not Subst.
624        if chaining:
625            subtablename = "Chain" + subtablename
626        return subtablename
627
628    def ruleAttr_(self, format=1, chaining=True):
629        if format == 1:
630            formatType = ""
631        elif format == 2:
632            formatType = "Class"
633        else:
634            raise AssertionError(formatType)
635        subtablename = f"{self.subtable_type[0:3]}{formatType}Rule"  # Sub, not Subst.
636        if chaining:
637            subtablename = "Chain" + subtablename
638        return subtablename
639
640    def newRuleSet_(self, format=1, chaining=True):
641        st = getattr(
642            ot, self.ruleSetAttr_(format, chaining)
643        )()  # ot.ChainPosRuleSet()/ot.SubRuleSet()/etc.
644        st.populateDefaults()
645        return st
646
647    def newRule_(self, format=1, chaining=True):
648        st = getattr(
649            ot, self.ruleAttr_(format, chaining)
650        )()  # ot.ChainPosClassRule()/ot.SubClassRule()/etc.
651        st.populateDefaults()
652        return st
653
654    def attachSubtableWithCount_(
655        self, st, subtable_name, count_name, existing=None, index=None, chaining=False
656    ):
657        if chaining:
658            subtable_name = "Chain" + subtable_name
659            count_name = "Chain" + count_name
660
661        if not hasattr(st, count_name):
662            setattr(st, count_name, 0)
663            setattr(st, subtable_name, [])
664
665        if existing:
666            new_subtable = existing
667        else:
668            # Create a new, empty subtable from otTables
669            new_subtable = getattr(ot, subtable_name)()
670
671        setattr(st, count_name, getattr(st, count_name) + 1)
672
673        if index:
674            getattr(st, subtable_name).insert(index, new_subtable)
675        else:
676            getattr(st, subtable_name).append(new_subtable)
677
678        return new_subtable
679
680    def newLookupRecord_(self, st):
681        return self.attachSubtableWithCount_(
682            st,
683            f"{self.subtable_type}LookupRecord",
684            f"{self.subtable_type}Count",
685            chaining=False,
686        )  # Oddly, it isn't ChainSubstLookupRecord
687
688
689class ChainContextPosBuilder(ChainContextualBuilder):
690    """Builds a Chained Contextual Positioning (GPOS8) lookup.
691
692    Users are expected to manually add rules to the ``rules`` attribute after
693    the object has been initialized, e.g.::
694
695        # pos [A B] [C D] x' lookup lu1 y' z' lookup lu2 E;
696
697        prefix  = [ ["A", "B"], ["C", "D"] ]
698        suffix  = [ ["E"] ]
699        glyphs  = [ ["x"], ["y"], ["z"] ]
700        lookups = [ [lu1], None,  [lu2] ]
701        builder.rules.append( (prefix, glyphs, suffix, lookups) )
702
703    Attributes:
704        font (``fontTools.TTLib.TTFont``): A font object.
705        location: A string or tuple representing the location in the original
706            source which produced this lookup.
707        rules: A list of tuples representing the rules in this lookup.
708        lookupflag (int): The lookup's flag
709        markFilterSet: Either ``None`` if no mark filtering set is used, or
710            an integer representing the filtering set to be used for this
711            lookup. If a mark filtering set is provided,
712            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
713            flags.
714    """
715
716    def __init__(self, font, location):
717        LookupBuilder.__init__(self, font, location, "GPOS", 8)
718        self.rules = []
719        self.subtable_type = "Pos"
720
721    def find_chainable_single_pos(self, lookups, glyphs, value):
722        """Helper for add_single_pos_chained_()"""
723        res = None
724        for lookup in lookups[::-1]:
725            if lookup == self.SUBTABLE_BREAK_:
726                return res
727            if isinstance(lookup, SinglePosBuilder) and all(
728                lookup.can_add(glyph, value) for glyph in glyphs
729            ):
730                res = lookup
731        return res
732
733
734class ChainContextSubstBuilder(ChainContextualBuilder):
735    """Builds a Chained Contextual Substitution (GSUB6) lookup.
736
737    Users are expected to manually add rules to the ``rules`` attribute after
738    the object has been initialized, e.g.::
739
740        # sub [A B] [C D] x' lookup lu1 y' z' lookup lu2 E;
741
742        prefix  = [ ["A", "B"], ["C", "D"] ]
743        suffix  = [ ["E"] ]
744        glyphs  = [ ["x"], ["y"], ["z"] ]
745        lookups = [ [lu1], None,  [lu2] ]
746        builder.rules.append( (prefix, glyphs, suffix, lookups) )
747
748    Attributes:
749        font (``fontTools.TTLib.TTFont``): A font object.
750        location: A string or tuple representing the location in the original
751            source which produced this lookup.
752        rules: A list of tuples representing the rules in this lookup.
753        lookupflag (int): The lookup's flag
754        markFilterSet: Either ``None`` if no mark filtering set is used, or
755            an integer representing the filtering set to be used for this
756            lookup. If a mark filtering set is provided,
757            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
758            flags.
759    """
760
761    def __init__(self, font, location):
762        LookupBuilder.__init__(self, font, location, "GSUB", 6)
763        self.rules = []  # (prefix, input, suffix, lookups)
764        self.subtable_type = "Subst"
765
766    def getAlternateGlyphs(self):
767        result = {}
768        for rule in self.rules:
769            if rule.is_subtable_break:
770                continue
771            for lookups in rule.lookups:
772                if not isinstance(lookups, list):
773                    lookups = [lookups]
774                for lookup in lookups:
775                    if lookup is not None:
776                        alts = lookup.getAlternateGlyphs()
777                        for glyph, replacements in alts.items():
778                            alts_for_glyph = result.setdefault(glyph, [])
779                            alts_for_glyph.extend(
780                                g for g in replacements if g not in alts_for_glyph
781                            )
782        return result
783
784    def find_chainable_single_subst(self, mapping):
785        """Helper for add_single_subst_chained_()"""
786        res = None
787        for rule in self.rules[::-1]:
788            if rule.is_subtable_break:
789                return res
790            for sub in rule.lookups:
791                if isinstance(sub, SingleSubstBuilder) and not any(
792                    g in mapping and mapping[g] != sub.mapping[g] for g in sub.mapping
793                ):
794                    res = sub
795        return res
796
797
798class LigatureSubstBuilder(LookupBuilder):
799    """Builds a Ligature Substitution (GSUB4) lookup.
800
801    Users are expected to manually add ligatures to the ``ligatures``
802    attribute after the object has been initialized, e.g.::
803
804        # sub f i by f_i;
805        builder.ligatures[("f","f","i")] = "f_f_i"
806
807    Attributes:
808        font (``fontTools.TTLib.TTFont``): A font object.
809        location: A string or tuple representing the location in the original
810            source which produced this lookup.
811        ligatures: An ordered dictionary mapping a tuple of glyph names to the
812            ligature glyphname.
813        lookupflag (int): The lookup's flag
814        markFilterSet: Either ``None`` if no mark filtering set is used, or
815            an integer representing the filtering set to be used for this
816            lookup. If a mark filtering set is provided,
817            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
818            flags.
819    """
820
821    def __init__(self, font, location):
822        LookupBuilder.__init__(self, font, location, "GSUB", 4)
823        self.ligatures = OrderedDict()  # {('f','f','i'): 'f_f_i'}
824
825    def equals(self, other):
826        return LookupBuilder.equals(self, other) and self.ligatures == other.ligatures
827
828    def build(self):
829        """Build the lookup.
830
831        Returns:
832            An ``otTables.Lookup`` object representing the ligature
833            substitution lookup.
834        """
835        subtables = self.build_subst_subtables(
836            self.ligatures, buildLigatureSubstSubtable
837        )
838        return self.buildLookup_(subtables)
839
840    def add_subtable_break(self, location):
841        self.ligatures[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
842
843
844class MultipleSubstBuilder(LookupBuilder):
845    """Builds a Multiple Substitution (GSUB2) lookup.
846
847    Users are expected to manually add substitutions to the ``mapping``
848    attribute after the object has been initialized, e.g.::
849
850        # sub uni06C0 by uni06D5.fina hamza.above;
851        builder.mapping["uni06C0"] = [ "uni06D5.fina", "hamza.above"]
852
853    Attributes:
854        font (``fontTools.TTLib.TTFont``): A font object.
855        location: A string or tuple representing the location in the original
856            source which produced this lookup.
857        mapping: An ordered dictionary mapping a glyph name to a list of
858            substituted glyph names.
859        lookupflag (int): The lookup's flag
860        markFilterSet: Either ``None`` if no mark filtering set is used, or
861            an integer representing the filtering set to be used for this
862            lookup. If a mark filtering set is provided,
863            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
864            flags.
865    """
866
867    def __init__(self, font, location):
868        LookupBuilder.__init__(self, font, location, "GSUB", 2)
869        self.mapping = OrderedDict()
870
871    def equals(self, other):
872        return LookupBuilder.equals(self, other) and self.mapping == other.mapping
873
874    def build(self):
875        subtables = self.build_subst_subtables(self.mapping, buildMultipleSubstSubtable)
876        return self.buildLookup_(subtables)
877
878    def add_subtable_break(self, location):
879        self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
880
881
882class CursivePosBuilder(LookupBuilder):
883    """Builds a Cursive Positioning (GPOS3) lookup.
884
885    Attributes:
886        font (``fontTools.TTLib.TTFont``): A font object.
887        location: A string or tuple representing the location in the original
888            source which produced this lookup.
889        attachments: An ordered dictionary mapping a glyph name to a two-element
890            tuple of ``otTables.Anchor`` objects.
891        lookupflag (int): The lookup's flag
892        markFilterSet: Either ``None`` if no mark filtering set is used, or
893            an integer representing the filtering set to be used for this
894            lookup. If a mark filtering set is provided,
895            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
896            flags.
897    """
898
899    def __init__(self, font, location):
900        LookupBuilder.__init__(self, font, location, "GPOS", 3)
901        self.attachments = {}
902
903    def equals(self, other):
904        return (
905            LookupBuilder.equals(self, other) and self.attachments == other.attachments
906        )
907
908    def add_attachment(self, location, glyphs, entryAnchor, exitAnchor):
909        """Adds attachment information to the cursive positioning lookup.
910
911        Args:
912            location: A string or tuple representing the location in the
913                original source which produced this lookup. (Unused.)
914            glyphs: A list of glyph names sharing these entry and exit
915                anchor locations.
916            entryAnchor: A ``otTables.Anchor`` object representing the
917                entry anchor, or ``None`` if no entry anchor is present.
918            exitAnchor: A ``otTables.Anchor`` object representing the
919                exit anchor, or ``None`` if no exit anchor is present.
920        """
921        for glyph in glyphs:
922            self.attachments[glyph] = (entryAnchor, exitAnchor)
923
924    def build(self):
925        """Build the lookup.
926
927        Returns:
928            An ``otTables.Lookup`` object representing the cursive
929            positioning lookup.
930        """
931        st = buildCursivePosSubtable(self.attachments, self.glyphMap)
932        return self.buildLookup_([st])
933
934
935class MarkBasePosBuilder(LookupBuilder):
936    """Builds a Mark-To-Base Positioning (GPOS4) lookup.
937
938    Users are expected to manually add marks and bases to the ``marks``
939    and ``bases`` attributes after the object has been initialized, e.g.::
940
941        builder.marks["acute"]   = (0, a1)
942        builder.marks["grave"]   = (0, a1)
943        builder.marks["cedilla"] = (1, a2)
944        builder.bases["a"] = {0: a3, 1: a5}
945        builder.bases["b"] = {0: a4, 1: a5}
946
947    Attributes:
948        font (``fontTools.TTLib.TTFont``): A font object.
949        location: A string or tuple representing the location in the original
950            source which produced this lookup.
951        marks: An dictionary mapping a glyph name to a two-element
952            tuple containing a mark class ID and ``otTables.Anchor`` object.
953        bases: An dictionary mapping a glyph name to a dictionary of
954            mark class IDs and ``otTables.Anchor`` object.
955        lookupflag (int): The lookup's flag
956        markFilterSet: Either ``None`` if no mark filtering set is used, or
957            an integer representing the filtering set to be used for this
958            lookup. If a mark filtering set is provided,
959            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
960            flags.
961    """
962
963    def __init__(self, font, location):
964        LookupBuilder.__init__(self, font, location, "GPOS", 4)
965        self.marks = {}  # glyphName -> (markClassName, anchor)
966        self.bases = {}  # glyphName -> {markClassName: anchor}
967
968    def equals(self, other):
969        return (
970            LookupBuilder.equals(self, other)
971            and self.marks == other.marks
972            and self.bases == other.bases
973        )
974
975    def inferGlyphClasses(self):
976        result = {glyph: 1 for glyph in self.bases}
977        result.update({glyph: 3 for glyph in self.marks})
978        return result
979
980    def build(self):
981        """Build the lookup.
982
983        Returns:
984            An ``otTables.Lookup`` object representing the mark-to-base
985            positioning lookup.
986        """
987        markClasses = self.buildMarkClasses_(self.marks)
988        marks = {}
989        for mark, (mc, anchor) in self.marks.items():
990            if mc not in markClasses:
991                raise ValueError(
992                    "Mark class %s not found for mark glyph %s" % (mc, mark)
993                )
994            marks[mark] = (markClasses[mc], anchor)
995        bases = {}
996        for glyph, anchors in self.bases.items():
997            bases[glyph] = {}
998            for mc, anchor in anchors.items():
999                if mc not in markClasses:
1000                    raise ValueError(
1001                        "Mark class %s not found for base glyph %s" % (mc, glyph)
1002                    )
1003                bases[glyph][markClasses[mc]] = anchor
1004        subtables = buildMarkBasePos(marks, bases, self.glyphMap)
1005        return self.buildLookup_(subtables)
1006
1007
1008class MarkLigPosBuilder(LookupBuilder):
1009    """Builds a Mark-To-Ligature Positioning (GPOS5) lookup.
1010
1011    Users are expected to manually add marks and bases to the ``marks``
1012    and ``ligatures`` attributes after the object has been initialized, e.g.::
1013
1014        builder.marks["acute"]   = (0, a1)
1015        builder.marks["grave"]   = (0, a1)
1016        builder.marks["cedilla"] = (1, a2)
1017        builder.ligatures["f_i"] = [
1018            { 0: a3, 1: a5 }, # f
1019            { 0: a4, 1: a5 }  # i
1020        ]
1021
1022    Attributes:
1023        font (``fontTools.TTLib.TTFont``): A font object.
1024        location: A string or tuple representing the location in the original
1025            source which produced this lookup.
1026        marks: An dictionary mapping a glyph name to a two-element
1027            tuple containing a mark class ID and ``otTables.Anchor`` object.
1028        ligatures: An dictionary mapping a glyph name to an array with one
1029            element for each ligature component. Each array element should be
1030            a dictionary mapping mark class IDs to ``otTables.Anchor`` objects.
1031        lookupflag (int): The lookup's flag
1032        markFilterSet: Either ``None`` if no mark filtering set is used, or
1033            an integer representing the filtering set to be used for this
1034            lookup. If a mark filtering set is provided,
1035            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1036            flags.
1037    """
1038
1039    def __init__(self, font, location):
1040        LookupBuilder.__init__(self, font, location, "GPOS", 5)
1041        self.marks = {}  # glyphName -> (markClassName, anchor)
1042        self.ligatures = {}  # glyphName -> [{markClassName: anchor}, ...]
1043
1044    def equals(self, other):
1045        return (
1046            LookupBuilder.equals(self, other)
1047            and self.marks == other.marks
1048            and self.ligatures == other.ligatures
1049        )
1050
1051    def inferGlyphClasses(self):
1052        result = {glyph: 2 for glyph in self.ligatures}
1053        result.update({glyph: 3 for glyph in self.marks})
1054        return result
1055
1056    def build(self):
1057        """Build the lookup.
1058
1059        Returns:
1060            An ``otTables.Lookup`` object representing the mark-to-ligature
1061            positioning lookup.
1062        """
1063        markClasses = self.buildMarkClasses_(self.marks)
1064        marks = {
1065            mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
1066        }
1067        ligs = {}
1068        for lig, components in self.ligatures.items():
1069            ligs[lig] = []
1070            for c in components:
1071                ligs[lig].append({markClasses[mc]: a for mc, a in c.items()})
1072        subtables = buildMarkLigPos(marks, ligs, self.glyphMap)
1073        return self.buildLookup_(subtables)
1074
1075
1076class MarkMarkPosBuilder(LookupBuilder):
1077    """Builds a Mark-To-Mark Positioning (GPOS6) lookup.
1078
1079    Users are expected to manually add marks and bases to the ``marks``
1080    and ``baseMarks`` attributes after the object has been initialized, e.g.::
1081
1082        builder.marks["acute"]     = (0, a1)
1083        builder.marks["grave"]     = (0, a1)
1084        builder.marks["cedilla"]   = (1, a2)
1085        builder.baseMarks["acute"] = {0: a3}
1086
1087    Attributes:
1088        font (``fontTools.TTLib.TTFont``): A font object.
1089        location: A string or tuple representing the location in the original
1090            source which produced this lookup.
1091        marks: An dictionary mapping a glyph name to a two-element
1092            tuple containing a mark class ID and ``otTables.Anchor`` object.
1093        baseMarks: An dictionary mapping a glyph name to a dictionary
1094            containing one item: a mark class ID and a ``otTables.Anchor`` object.
1095        lookupflag (int): The lookup's flag
1096        markFilterSet: Either ``None`` if no mark filtering set is used, or
1097            an integer representing the filtering set to be used for this
1098            lookup. If a mark filtering set is provided,
1099            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1100            flags.
1101    """
1102
1103    def __init__(self, font, location):
1104        LookupBuilder.__init__(self, font, location, "GPOS", 6)
1105        self.marks = {}  # glyphName -> (markClassName, anchor)
1106        self.baseMarks = {}  # glyphName -> {markClassName: anchor}
1107
1108    def equals(self, other):
1109        return (
1110            LookupBuilder.equals(self, other)
1111            and self.marks == other.marks
1112            and self.baseMarks == other.baseMarks
1113        )
1114
1115    def inferGlyphClasses(self):
1116        result = {glyph: 3 for glyph in self.baseMarks}
1117        result.update({glyph: 3 for glyph in self.marks})
1118        return result
1119
1120    def build(self):
1121        """Build the lookup.
1122
1123        Returns:
1124            An ``otTables.Lookup`` object representing the mark-to-mark
1125            positioning lookup.
1126        """
1127        markClasses = self.buildMarkClasses_(self.marks)
1128        markClassList = sorted(markClasses.keys(), key=markClasses.get)
1129        marks = {
1130            mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
1131        }
1132
1133        st = ot.MarkMarkPos()
1134        st.Format = 1
1135        st.ClassCount = len(markClasses)
1136        st.Mark1Coverage = buildCoverage(marks, self.glyphMap)
1137        st.Mark2Coverage = buildCoverage(self.baseMarks, self.glyphMap)
1138        st.Mark1Array = buildMarkArray(marks, self.glyphMap)
1139        st.Mark2Array = ot.Mark2Array()
1140        st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs)
1141        st.Mark2Array.Mark2Record = []
1142        for base in st.Mark2Coverage.glyphs:
1143            anchors = [self.baseMarks[base].get(mc) for mc in markClassList]
1144            st.Mark2Array.Mark2Record.append(buildMark2Record(anchors))
1145        return self.buildLookup_([st])
1146
1147
1148class ReverseChainSingleSubstBuilder(LookupBuilder):
1149    """Builds a Reverse Chaining Contextual Single Substitution (GSUB8) lookup.
1150
1151    Users are expected to manually add substitutions to the ``substitutions``
1152    attribute after the object has been initialized, e.g.::
1153
1154        # reversesub [a e n] d' by d.alt;
1155        prefix = [ ["a", "e", "n"] ]
1156        suffix = []
1157        mapping = { "d": "d.alt" }
1158        builder.substitutions.append( (prefix, suffix, mapping) )
1159
1160    Attributes:
1161        font (``fontTools.TTLib.TTFont``): A font object.
1162        location: A string or tuple representing the location in the original
1163            source which produced this lookup.
1164        substitutions: A three-element tuple consisting of a prefix sequence,
1165            a suffix sequence, and a dictionary of single substitutions.
1166        lookupflag (int): The lookup's flag
1167        markFilterSet: Either ``None`` if no mark filtering set is used, or
1168            an integer representing the filtering set to be used for this
1169            lookup. If a mark filtering set is provided,
1170            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1171            flags.
1172    """
1173
1174    def __init__(self, font, location):
1175        LookupBuilder.__init__(self, font, location, "GSUB", 8)
1176        self.rules = []  # (prefix, suffix, mapping)
1177
1178    def equals(self, other):
1179        return LookupBuilder.equals(self, other) and self.rules == other.rules
1180
1181    def build(self):
1182        """Build the lookup.
1183
1184        Returns:
1185            An ``otTables.Lookup`` object representing the chained
1186            contextual substitution lookup.
1187        """
1188        subtables = []
1189        for prefix, suffix, mapping in self.rules:
1190            st = ot.ReverseChainSingleSubst()
1191            st.Format = 1
1192            self.setBacktrackCoverage_(prefix, st)
1193            self.setLookAheadCoverage_(suffix, st)
1194            st.Coverage = buildCoverage(mapping.keys(), self.glyphMap)
1195            st.GlyphCount = len(mapping)
1196            st.Substitute = [mapping[g] for g in st.Coverage.glyphs]
1197            subtables.append(st)
1198        return self.buildLookup_(subtables)
1199
1200    def add_subtable_break(self, location):
1201        # Nothing to do here, each substitution is in its own subtable.
1202        pass
1203
1204
1205class SingleSubstBuilder(LookupBuilder):
1206    """Builds a Single Substitution (GSUB1) lookup.
1207
1208    Users are expected to manually add substitutions to the ``mapping``
1209    attribute after the object has been initialized, e.g.::
1210
1211        # sub x by y;
1212        builder.mapping["x"] = "y"
1213
1214    Attributes:
1215        font (``fontTools.TTLib.TTFont``): A font object.
1216        location: A string or tuple representing the location in the original
1217            source which produced this lookup.
1218        mapping: A dictionary mapping a single glyph name to another glyph name.
1219        lookupflag (int): The lookup's flag
1220        markFilterSet: Either ``None`` if no mark filtering set is used, or
1221            an integer representing the filtering set to be used for this
1222            lookup. If a mark filtering set is provided,
1223            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1224            flags.
1225    """
1226
1227    def __init__(self, font, location):
1228        LookupBuilder.__init__(self, font, location, "GSUB", 1)
1229        self.mapping = OrderedDict()
1230
1231    def equals(self, other):
1232        return LookupBuilder.equals(self, other) and self.mapping == other.mapping
1233
1234    def build(self):
1235        """Build the lookup.
1236
1237        Returns:
1238            An ``otTables.Lookup`` object representing the multiple
1239            substitution lookup.
1240        """
1241        subtables = self.build_subst_subtables(self.mapping, buildSingleSubstSubtable)
1242        return self.buildLookup_(subtables)
1243
1244    def getAlternateGlyphs(self):
1245        return {glyph: [repl] for glyph, repl in self.mapping.items()}
1246
1247    def add_subtable_break(self, location):
1248        self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
1249
1250
1251class ClassPairPosSubtableBuilder(object):
1252    """Builds class-based Pair Positioning (GPOS2 format 2) subtables.
1253
1254    Note that this does *not* build a GPOS2 ``otTables.Lookup`` directly,
1255    but builds a list of ``otTables.PairPos`` subtables. It is used by the
1256    :class:`PairPosBuilder` below.
1257
1258    Attributes:
1259        builder (PairPosBuilder): A pair positioning lookup builder.
1260    """
1261
1262    def __init__(self, builder):
1263        self.builder_ = builder
1264        self.classDef1_, self.classDef2_ = None, None
1265        self.values_ = {}  # (glyphclass1, glyphclass2) --> (value1, value2)
1266        self.forceSubtableBreak_ = False
1267        self.subtables_ = []
1268
1269    def addPair(self, gc1, value1, gc2, value2):
1270        """Add a pair positioning rule.
1271
1272        Args:
1273            gc1: A set of glyph names for the "left" glyph
1274            value1: An ``otTables.ValueRecord`` object for the left glyph's
1275                positioning.
1276            gc2: A set of glyph names for the "right" glyph
1277            value2: An ``otTables.ValueRecord`` object for the right glyph's
1278                positioning.
1279        """
1280        mergeable = (
1281            not self.forceSubtableBreak_
1282            and self.classDef1_ is not None
1283            and self.classDef1_.canAdd(gc1)
1284            and self.classDef2_ is not None
1285            and self.classDef2_.canAdd(gc2)
1286        )
1287        if not mergeable:
1288            self.flush_()
1289            self.classDef1_ = ClassDefBuilder(useClass0=True)
1290            self.classDef2_ = ClassDefBuilder(useClass0=False)
1291            self.values_ = {}
1292        self.classDef1_.add(gc1)
1293        self.classDef2_.add(gc2)
1294        self.values_[(gc1, gc2)] = (value1, value2)
1295
1296    def addSubtableBreak(self):
1297        """Add an explicit subtable break at this point."""
1298        self.forceSubtableBreak_ = True
1299
1300    def subtables(self):
1301        """Return the list of ``otTables.PairPos`` subtables constructed."""
1302        self.flush_()
1303        return self.subtables_
1304
1305    def flush_(self):
1306        if self.classDef1_ is None or self.classDef2_ is None:
1307            return
1308        st = buildPairPosClassesSubtable(self.values_, self.builder_.glyphMap)
1309        if st.Coverage is None:
1310            return
1311        self.subtables_.append(st)
1312        self.forceSubtableBreak_ = False
1313
1314
1315class PairPosBuilder(LookupBuilder):
1316    """Builds a Pair Positioning (GPOS2) lookup.
1317
1318    Attributes:
1319        font (``fontTools.TTLib.TTFont``): A font object.
1320        location: A string or tuple representing the location in the original
1321            source which produced this lookup.
1322        pairs: An array of class-based pair positioning tuples. Usually
1323            manipulated with the :meth:`addClassPair` method below.
1324        glyphPairs: A dictionary mapping a tuple of glyph names to a tuple
1325            of ``otTables.ValueRecord`` objects. Usually manipulated with the
1326            :meth:`addGlyphPair` method below.
1327        lookupflag (int): The lookup's flag
1328        markFilterSet: Either ``None`` if no mark filtering set is used, or
1329            an integer representing the filtering set to be used for this
1330            lookup. If a mark filtering set is provided,
1331            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1332            flags.
1333    """
1334
1335    def __init__(self, font, location):
1336        LookupBuilder.__init__(self, font, location, "GPOS", 2)
1337        self.pairs = []  # [(gc1, value1, gc2, value2)*]
1338        self.glyphPairs = {}  # (glyph1, glyph2) --> (value1, value2)
1339        self.locations = {}  # (gc1, gc2) --> (filepath, line, column)
1340
1341    def addClassPair(self, location, glyphclass1, value1, glyphclass2, value2):
1342        """Add a class pair positioning rule to the current lookup.
1343
1344        Args:
1345            location: A string or tuple representing the location in the
1346                original source which produced this rule. Unused.
1347            glyphclass1: A set of glyph names for the "left" glyph in the pair.
1348            value1: A ``otTables.ValueRecord`` for positioning the left glyph.
1349            glyphclass2: A set of glyph names for the "right" glyph in the pair.
1350            value2: A ``otTables.ValueRecord`` for positioning the right glyph.
1351        """
1352        self.pairs.append((glyphclass1, value1, glyphclass2, value2))
1353
1354    def addGlyphPair(self, location, glyph1, value1, glyph2, value2):
1355        """Add a glyph pair positioning rule to the current lookup.
1356
1357        Args:
1358            location: A string or tuple representing the location in the
1359                original source which produced this rule.
1360            glyph1: A glyph name for the "left" glyph in the pair.
1361            value1: A ``otTables.ValueRecord`` for positioning the left glyph.
1362            glyph2: A glyph name for the "right" glyph in the pair.
1363            value2: A ``otTables.ValueRecord`` for positioning the right glyph.
1364        """
1365        key = (glyph1, glyph2)
1366        oldValue = self.glyphPairs.get(key, None)
1367        if oldValue is not None:
1368            # the Feature File spec explicitly allows specific pairs generated
1369            # by an 'enum' rule to be overridden by preceding single pairs
1370            otherLoc = self.locations[key]
1371            log.debug(
1372                "Already defined position for pair %s %s at %s; "
1373                "choosing the first value",
1374                glyph1,
1375                glyph2,
1376                otherLoc,
1377            )
1378        else:
1379            self.glyphPairs[key] = (value1, value2)
1380            self.locations[key] = location
1381
1382    def add_subtable_break(self, location):
1383        self.pairs.append(
1384            (
1385                self.SUBTABLE_BREAK_,
1386                self.SUBTABLE_BREAK_,
1387                self.SUBTABLE_BREAK_,
1388                self.SUBTABLE_BREAK_,
1389            )
1390        )
1391
1392    def equals(self, other):
1393        return (
1394            LookupBuilder.equals(self, other)
1395            and self.glyphPairs == other.glyphPairs
1396            and self.pairs == other.pairs
1397        )
1398
1399    def build(self):
1400        """Build the lookup.
1401
1402        Returns:
1403            An ``otTables.Lookup`` object representing the pair positioning
1404            lookup.
1405        """
1406        builders = {}
1407        builder = ClassPairPosSubtableBuilder(self)
1408        for glyphclass1, value1, glyphclass2, value2 in self.pairs:
1409            if glyphclass1 is self.SUBTABLE_BREAK_:
1410                builder.addSubtableBreak()
1411                continue
1412            builder.addPair(glyphclass1, value1, glyphclass2, value2)
1413        subtables = []
1414        if self.glyphPairs:
1415            subtables.extend(buildPairPosGlyphs(self.glyphPairs, self.glyphMap))
1416        subtables.extend(builder.subtables())
1417        lookup = self.buildLookup_(subtables)
1418
1419        # Compact the lookup
1420        # This is a good moment to do it because the compaction should create
1421        # smaller subtables, which may prevent overflows from happening.
1422        # Keep reading the value from the ENV until ufo2ft switches to the config system
1423        level = self.font.cfg.get(
1424            "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
1425            default=_compression_level_from_env(),
1426        )
1427        if level != 0:
1428            log.info("Compacting GPOS...")
1429            compact_lookup(self.font, level, lookup)
1430
1431        return lookup
1432
1433
1434class SinglePosBuilder(LookupBuilder):
1435    """Builds a Single Positioning (GPOS1) lookup.
1436
1437    Attributes:
1438        font (``fontTools.TTLib.TTFont``): A font object.
1439        location: A string or tuple representing the location in the original
1440            source which produced this lookup.
1441        mapping: A dictionary mapping a glyph name to a ``otTables.ValueRecord``
1442            objects. Usually manipulated with the :meth:`add_pos` method below.
1443        lookupflag (int): The lookup's flag
1444        markFilterSet: Either ``None`` if no mark filtering set is used, or
1445            an integer representing the filtering set to be used for this
1446            lookup. If a mark filtering set is provided,
1447            `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
1448            flags.
1449    """
1450
1451    def __init__(self, font, location):
1452        LookupBuilder.__init__(self, font, location, "GPOS", 1)
1453        self.locations = {}  # glyph -> (filename, line, column)
1454        self.mapping = {}  # glyph -> ot.ValueRecord
1455
1456    def add_pos(self, location, glyph, otValueRecord):
1457        """Add a single positioning rule.
1458
1459        Args:
1460            location: A string or tuple representing the location in the
1461                original source which produced this lookup.
1462            glyph: A glyph name.
1463            otValueRection: A ``otTables.ValueRecord`` used to position the
1464                glyph.
1465        """
1466        if not self.can_add(glyph, otValueRecord):
1467            otherLoc = self.locations[glyph]
1468            raise OpenTypeLibError(
1469                'Already defined different position for glyph "%s" at %s'
1470                % (glyph, otherLoc),
1471                location,
1472            )
1473        if otValueRecord:
1474            self.mapping[glyph] = otValueRecord
1475        self.locations[glyph] = location
1476
1477    def can_add(self, glyph, value):
1478        assert isinstance(value, ValueRecord)
1479        curValue = self.mapping.get(glyph)
1480        return curValue is None or curValue == value
1481
1482    def equals(self, other):
1483        return LookupBuilder.equals(self, other) and self.mapping == other.mapping
1484
1485    def build(self):
1486        """Build the lookup.
1487
1488        Returns:
1489            An ``otTables.Lookup`` object representing the single positioning
1490            lookup.
1491        """
1492        subtables = buildSinglePos(self.mapping, self.glyphMap)
1493        return self.buildLookup_(subtables)
1494
1495
1496# GSUB
1497
1498
1499def buildSingleSubstSubtable(mapping):
1500    """Builds a single substitution (GSUB1) subtable.
1501
1502    Note that if you are implementing a layout compiler, you may find it more
1503    flexible to use
1504    :py:class:`fontTools.otlLib.lookupBuilders.SingleSubstBuilder` instead.
1505
1506    Args:
1507        mapping: A dictionary mapping input glyph names to output glyph names.
1508
1509    Returns:
1510        An ``otTables.SingleSubst`` object, or ``None`` if the mapping dictionary
1511        is empty.
1512    """
1513    if not mapping:
1514        return None
1515    self = ot.SingleSubst()
1516    self.mapping = dict(mapping)
1517    return self
1518
1519
1520def buildMultipleSubstSubtable(mapping):
1521    """Builds a multiple substitution (GSUB2) subtable.
1522
1523    Note that if you are implementing a layout compiler, you may find it more
1524    flexible to use
1525    :py:class:`fontTools.otlLib.lookupBuilders.MultipleSubstBuilder` instead.
1526
1527    Example::
1528
1529        # sub uni06C0 by uni06D5.fina hamza.above
1530        # sub uni06C2 by uni06C1.fina hamza.above;
1531
1532        subtable = buildMultipleSubstSubtable({
1533            "uni06C0": [ "uni06D5.fina", "hamza.above"],
1534            "uni06C2": [ "uni06D1.fina", "hamza.above"]
1535        })
1536
1537    Args:
1538        mapping: A dictionary mapping input glyph names to a list of output
1539            glyph names.
1540
1541    Returns:
1542        An ``otTables.MultipleSubst`` object or ``None`` if the mapping dictionary
1543        is empty.
1544    """
1545    if not mapping:
1546        return None
1547    self = ot.MultipleSubst()
1548    self.mapping = dict(mapping)
1549    return self
1550
1551
1552def buildAlternateSubstSubtable(mapping):
1553    """Builds an alternate substitution (GSUB3) subtable.
1554
1555    Note that if you are implementing a layout compiler, you may find it more
1556    flexible to use
1557    :py:class:`fontTools.otlLib.lookupBuilders.AlternateSubstBuilder` instead.
1558
1559    Args:
1560        mapping: A dictionary mapping input glyph names to a list of output
1561            glyph names.
1562
1563    Returns:
1564        An ``otTables.AlternateSubst`` object or ``None`` if the mapping dictionary
1565        is empty.
1566    """
1567    if not mapping:
1568        return None
1569    self = ot.AlternateSubst()
1570    self.alternates = dict(mapping)
1571    return self
1572
1573
1574def buildLigatureSubstSubtable(mapping):
1575    """Builds a ligature substitution (GSUB4) subtable.
1576
1577    Note that if you are implementing a layout compiler, you may find it more
1578    flexible to use
1579    :py:class:`fontTools.otlLib.lookupBuilders.LigatureSubstBuilder` instead.
1580
1581    Example::
1582
1583        # sub f f i by f_f_i;
1584        # sub f i by f_i;
1585
1586        subtable = buildLigatureSubstSubtable({
1587            ("f", "f", "i"): "f_f_i",
1588            ("f", "i"): "f_i",
1589        })
1590
1591    Args:
1592        mapping: A dictionary mapping tuples of glyph names to output
1593            glyph names.
1594
1595    Returns:
1596        An ``otTables.LigatureSubst`` object or ``None`` if the mapping dictionary
1597        is empty.
1598    """
1599
1600    if not mapping:
1601        return None
1602    self = ot.LigatureSubst()
1603    # The following single line can replace the rest of this function
1604    # with fontTools >= 3.1:
1605    # self.ligatures = dict(mapping)
1606    self.ligatures = {}
1607    for components in sorted(mapping.keys(), key=self._getLigatureSortKey):
1608        ligature = ot.Ligature()
1609        ligature.Component = components[1:]
1610        ligature.CompCount = len(ligature.Component) + 1
1611        ligature.LigGlyph = mapping[components]
1612        firstGlyph = components[0]
1613        self.ligatures.setdefault(firstGlyph, []).append(ligature)
1614    return self
1615
1616
1617# GPOS
1618
1619
1620def buildAnchor(x, y, point=None, deviceX=None, deviceY=None):
1621    """Builds an Anchor table.
1622
1623    This determines the appropriate anchor format based on the passed parameters.
1624
1625    Args:
1626        x (int): X coordinate.
1627        y (int): Y coordinate.
1628        point (int): Index of glyph contour point, if provided.
1629        deviceX (``otTables.Device``): X coordinate device table, if provided.
1630        deviceY (``otTables.Device``): Y coordinate device table, if provided.
1631
1632    Returns:
1633        An ``otTables.Anchor`` object.
1634    """
1635    self = ot.Anchor()
1636    self.XCoordinate, self.YCoordinate = x, y
1637    self.Format = 1
1638    if point is not None:
1639        self.AnchorPoint = point
1640        self.Format = 2
1641    if deviceX is not None or deviceY is not None:
1642        assert (
1643            self.Format == 1
1644        ), "Either point, or both of deviceX/deviceY, must be None."
1645        self.XDeviceTable = deviceX
1646        self.YDeviceTable = deviceY
1647        self.Format = 3
1648    return self
1649
1650
1651def buildBaseArray(bases, numMarkClasses, glyphMap):
1652    """Builds a base array record.
1653
1654    As part of building mark-to-base positioning rules, you will need to define
1655    a ``BaseArray`` record, which "defines for each base glyph an array of
1656    anchors, one for each mark class." This function builds the base array
1657    subtable.
1658
1659    Example::
1660
1661        bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}}
1662        basearray = buildBaseArray(bases, 2, font.getReverseGlyphMap())
1663
1664    Args:
1665        bases (dict): A dictionary mapping anchors to glyphs; the keys being
1666            glyph names, and the values being dictionaries mapping mark class ID
1667            to the appropriate ``otTables.Anchor`` object used for attaching marks
1668            of that class.
1669        numMarkClasses (int): The total number of mark classes for which anchors
1670            are defined.
1671        glyphMap: a glyph name to ID map, typically returned from
1672            ``font.getReverseGlyphMap()``.
1673
1674    Returns:
1675        An ``otTables.BaseArray`` object.
1676    """
1677    self = ot.BaseArray()
1678    self.BaseRecord = []
1679    for base in sorted(bases, key=glyphMap.__getitem__):
1680        b = bases[base]
1681        anchors = [b.get(markClass) for markClass in range(numMarkClasses)]
1682        self.BaseRecord.append(buildBaseRecord(anchors))
1683    self.BaseCount = len(self.BaseRecord)
1684    return self
1685
1686
1687def buildBaseRecord(anchors):
1688    # [otTables.Anchor, otTables.Anchor, ...] --> otTables.BaseRecord
1689    self = ot.BaseRecord()
1690    self.BaseAnchor = anchors
1691    return self
1692
1693
1694def buildComponentRecord(anchors):
1695    """Builds a component record.
1696
1697    As part of building mark-to-ligature positioning rules, you will need to
1698    define ``ComponentRecord`` objects, which contain "an array of offsets...
1699    to the Anchor tables that define all the attachment points used to attach
1700    marks to the component." This function builds the component record.
1701
1702    Args:
1703        anchors: A list of ``otTables.Anchor`` objects or ``None``.
1704
1705    Returns:
1706        A ``otTables.ComponentRecord`` object or ``None`` if no anchors are
1707        supplied.
1708    """
1709    if not anchors:
1710        return None
1711    self = ot.ComponentRecord()
1712    self.LigatureAnchor = anchors
1713    return self
1714
1715
1716def buildCursivePosSubtable(attach, glyphMap):
1717    """Builds a cursive positioning (GPOS3) subtable.
1718
1719    Cursive positioning lookups are made up of a coverage table of glyphs,
1720    and a set of ``EntryExitRecord`` records containing the anchors for
1721    each glyph. This function builds the cursive positioning subtable.
1722
1723    Example::
1724
1725        subtable = buildCursivePosSubtable({
1726            "AlifIni": (None, buildAnchor(0, 50)),
1727            "BehMed": (buildAnchor(500,250), buildAnchor(0,50)),
1728            # ...
1729        }, font.getReverseGlyphMap())
1730
1731    Args:
1732        attach (dict): A mapping between glyph names and a tuple of two
1733            ``otTables.Anchor`` objects representing entry and exit anchors.
1734        glyphMap: a glyph name to ID map, typically returned from
1735            ``font.getReverseGlyphMap()``.
1736
1737    Returns:
1738        An ``otTables.CursivePos`` object, or ``None`` if the attachment
1739        dictionary was empty.
1740    """
1741    if not attach:
1742        return None
1743    self = ot.CursivePos()
1744    self.Format = 1
1745    self.Coverage = buildCoverage(attach.keys(), glyphMap)
1746    self.EntryExitRecord = []
1747    for glyph in self.Coverage.glyphs:
1748        entryAnchor, exitAnchor = attach[glyph]
1749        rec = ot.EntryExitRecord()
1750        rec.EntryAnchor = entryAnchor
1751        rec.ExitAnchor = exitAnchor
1752        self.EntryExitRecord.append(rec)
1753    self.EntryExitCount = len(self.EntryExitRecord)
1754    return self
1755
1756
1757def buildDevice(deltas):
1758    """Builds a Device record as part of a ValueRecord or Anchor.
1759
1760    Device tables specify size-specific adjustments to value records
1761    and anchors to reflect changes based on the resolution of the output.
1762    For example, one could specify that an anchor's Y position should be
1763    increased by 1 pixel when displayed at 8 pixels per em. This routine
1764    builds device records.
1765
1766    Args:
1767        deltas: A dictionary mapping pixels-per-em sizes to the delta
1768            adjustment in pixels when the font is displayed at that size.
1769
1770    Returns:
1771        An ``otTables.Device`` object if any deltas were supplied, or
1772        ``None`` otherwise.
1773    """
1774    if not deltas:
1775        return None
1776    self = ot.Device()
1777    keys = deltas.keys()
1778    self.StartSize = startSize = min(keys)
1779    self.EndSize = endSize = max(keys)
1780    assert 0 <= startSize <= endSize
1781    self.DeltaValue = deltaValues = [
1782        deltas.get(size, 0) for size in range(startSize, endSize + 1)
1783    ]
1784    maxDelta = max(deltaValues)
1785    minDelta = min(deltaValues)
1786    assert minDelta > -129 and maxDelta < 128
1787    if minDelta > -3 and maxDelta < 2:
1788        self.DeltaFormat = 1
1789    elif minDelta > -9 and maxDelta < 8:
1790        self.DeltaFormat = 2
1791    else:
1792        self.DeltaFormat = 3
1793    return self
1794
1795
1796def buildLigatureArray(ligs, numMarkClasses, glyphMap):
1797    """Builds a LigatureArray subtable.
1798
1799    As part of building a mark-to-ligature lookup, you will need to define
1800    the set of anchors (for each mark class) on each component of the ligature
1801    where marks can be attached. For example, for an Arabic divine name ligature
1802    (lam lam heh), you may want to specify mark attachment positioning for
1803    superior marks (fatha, etc.) and inferior marks (kasra, etc.) on each glyph
1804    of the ligature. This routine builds the ligature array record.
1805
1806    Example::
1807
1808        buildLigatureArray({
1809            "lam-lam-heh": [
1810                { 0: superiorAnchor1, 1: inferiorAnchor1 }, # attach points for lam1
1811                { 0: superiorAnchor2, 1: inferiorAnchor2 }, # attach points for lam2
1812                { 0: superiorAnchor3, 1: inferiorAnchor3 }, # attach points for heh
1813            ]
1814        }, 2, font.getReverseGlyphMap())
1815
1816    Args:
1817        ligs (dict): A mapping of ligature names to an array of dictionaries:
1818            for each component glyph in the ligature, an dictionary mapping
1819            mark class IDs to anchors.
1820        numMarkClasses (int): The number of mark classes.
1821        glyphMap: a glyph name to ID map, typically returned from
1822            ``font.getReverseGlyphMap()``.
1823
1824    Returns:
1825        An ``otTables.LigatureArray`` object if deltas were supplied.
1826    """
1827    self = ot.LigatureArray()
1828    self.LigatureAttach = []
1829    for lig in sorted(ligs, key=glyphMap.__getitem__):
1830        anchors = []
1831        for component in ligs[lig]:
1832            anchors.append([component.get(mc) for mc in range(numMarkClasses)])
1833        self.LigatureAttach.append(buildLigatureAttach(anchors))
1834    self.LigatureCount = len(self.LigatureAttach)
1835    return self
1836
1837
1838def buildLigatureAttach(components):
1839    # [[Anchor, Anchor], [Anchor, Anchor, Anchor]] --> LigatureAttach
1840    self = ot.LigatureAttach()
1841    self.ComponentRecord = [buildComponentRecord(c) for c in components]
1842    self.ComponentCount = len(self.ComponentRecord)
1843    return self
1844
1845
1846def buildMarkArray(marks, glyphMap):
1847    """Builds a mark array subtable.
1848
1849    As part of building mark-to-* positioning rules, you will need to define
1850    a MarkArray subtable, which "defines the class and the anchor point
1851    for a mark glyph." This function builds the mark array subtable.
1852
1853    Example::
1854
1855        mark = {
1856            "acute": (0, buildAnchor(300,712)),
1857            # ...
1858        }
1859        markarray = buildMarkArray(marks, font.getReverseGlyphMap())
1860
1861    Args:
1862        marks (dict): A dictionary mapping anchors to glyphs; the keys being
1863            glyph names, and the values being a tuple of mark class number and
1864            an ``otTables.Anchor`` object representing the mark's attachment
1865            point.
1866        glyphMap: a glyph name to ID map, typically returned from
1867            ``font.getReverseGlyphMap()``.
1868
1869    Returns:
1870        An ``otTables.MarkArray`` object.
1871    """
1872    self = ot.MarkArray()
1873    self.MarkRecord = []
1874    for mark in sorted(marks.keys(), key=glyphMap.__getitem__):
1875        markClass, anchor = marks[mark]
1876        markrec = buildMarkRecord(markClass, anchor)
1877        self.MarkRecord.append(markrec)
1878    self.MarkCount = len(self.MarkRecord)
1879    return self
1880
1881
1882def buildMarkBasePos(marks, bases, glyphMap):
1883    """Build a list of MarkBasePos (GPOS4) subtables.
1884
1885    This routine turns a set of marks and bases into a list of mark-to-base
1886    positioning subtables. Currently the list will contain a single subtable
1887    containing all marks and bases, although at a later date it may return the
1888    optimal list of subtables subsetting the marks and bases into groups which
1889    save space. See :func:`buildMarkBasePosSubtable` below.
1890
1891    Note that if you are implementing a layout compiler, you may find it more
1892    flexible to use
1893    :py:class:`fontTools.otlLib.lookupBuilders.MarkBasePosBuilder` instead.
1894
1895    Example::
1896
1897        # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ...
1898
1899        marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)}
1900        bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}}
1901        markbaseposes = buildMarkBasePos(marks, bases, font.getReverseGlyphMap())
1902
1903    Args:
1904        marks (dict): A dictionary mapping anchors to glyphs; the keys being
1905            glyph names, and the values being a tuple of mark class number and
1906            an ``otTables.Anchor`` object representing the mark's attachment
1907            point. (See :func:`buildMarkArray`.)
1908        bases (dict): A dictionary mapping anchors to glyphs; the keys being
1909            glyph names, and the values being dictionaries mapping mark class ID
1910            to the appropriate ``otTables.Anchor`` object used for attaching marks
1911            of that class. (See :func:`buildBaseArray`.)
1912        glyphMap: a glyph name to ID map, typically returned from
1913            ``font.getReverseGlyphMap()``.
1914
1915    Returns:
1916        A list of ``otTables.MarkBasePos`` objects.
1917    """
1918    # TODO: Consider emitting multiple subtables to save space.
1919    # Partition the marks and bases into disjoint subsets, so that
1920    # MarkBasePos rules would only access glyphs from a single
1921    # subset. This would likely lead to smaller mark/base
1922    # matrices, so we might be able to omit many of the empty
1923    # anchor tables that we currently produce. Of course, this
1924    # would only work if the MarkBasePos rules of real-world fonts
1925    # allow partitioning into multiple subsets. We should find out
1926    # whether this is the case; if so, implement the optimization.
1927    # On the other hand, a very large number of subtables could
1928    # slow down layout engines; so this would need profiling.
1929    return [buildMarkBasePosSubtable(marks, bases, glyphMap)]
1930
1931
1932def buildMarkBasePosSubtable(marks, bases, glyphMap):
1933    """Build a single MarkBasePos (GPOS4) subtable.
1934
1935    This builds a mark-to-base lookup subtable containing all of the referenced
1936    marks and bases. See :func:`buildMarkBasePos`.
1937
1938    Args:
1939        marks (dict): A dictionary mapping anchors to glyphs; the keys being
1940            glyph names, and the values being a tuple of mark class number and
1941            an ``otTables.Anchor`` object representing the mark's attachment
1942            point. (See :func:`buildMarkArray`.)
1943        bases (dict): A dictionary mapping anchors to glyphs; the keys being
1944            glyph names, and the values being dictionaries mapping mark class ID
1945            to the appropriate ``otTables.Anchor`` object used for attaching marks
1946            of that class. (See :func:`buildBaseArray`.)
1947        glyphMap: a glyph name to ID map, typically returned from
1948            ``font.getReverseGlyphMap()``.
1949
1950    Returns:
1951        A ``otTables.MarkBasePos`` object.
1952    """
1953    self = ot.MarkBasePos()
1954    self.Format = 1
1955    self.MarkCoverage = buildCoverage(marks, glyphMap)
1956    self.MarkArray = buildMarkArray(marks, glyphMap)
1957    self.ClassCount = max([mc for mc, _ in marks.values()]) + 1
1958    self.BaseCoverage = buildCoverage(bases, glyphMap)
1959    self.BaseArray = buildBaseArray(bases, self.ClassCount, glyphMap)
1960    return self
1961
1962
1963def buildMarkLigPos(marks, ligs, glyphMap):
1964    """Build a list of MarkLigPos (GPOS5) subtables.
1965
1966    This routine turns a set of marks and ligatures into a list of mark-to-ligature
1967    positioning subtables. Currently the list will contain a single subtable
1968    containing all marks and ligatures, although at a later date it may return
1969    the optimal list of subtables subsetting the marks and ligatures into groups
1970    which save space. See :func:`buildMarkLigPosSubtable` below.
1971
1972    Note that if you are implementing a layout compiler, you may find it more
1973    flexible to use
1974    :py:class:`fontTools.otlLib.lookupBuilders.MarkLigPosBuilder` instead.
1975
1976    Example::
1977
1978        # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ...
1979        marks = {
1980            "acute": (0, a1),
1981            "grave": (0, a1),
1982            "cedilla": (1, a2)
1983        }
1984        ligs = {
1985            "f_i": [
1986                { 0: a3, 1: a5 }, # f
1987                { 0: a4, 1: a5 }  # i
1988                ],
1989        #   "c_t": [{...}, {...}]
1990        }
1991        markligposes = buildMarkLigPos(marks, ligs,
1992            font.getReverseGlyphMap())
1993
1994    Args:
1995        marks (dict): A dictionary mapping anchors to glyphs; the keys being
1996            glyph names, and the values being a tuple of mark class number and
1997            an ``otTables.Anchor`` object representing the mark's attachment
1998            point. (See :func:`buildMarkArray`.)
1999        ligs (dict): A mapping of ligature names to an array of dictionaries:
2000            for each component glyph in the ligature, an dictionary mapping
2001            mark class IDs to anchors. (See :func:`buildLigatureArray`.)
2002        glyphMap: a glyph name to ID map, typically returned from
2003            ``font.getReverseGlyphMap()``.
2004
2005    Returns:
2006        A list of ``otTables.MarkLigPos`` objects.
2007
2008    """
2009    # TODO: Consider splitting into multiple subtables to save space,
2010    # as with MarkBasePos, this would be a trade-off that would need
2011    # profiling. And, depending on how typical fonts are structured,
2012    # it might not be worth doing at all.
2013    return [buildMarkLigPosSubtable(marks, ligs, glyphMap)]
2014
2015
2016def buildMarkLigPosSubtable(marks, ligs, glyphMap):
2017    """Build a single MarkLigPos (GPOS5) subtable.
2018
2019    This builds a mark-to-base lookup subtable containing all of the referenced
2020    marks and bases. See :func:`buildMarkLigPos`.
2021
2022    Args:
2023        marks (dict): A dictionary mapping anchors to glyphs; the keys being
2024            glyph names, and the values being a tuple of mark class number and
2025            an ``otTables.Anchor`` object representing the mark's attachment
2026            point. (See :func:`buildMarkArray`.)
2027        ligs (dict): A mapping of ligature names to an array of dictionaries:
2028            for each component glyph in the ligature, an dictionary mapping
2029            mark class IDs to anchors. (See :func:`buildLigatureArray`.)
2030        glyphMap: a glyph name to ID map, typically returned from
2031            ``font.getReverseGlyphMap()``.
2032
2033    Returns:
2034        A ``otTables.MarkLigPos`` object.
2035    """
2036    self = ot.MarkLigPos()
2037    self.Format = 1
2038    self.MarkCoverage = buildCoverage(marks, glyphMap)
2039    self.MarkArray = buildMarkArray(marks, glyphMap)
2040    self.ClassCount = max([mc for mc, _ in marks.values()]) + 1
2041    self.LigatureCoverage = buildCoverage(ligs, glyphMap)
2042    self.LigatureArray = buildLigatureArray(ligs, self.ClassCount, glyphMap)
2043    return self
2044
2045
2046def buildMarkRecord(classID, anchor):
2047    assert isinstance(classID, int)
2048    assert isinstance(anchor, ot.Anchor)
2049    self = ot.MarkRecord()
2050    self.Class = classID
2051    self.MarkAnchor = anchor
2052    return self
2053
2054
2055def buildMark2Record(anchors):
2056    # [otTables.Anchor, otTables.Anchor, ...] --> otTables.Mark2Record
2057    self = ot.Mark2Record()
2058    self.Mark2Anchor = anchors
2059    return self
2060
2061
2062def _getValueFormat(f, values, i):
2063    # Helper for buildPairPos{Glyphs|Classes}Subtable.
2064    if f is not None:
2065        return f
2066    mask = 0
2067    for value in values:
2068        if value is not None and value[i] is not None:
2069            mask |= value[i].getFormat()
2070    return mask
2071
2072
2073def buildPairPosClassesSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None):
2074    """Builds a class pair adjustment (GPOS2 format 2) subtable.
2075
2076    Kerning tables are generally expressed as pair positioning tables using
2077    class-based pair adjustments. This routine builds format 2 PairPos
2078    subtables.
2079
2080    Note that if you are implementing a layout compiler, you may find it more
2081    flexible to use
2082    :py:class:`fontTools.otlLib.lookupBuilders.ClassPairPosSubtableBuilder`
2083    instead, as this takes care of ensuring that the supplied pairs can be
2084    formed into non-overlapping classes and emitting individual subtables
2085    whenever the non-overlapping requirement means that a new subtable is
2086    required.
2087
2088    Example::
2089
2090        pairs = {}
2091
2092        pairs[(
2093            [ "K", "X" ],
2094            [ "W", "V" ]
2095        )] = ( buildValue(xAdvance=+5), buildValue() )
2096        # pairs[(... , ...)] = (..., ...)
2097
2098        pairpos = buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap())
2099
2100    Args:
2101        pairs (dict): Pair positioning data; the keys being a two-element
2102            tuple of lists of glyphnames, and the values being a two-element
2103            tuple of ``otTables.ValueRecord`` objects.
2104        glyphMap: a glyph name to ID map, typically returned from
2105            ``font.getReverseGlyphMap()``.
2106        valueFormat1: Force the "left" value records to the given format.
2107        valueFormat2: Force the "right" value records to the given format.
2108
2109    Returns:
2110        A ``otTables.PairPos`` object.
2111    """
2112    coverage = set()
2113    classDef1 = ClassDefBuilder(useClass0=True)
2114    classDef2 = ClassDefBuilder(useClass0=False)
2115    for gc1, gc2 in sorted(pairs):
2116        coverage.update(gc1)
2117        classDef1.add(gc1)
2118        classDef2.add(gc2)
2119    self = ot.PairPos()
2120    self.Format = 2
2121    valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0)
2122    valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1)
2123    self.Coverage = buildCoverage(coverage, glyphMap)
2124    self.ClassDef1 = classDef1.build()
2125    self.ClassDef2 = classDef2.build()
2126    classes1 = classDef1.classes()
2127    classes2 = classDef2.classes()
2128    self.Class1Record = []
2129    for c1 in classes1:
2130        rec1 = ot.Class1Record()
2131        rec1.Class2Record = []
2132        self.Class1Record.append(rec1)
2133        for c2 in classes2:
2134            rec2 = ot.Class2Record()
2135            val1, val2 = pairs.get((c1, c2), (None, None))
2136            rec2.Value1 = (
2137                ValueRecord(src=val1, valueFormat=valueFormat1)
2138                if valueFormat1
2139                else None
2140            )
2141            rec2.Value2 = (
2142                ValueRecord(src=val2, valueFormat=valueFormat2)
2143                if valueFormat2
2144                else None
2145            )
2146            rec1.Class2Record.append(rec2)
2147    self.Class1Count = len(self.Class1Record)
2148    self.Class2Count = len(classes2)
2149    return self
2150
2151
2152def buildPairPosGlyphs(pairs, glyphMap):
2153    """Builds a list of glyph-based pair adjustment (GPOS2 format 1) subtables.
2154
2155    This organises a list of pair positioning adjustments into subtables based
2156    on common value record formats.
2157
2158    Note that if you are implementing a layout compiler, you may find it more
2159    flexible to use
2160    :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder`
2161    instead.
2162
2163    Example::
2164
2165        pairs = {
2166            ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ),
2167            ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ),
2168            # ...
2169        }
2170
2171        subtables = buildPairPosGlyphs(pairs, font.getReverseGlyphMap())
2172
2173    Args:
2174        pairs (dict): Pair positioning data; the keys being a two-element
2175            tuple of glyphnames, and the values being a two-element
2176            tuple of ``otTables.ValueRecord`` objects.
2177        glyphMap: a glyph name to ID map, typically returned from
2178            ``font.getReverseGlyphMap()``.
2179
2180    Returns:
2181        A list of ``otTables.PairPos`` objects.
2182    """
2183
2184    p = {}  # (formatA, formatB) --> {(glyphA, glyphB): (valA, valB)}
2185    for (glyphA, glyphB), (valA, valB) in pairs.items():
2186        formatA = valA.getFormat() if valA is not None else 0
2187        formatB = valB.getFormat() if valB is not None else 0
2188        pos = p.setdefault((formatA, formatB), {})
2189        pos[(glyphA, glyphB)] = (valA, valB)
2190    return [
2191        buildPairPosGlyphsSubtable(pos, glyphMap, formatA, formatB)
2192        for ((formatA, formatB), pos) in sorted(p.items())
2193    ]
2194
2195
2196def buildPairPosGlyphsSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None):
2197    """Builds a single glyph-based pair adjustment (GPOS2 format 1) subtable.
2198
2199    This builds a PairPos subtable from a dictionary of glyph pairs and
2200    their positioning adjustments. See also :func:`buildPairPosGlyphs`.
2201
2202    Note that if you are implementing a layout compiler, you may find it more
2203    flexible to use
2204    :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder` instead.
2205
2206    Example::
2207
2208        pairs = {
2209            ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ),
2210            ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ),
2211            # ...
2212        }
2213
2214        pairpos = buildPairPosGlyphsSubtable(pairs, font.getReverseGlyphMap())
2215
2216    Args:
2217        pairs (dict): Pair positioning data; the keys being a two-element
2218            tuple of glyphnames, and the values being a two-element
2219            tuple of ``otTables.ValueRecord`` objects.
2220        glyphMap: a glyph name to ID map, typically returned from
2221            ``font.getReverseGlyphMap()``.
2222        valueFormat1: Force the "left" value records to the given format.
2223        valueFormat2: Force the "right" value records to the given format.
2224
2225    Returns:
2226        A ``otTables.PairPos`` object.
2227    """
2228    self = ot.PairPos()
2229    self.Format = 1
2230    valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0)
2231    valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1)
2232    p = {}
2233    for (glyphA, glyphB), (valA, valB) in pairs.items():
2234        p.setdefault(glyphA, []).append((glyphB, valA, valB))
2235    self.Coverage = buildCoverage({g for g, _ in pairs.keys()}, glyphMap)
2236    self.PairSet = []
2237    for glyph in self.Coverage.glyphs:
2238        ps = ot.PairSet()
2239        ps.PairValueRecord = []
2240        self.PairSet.append(ps)
2241        for glyph2, val1, val2 in sorted(p[glyph], key=lambda x: glyphMap[x[0]]):
2242            pvr = ot.PairValueRecord()
2243            pvr.SecondGlyph = glyph2
2244            pvr.Value1 = (
2245                ValueRecord(src=val1, valueFormat=valueFormat1)
2246                if valueFormat1
2247                else None
2248            )
2249            pvr.Value2 = (
2250                ValueRecord(src=val2, valueFormat=valueFormat2)
2251                if valueFormat2
2252                else None
2253            )
2254            ps.PairValueRecord.append(pvr)
2255        ps.PairValueCount = len(ps.PairValueRecord)
2256    self.PairSetCount = len(self.PairSet)
2257    return self
2258
2259
2260def buildSinglePos(mapping, glyphMap):
2261    """Builds a list of single adjustment (GPOS1) subtables.
2262
2263    This builds a list of SinglePos subtables from a dictionary of glyph
2264    names and their positioning adjustments. The format of the subtables are
2265    determined to optimize the size of the resulting subtables.
2266    See also :func:`buildSinglePosSubtable`.
2267
2268    Note that if you are implementing a layout compiler, you may find it more
2269    flexible to use
2270    :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead.
2271
2272    Example::
2273
2274        mapping = {
2275            "V": buildValue({ "xAdvance" : +5 }),
2276            # ...
2277        }
2278
2279        subtables = buildSinglePos(pairs, font.getReverseGlyphMap())
2280
2281    Args:
2282        mapping (dict): A mapping between glyphnames and
2283            ``otTables.ValueRecord`` objects.
2284        glyphMap: a glyph name to ID map, typically returned from
2285            ``font.getReverseGlyphMap()``.
2286
2287    Returns:
2288        A list of ``otTables.SinglePos`` objects.
2289    """
2290    result, handled = [], set()
2291    # In SinglePos format 1, the covered glyphs all share the same ValueRecord.
2292    # In format 2, each glyph has its own ValueRecord, but these records
2293    # all have the same properties (eg., all have an X but no Y placement).
2294    coverages, masks, values = {}, {}, {}
2295    for glyph, value in mapping.items():
2296        key = _getSinglePosValueKey(value)
2297        coverages.setdefault(key, []).append(glyph)
2298        masks.setdefault(key[0], []).append(key)
2299        values[key] = value
2300
2301    # If a ValueRecord is shared between multiple glyphs, we generate
2302    # a SinglePos format 1 subtable; that is the most compact form.
2303    for key, glyphs in coverages.items():
2304        # 5 ushorts is the length of introducing another sublookup
2305        if len(glyphs) * _getSinglePosValueSize(key) > 5:
2306            format1Mapping = {g: values[key] for g in glyphs}
2307            result.append(buildSinglePosSubtable(format1Mapping, glyphMap))
2308            handled.add(key)
2309
2310    # In the remaining ValueRecords, look for those whose valueFormat
2311    # (the set of used properties) is shared between multiple records.
2312    # These will get encoded in format 2.
2313    for valueFormat, keys in masks.items():
2314        f2 = [k for k in keys if k not in handled]
2315        if len(f2) > 1:
2316            format2Mapping = {}
2317            for k in f2:
2318                format2Mapping.update((g, values[k]) for g in coverages[k])
2319            result.append(buildSinglePosSubtable(format2Mapping, glyphMap))
2320            handled.update(f2)
2321
2322    # The remaining ValueRecords are only used by a few glyphs, normally
2323    # one. We encode these in format 1 again.
2324    for key, glyphs in coverages.items():
2325        if key not in handled:
2326            for g in glyphs:
2327                st = buildSinglePosSubtable({g: values[key]}, glyphMap)
2328            result.append(st)
2329
2330    # When the OpenType layout engine traverses the subtables, it will
2331    # stop after the first matching subtable.  Therefore, we sort the
2332    # resulting subtables by decreasing coverage size; this increases
2333    # the chance that the layout engine can do an early exit. (Of course,
2334    # this would only be true if all glyphs were equally frequent, which
2335    # is not really the case; but we do not know their distribution).
2336    # If two subtables cover the same number of glyphs, we sort them
2337    # by glyph ID so that our output is deterministic.
2338    result.sort(key=lambda t: _getSinglePosTableKey(t, glyphMap))
2339    return result
2340
2341
2342def buildSinglePosSubtable(values, glyphMap):
2343    """Builds a single adjustment (GPOS1) subtable.
2344
2345    This builds a list of SinglePos subtables from a dictionary of glyph
2346    names and their positioning adjustments. The format of the subtable is
2347    determined to optimize the size of the output.
2348    See also :func:`buildSinglePos`.
2349
2350    Note that if you are implementing a layout compiler, you may find it more
2351    flexible to use
2352    :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead.
2353
2354    Example::
2355
2356        mapping = {
2357            "V": buildValue({ "xAdvance" : +5 }),
2358            # ...
2359        }
2360
2361        subtable = buildSinglePos(pairs, font.getReverseGlyphMap())
2362
2363    Args:
2364        mapping (dict): A mapping between glyphnames and
2365            ``otTables.ValueRecord`` objects.
2366        glyphMap: a glyph name to ID map, typically returned from
2367            ``font.getReverseGlyphMap()``.
2368
2369    Returns:
2370        A ``otTables.SinglePos`` object.
2371    """
2372    self = ot.SinglePos()
2373    self.Coverage = buildCoverage(values.keys(), glyphMap)
2374    valueFormat = self.ValueFormat = reduce(
2375        int.__or__, [v.getFormat() for v in values.values()], 0
2376    )
2377    valueRecords = [
2378        ValueRecord(src=values[g], valueFormat=valueFormat)
2379        for g in self.Coverage.glyphs
2380    ]
2381    if all(v == valueRecords[0] for v in valueRecords):
2382        self.Format = 1
2383        if self.ValueFormat != 0:
2384            self.Value = valueRecords[0]
2385        else:
2386            self.Value = None
2387    else:
2388        self.Format = 2
2389        self.Value = valueRecords
2390        self.ValueCount = len(self.Value)
2391    return self
2392
2393
2394def _getSinglePosTableKey(subtable, glyphMap):
2395    assert isinstance(subtable, ot.SinglePos), subtable
2396    glyphs = subtable.Coverage.glyphs
2397    return (-len(glyphs), glyphMap[glyphs[0]])
2398
2399
2400def _getSinglePosValueKey(valueRecord):
2401    # otBase.ValueRecord --> (2, ("YPlacement": 12))
2402    assert isinstance(valueRecord, ValueRecord), valueRecord
2403    valueFormat, result = 0, []
2404    for name, value in valueRecord.__dict__.items():
2405        if isinstance(value, ot.Device):
2406            result.append((name, _makeDeviceTuple(value)))
2407        else:
2408            result.append((name, value))
2409        valueFormat |= valueRecordFormatDict[name][0]
2410    result.sort()
2411    result.insert(0, valueFormat)
2412    return tuple(result)
2413
2414
2415_DeviceTuple = namedtuple("_DeviceTuple", "DeltaFormat StartSize EndSize DeltaValue")
2416
2417
2418def _makeDeviceTuple(device):
2419    # otTables.Device --> tuple, for making device tables unique
2420    return _DeviceTuple(
2421        device.DeltaFormat,
2422        device.StartSize,
2423        device.EndSize,
2424        () if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue),
2425    )
2426
2427
2428def _getSinglePosValueSize(valueKey):
2429    # Returns how many ushorts this valueKey (short form of ValueRecord) takes up
2430    count = 0
2431    for _, v in valueKey[1:]:
2432        if isinstance(v, _DeviceTuple):
2433            count += len(v.DeltaValue) + 3
2434        else:
2435            count += 1
2436    return count
2437
2438
2439def buildValue(value):
2440    """Builds a positioning value record.
2441
2442    Value records are used to specify coordinates and adjustments for
2443    positioning and attaching glyphs. Many of the positioning functions
2444    in this library take ``otTables.ValueRecord`` objects as arguments.
2445    This function builds value records from dictionaries.
2446
2447    Args:
2448        value (dict): A dictionary with zero or more of the following keys:
2449            - ``xPlacement``
2450            - ``yPlacement``
2451            - ``xAdvance``
2452            - ``yAdvance``
2453            - ``xPlaDevice``
2454            - ``yPlaDevice``
2455            - ``xAdvDevice``
2456            - ``yAdvDevice``
2457
2458    Returns:
2459        An ``otTables.ValueRecord`` object.
2460    """
2461    self = ValueRecord()
2462    for k, v in value.items():
2463        setattr(self, k, v)
2464    return self
2465
2466
2467# GDEF
2468
2469
2470def buildAttachList(attachPoints, glyphMap):
2471    """Builds an AttachList subtable.
2472
2473    A GDEF table may contain an Attachment Point List table (AttachList)
2474    which stores the contour indices of attachment points for glyphs with
2475    attachment points. This routine builds AttachList subtables.
2476
2477    Args:
2478        attachPoints (dict): A mapping between glyph names and a list of
2479            contour indices.
2480
2481    Returns:
2482        An ``otTables.AttachList`` object if attachment points are supplied,
2483            or ``None`` otherwise.
2484    """
2485    if not attachPoints:
2486        return None
2487    self = ot.AttachList()
2488    self.Coverage = buildCoverage(attachPoints.keys(), glyphMap)
2489    self.AttachPoint = [buildAttachPoint(attachPoints[g]) for g in self.Coverage.glyphs]
2490    self.GlyphCount = len(self.AttachPoint)
2491    return self
2492
2493
2494def buildAttachPoint(points):
2495    # [4, 23, 41] --> otTables.AttachPoint
2496    # Only used by above.
2497    if not points:
2498        return None
2499    self = ot.AttachPoint()
2500    self.PointIndex = sorted(set(points))
2501    self.PointCount = len(self.PointIndex)
2502    return self
2503
2504
2505def buildCaretValueForCoord(coord):
2506    # 500 --> otTables.CaretValue, format 1
2507    # (500, DeviceTable) --> otTables.CaretValue, format 3
2508    self = ot.CaretValue()
2509    if isinstance(coord, tuple):
2510        self.Format = 3
2511        self.Coordinate, self.DeviceTable = coord
2512    else:
2513        self.Format = 1
2514        self.Coordinate = coord
2515    return self
2516
2517
2518def buildCaretValueForPoint(point):
2519    # 4 --> otTables.CaretValue, format 2
2520    self = ot.CaretValue()
2521    self.Format = 2
2522    self.CaretValuePoint = point
2523    return self
2524
2525
2526def buildLigCaretList(coords, points, glyphMap):
2527    """Builds a ligature caret list table.
2528
2529    Ligatures appear as a single glyph representing multiple characters; however
2530    when, for example, editing text containing a ``f_i`` ligature, the user may
2531    want to place the cursor between the ``f`` and the ``i``. The ligature caret
2532    list in the GDEF table specifies the position to display the "caret" (the
2533    character insertion indicator, typically a flashing vertical bar) "inside"
2534    the ligature to represent an insertion point. The insertion positions may
2535    be specified either by coordinate or by contour point.
2536
2537    Example::
2538
2539        coords = {
2540            "f_f_i": [300, 600] # f|fi cursor at 300 units, ff|i cursor at 600.
2541        }
2542        points = {
2543            "c_t": [28] # c|t cursor appears at coordinate of contour point 28.
2544        }
2545        ligcaretlist = buildLigCaretList(coords, points, font.getReverseGlyphMap())
2546
2547    Args:
2548        coords: A mapping between glyph names and a list of coordinates for
2549            the insertion point of each ligature component after the first one.
2550        points: A mapping between glyph names and a list of contour points for
2551            the insertion point of each ligature component after the first one.
2552        glyphMap: a glyph name to ID map, typically returned from
2553            ``font.getReverseGlyphMap()``.
2554
2555    Returns:
2556        A ``otTables.LigCaretList`` object if any carets are present, or
2557            ``None`` otherwise."""
2558    glyphs = set(coords.keys()) if coords else set()
2559    if points:
2560        glyphs.update(points.keys())
2561    carets = {g: buildLigGlyph(coords.get(g), points.get(g)) for g in glyphs}
2562    carets = {g: c for g, c in carets.items() if c is not None}
2563    if not carets:
2564        return None
2565    self = ot.LigCaretList()
2566    self.Coverage = buildCoverage(carets.keys(), glyphMap)
2567    self.LigGlyph = [carets[g] for g in self.Coverage.glyphs]
2568    self.LigGlyphCount = len(self.LigGlyph)
2569    return self
2570
2571
2572def buildLigGlyph(coords, points):
2573    # ([500], [4]) --> otTables.LigGlyph; None for empty coords/points
2574    carets = []
2575    if coords:
2576        coords = sorted(coords, key=lambda c: c[0] if isinstance(c, tuple) else c)
2577        carets.extend([buildCaretValueForCoord(c) for c in coords])
2578    if points:
2579        carets.extend([buildCaretValueForPoint(p) for p in sorted(points)])
2580    if not carets:
2581        return None
2582    self = ot.LigGlyph()
2583    self.CaretValue = carets
2584    self.CaretCount = len(self.CaretValue)
2585    return self
2586
2587
2588def buildMarkGlyphSetsDef(markSets, glyphMap):
2589    """Builds a mark glyph sets definition table.
2590
2591    OpenType Layout lookups may choose to use mark filtering sets to consider
2592    or ignore particular combinations of marks. These sets are specified by
2593    setting a flag on the lookup, but the mark filtering sets are defined in
2594    the ``GDEF`` table. This routine builds the subtable containing the mark
2595    glyph set definitions.
2596
2597    Example::
2598
2599        set0 = set("acute", "grave")
2600        set1 = set("caron", "grave")
2601
2602        markglyphsets = buildMarkGlyphSetsDef([set0, set1], font.getReverseGlyphMap())
2603
2604    Args:
2605
2606        markSets: A list of sets of glyphnames.
2607        glyphMap: a glyph name to ID map, typically returned from
2608            ``font.getReverseGlyphMap()``.
2609
2610    Returns
2611        An ``otTables.MarkGlyphSetsDef`` object.
2612    """
2613    if not markSets:
2614        return None
2615    self = ot.MarkGlyphSetsDef()
2616    self.MarkSetTableFormat = 1
2617    self.Coverage = [buildCoverage(m, glyphMap) for m in markSets]
2618    self.MarkSetCount = len(self.Coverage)
2619    return self
2620
2621
2622class ClassDefBuilder(object):
2623    """Helper for building ClassDef tables."""
2624
2625    def __init__(self, useClass0):
2626        self.classes_ = set()
2627        self.glyphs_ = {}
2628        self.useClass0_ = useClass0
2629
2630    def canAdd(self, glyphs):
2631        if isinstance(glyphs, (set, frozenset)):
2632            glyphs = sorted(glyphs)
2633        glyphs = tuple(glyphs)
2634        if glyphs in self.classes_:
2635            return True
2636        for glyph in glyphs:
2637            if glyph in self.glyphs_:
2638                return False
2639        return True
2640
2641    def add(self, glyphs):
2642        if isinstance(glyphs, (set, frozenset)):
2643            glyphs = sorted(glyphs)
2644        glyphs = tuple(glyphs)
2645        if glyphs in self.classes_:
2646            return
2647        self.classes_.add(glyphs)
2648        for glyph in glyphs:
2649            if glyph in self.glyphs_:
2650                raise OpenTypeLibError(
2651                    f"Glyph {glyph} is already present in class.", None
2652                )
2653            self.glyphs_[glyph] = glyphs
2654
2655    def classes(self):
2656        # In ClassDef1 tables, class id #0 does not need to be encoded
2657        # because zero is the default. Therefore, we use id #0 for the
2658        # glyph class that has the largest number of members. However,
2659        # in other tables than ClassDef1, 0 means "every other glyph"
2660        # so we should not use that ID for any real glyph classes;
2661        # we implement this by inserting an empty set at position 0.
2662        #
2663        # TODO: Instead of counting the number of glyphs in each class,
2664        # we should determine the encoded size. If the glyphs in a large
2665        # class form a contiguous range, the encoding is actually quite
2666        # compact, whereas a non-contiguous set might need a lot of bytes
2667        # in the output file. We don't get this right with the key below.
2668        result = sorted(self.classes_, key=lambda s: (-len(s), s))
2669        if not self.useClass0_:
2670            result.insert(0, frozenset())
2671        return result
2672
2673    def build(self):
2674        glyphClasses = {}
2675        for classID, glyphs in enumerate(self.classes()):
2676            if classID == 0:
2677                continue
2678            for glyph in glyphs:
2679                glyphClasses[glyph] = classID
2680        classDef = ot.ClassDef()
2681        classDef.classDefs = glyphClasses
2682        return classDef
2683
2684
2685AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16)
2686AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16)
2687
2688
2689def buildStatTable(
2690    ttFont, axes, locations=None, elidedFallbackName=2, windowsNames=True, macNames=True
2691):
2692    """Add a 'STAT' table to 'ttFont'.
2693
2694    'axes' is a list of dictionaries describing axes and their
2695    values.
2696
2697    Example::
2698
2699        axes = [
2700            dict(
2701                tag="wght",
2702                name="Weight",
2703                ordering=0,  # optional
2704                values=[
2705                    dict(value=100, name='Thin'),
2706                    dict(value=300, name='Light'),
2707                    dict(value=400, name='Regular', flags=0x2),
2708                    dict(value=900, name='Black'),
2709                ],
2710            )
2711        ]
2712
2713    Each axis dict must have 'tag' and 'name' items. 'tag' maps
2714    to the 'AxisTag' field. 'name' can be a name ID (int), a string,
2715    or a dictionary containing multilingual names (see the
2716    addMultilingualName() name table method), and will translate to
2717    the AxisNameID field.
2718
2719    An axis dict may contain an 'ordering' item that maps to the
2720    AxisOrdering field. If omitted, the order of the axes list is
2721    used to calculate AxisOrdering fields.
2722
2723    The axis dict may contain a 'values' item, which is a list of
2724    dictionaries describing AxisValue records belonging to this axis.
2725
2726    Each value dict must have a 'name' item, which can be a name ID
2727    (int), a string, or a dictionary containing multilingual names,
2728    like the axis name. It translates to the ValueNameID field.
2729
2730    Optionally the value dict can contain a 'flags' item. It maps to
2731    the AxisValue Flags field, and will be 0 when omitted.
2732
2733    The format of the AxisValue is determined by the remaining contents
2734    of the value dictionary:
2735
2736    If the value dict contains a 'value' item, an AxisValue record
2737    Format 1 is created. If in addition to the 'value' item it contains
2738    a 'linkedValue' item, an AxisValue record Format 3 is built.
2739
2740    If the value dict contains a 'nominalValue' item, an AxisValue
2741    record Format 2 is built. Optionally it may contain 'rangeMinValue'
2742    and 'rangeMaxValue' items. These map to -Infinity and +Infinity
2743    respectively if omitted.
2744
2745    You cannot specify Format 4 AxisValue tables this way, as they are
2746    not tied to a single axis, and specify a name for a location that
2747    is defined by multiple axes values. Instead, you need to supply the
2748    'locations' argument.
2749
2750    The optional 'locations' argument specifies AxisValue Format 4
2751    tables. It should be a list of dicts, where each dict has a 'name'
2752    item, which works just like the value dicts above, an optional
2753    'flags' item (defaulting to 0x0), and a 'location' dict. A
2754    location dict key is an axis tag, and the associated value is the
2755    location on the specified axis. They map to the AxisIndex and Value
2756    fields of the AxisValueRecord.
2757
2758    Example::
2759
2760        locations = [
2761            dict(name='Regular ABCD', location=dict(wght=300, ABCD=100)),
2762            dict(name='Bold ABCD XYZ', location=dict(wght=600, ABCD=200)),
2763        ]
2764
2765    The optional 'elidedFallbackName' argument can be a name ID (int),
2766    a string, a dictionary containing multilingual names, or a list of
2767    STATNameStatements. It translates to the ElidedFallbackNameID field.
2768
2769    The 'ttFont' argument must be a TTFont instance that already has a
2770    'name' table. If a 'STAT' table already exists, it will be
2771    overwritten by the newly created one.
2772    """
2773    ttFont["STAT"] = ttLib.newTable("STAT")
2774    statTable = ttFont["STAT"].table = ot.STAT()
2775    statTable.ElidedFallbackNameID = _addName(
2776        ttFont, elidedFallbackName, windows=windowsNames, mac=macNames
2777    )
2778
2779    # 'locations' contains data for AxisValue Format 4
2780    axisRecords, axisValues = _buildAxisRecords(
2781        axes, ttFont, windowsNames=windowsNames, macNames=macNames
2782    )
2783    if not locations:
2784        statTable.Version = 0x00010001
2785    else:
2786        # We'll be adding Format 4 AxisValue records, which
2787        # requires a higher table version
2788        statTable.Version = 0x00010002
2789        multiAxisValues = _buildAxisValuesFormat4(
2790            locations, axes, ttFont, windowsNames=windowsNames, macNames=macNames
2791        )
2792        axisValues = multiAxisValues + axisValues
2793    ttFont["name"].names.sort()
2794
2795    # Store AxisRecords
2796    axisRecordArray = ot.AxisRecordArray()
2797    axisRecordArray.Axis = axisRecords
2798    # XXX these should not be hard-coded but computed automatically
2799    statTable.DesignAxisRecordSize = 8
2800    statTable.DesignAxisRecord = axisRecordArray
2801    statTable.DesignAxisCount = len(axisRecords)
2802
2803    statTable.AxisValueCount = 0
2804    statTable.AxisValueArray = None
2805    if axisValues:
2806        # Store AxisValueRecords
2807        axisValueArray = ot.AxisValueArray()
2808        axisValueArray.AxisValue = axisValues
2809        statTable.AxisValueArray = axisValueArray
2810        statTable.AxisValueCount = len(axisValues)
2811
2812
2813def _buildAxisRecords(axes, ttFont, windowsNames=True, macNames=True):
2814    axisRecords = []
2815    axisValues = []
2816    for axisRecordIndex, axisDict in enumerate(axes):
2817        axis = ot.AxisRecord()
2818        axis.AxisTag = axisDict["tag"]
2819        axis.AxisNameID = _addName(
2820            ttFont, axisDict["name"], 256, windows=windowsNames, mac=macNames
2821        )
2822        axis.AxisOrdering = axisDict.get("ordering", axisRecordIndex)
2823        axisRecords.append(axis)
2824
2825        for axisVal in axisDict.get("values", ()):
2826            axisValRec = ot.AxisValue()
2827            axisValRec.AxisIndex = axisRecordIndex
2828            axisValRec.Flags = axisVal.get("flags", 0)
2829            axisValRec.ValueNameID = _addName(
2830                ttFont, axisVal["name"], windows=windowsNames, mac=macNames
2831            )
2832
2833            if "value" in axisVal:
2834                axisValRec.Value = axisVal["value"]
2835                if "linkedValue" in axisVal:
2836                    axisValRec.Format = 3
2837                    axisValRec.LinkedValue = axisVal["linkedValue"]
2838                else:
2839                    axisValRec.Format = 1
2840            elif "nominalValue" in axisVal:
2841                axisValRec.Format = 2
2842                axisValRec.NominalValue = axisVal["nominalValue"]
2843                axisValRec.RangeMinValue = axisVal.get(
2844                    "rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY
2845                )
2846                axisValRec.RangeMaxValue = axisVal.get(
2847                    "rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY
2848                )
2849            else:
2850                raise ValueError("Can't determine format for AxisValue")
2851
2852            axisValues.append(axisValRec)
2853    return axisRecords, axisValues
2854
2855
2856def _buildAxisValuesFormat4(locations, axes, ttFont, windowsNames=True, macNames=True):
2857    axisTagToIndex = {}
2858    for axisRecordIndex, axisDict in enumerate(axes):
2859        axisTagToIndex[axisDict["tag"]] = axisRecordIndex
2860
2861    axisValues = []
2862    for axisLocationDict in locations:
2863        axisValRec = ot.AxisValue()
2864        axisValRec.Format = 4
2865        axisValRec.ValueNameID = _addName(
2866            ttFont, axisLocationDict["name"], windows=windowsNames, mac=macNames
2867        )
2868        axisValRec.Flags = axisLocationDict.get("flags", 0)
2869        axisValueRecords = []
2870        for tag, value in axisLocationDict["location"].items():
2871            avr = ot.AxisValueRecord()
2872            avr.AxisIndex = axisTagToIndex[tag]
2873            avr.Value = value
2874            axisValueRecords.append(avr)
2875        axisValueRecords.sort(key=lambda avr: avr.AxisIndex)
2876        axisValRec.AxisCount = len(axisValueRecords)
2877        axisValRec.AxisValueRecord = axisValueRecords
2878        axisValues.append(axisValRec)
2879    return axisValues
2880
2881
2882def _addName(ttFont, value, minNameID=0, windows=True, mac=True):
2883    nameTable = ttFont["name"]
2884    if isinstance(value, int):
2885        # Already a nameID
2886        return value
2887    if isinstance(value, str):
2888        names = dict(en=value)
2889    elif isinstance(value, dict):
2890        names = value
2891    elif isinstance(value, list):
2892        nameID = nameTable._findUnusedNameID()
2893        for nameRecord in value:
2894            if isinstance(nameRecord, STATNameStatement):
2895                nameTable.setName(
2896                    nameRecord.string,
2897                    nameID,
2898                    nameRecord.platformID,
2899                    nameRecord.platEncID,
2900                    nameRecord.langID,
2901                )
2902            else:
2903                raise TypeError("value must be a list of STATNameStatements")
2904        return nameID
2905    else:
2906        raise TypeError("value must be int, str, dict or list")
2907    return nameTable.addMultilingualName(
2908        names, ttFont=ttFont, windows=windows, mac=mac, minNameID=minNameID
2909    )
2910
2911
2912def buildMathTable(
2913    ttFont,
2914    constants=None,
2915    italicsCorrections=None,
2916    topAccentAttachments=None,
2917    extendedShapes=None,
2918    mathKerns=None,
2919    minConnectorOverlap=0,
2920    vertGlyphVariants=None,
2921    horizGlyphVariants=None,
2922    vertGlyphAssembly=None,
2923    horizGlyphAssembly=None,
2924):
2925    """
2926    Add a 'MATH' table to 'ttFont'.
2927
2928    'constants' is a dictionary of math constants. The keys are the constant
2929    names from the MATH table specification (with capital first letter), and the
2930    values are the constant values as numbers.
2931
2932    'italicsCorrections' is a dictionary of italic corrections. The keys are the
2933    glyph names, and the values are the italic corrections as numbers.
2934
2935    'topAccentAttachments' is a dictionary of top accent attachments. The keys
2936    are the glyph names, and the values are the top accent horizontal positions
2937    as numbers.
2938
2939    'extendedShapes' is a set of extended shape glyphs.
2940
2941    'mathKerns' is a dictionary of math kerns. The keys are the glyph names, and
2942    the values are dictionaries. The keys of these dictionaries are the side
2943    names ('TopRight', 'TopLeft', 'BottomRight', 'BottomLeft'), and the values
2944    are tuples of two lists. The first list contains the correction heights as
2945    numbers, and the second list contains the kern values as numbers.
2946
2947    'minConnectorOverlap' is the minimum connector overlap as a number.
2948
2949    'vertGlyphVariants' is a dictionary of vertical glyph variants. The keys are
2950    the glyph names, and the values are tuples of glyph name and full advance height.
2951
2952    'horizGlyphVariants' is a dictionary of horizontal glyph variants. The keys
2953    are the glyph names, and the values are tuples of glyph name and full
2954    advance width.
2955
2956    'vertGlyphAssembly' is a dictionary of vertical glyph assemblies. The keys
2957    are the glyph names, and the values are tuples of assembly parts and italics
2958    correction. The assembly parts are tuples of glyph name, flags, start
2959    connector length, end connector length, and full advance height.
2960
2961    'horizGlyphAssembly' is a dictionary of horizontal glyph assemblies. The
2962    keys are the glyph names, and the values are tuples of assembly parts
2963    and italics correction. The assembly parts are tuples of glyph name, flags,
2964    start connector length, end connector length, and full advance width.
2965
2966    Where a number is expected, an integer or a float can be used. The floats
2967    will be rounded.
2968
2969    Example::
2970
2971        constants = {
2972            "ScriptPercentScaleDown": 70,
2973            "ScriptScriptPercentScaleDown": 50,
2974            "DelimitedSubFormulaMinHeight": 24,
2975            "DisplayOperatorMinHeight": 60,
2976            ...
2977        }
2978        italicsCorrections = {
2979            "fitalic-math": 100,
2980            "fbolditalic-math": 120,
2981            ...
2982        }
2983        topAccentAttachments = {
2984            "circumflexcomb": 500,
2985            "acutecomb": 400,
2986            "A": 300,
2987            "B": 340,
2988            ...
2989        }
2990        extendedShapes = {"parenleft", "parenright", ...}
2991        mathKerns = {
2992            "A": {
2993                "TopRight": ([-50, -100], [10, 20, 30]),
2994                "TopLeft": ([50, 100], [10, 20, 30]),
2995                ...
2996            },
2997            ...
2998        }
2999        vertGlyphVariants = {
3000            "parenleft": [("parenleft", 700), ("parenleft.size1", 1000), ...],
3001            "parenright": [("parenright", 700), ("parenright.size1", 1000), ...],
3002            ...
3003        }
3004        vertGlyphAssembly = {
3005            "braceleft": [
3006                (
3007                    ("braceleft.bottom", 0, 0, 200, 500),
3008                    ("braceleft.extender", 1, 200, 200, 200)),
3009                    ("braceleft.middle", 0, 100, 100, 700),
3010                    ("braceleft.extender", 1, 200, 200, 200),
3011                    ("braceleft.top", 0, 200, 0, 500),
3012                ),
3013                100,
3014            ],
3015            ...
3016        }
3017    """
3018    glyphMap = ttFont.getReverseGlyphMap()
3019
3020    ttFont["MATH"] = math = ttLib.newTable("MATH")
3021    math.table = table = ot.MATH()
3022    table.Version = 0x00010000
3023    table.populateDefaults()
3024
3025    table.MathConstants = _buildMathConstants(constants)
3026    table.MathGlyphInfo = _buildMathGlyphInfo(
3027        glyphMap,
3028        italicsCorrections,
3029        topAccentAttachments,
3030        extendedShapes,
3031        mathKerns,
3032    )
3033    table.MathVariants = _buildMathVariants(
3034        glyphMap,
3035        minConnectorOverlap,
3036        vertGlyphVariants,
3037        horizGlyphVariants,
3038        vertGlyphAssembly,
3039        horizGlyphAssembly,
3040    )
3041
3042
3043def _buildMathConstants(constants):
3044    if not constants:
3045        return None
3046
3047    mathConstants = ot.MathConstants()
3048    for conv in mathConstants.getConverters():
3049        value = otRound(constants.get(conv.name, 0))
3050        if conv.tableClass:
3051            assert issubclass(conv.tableClass, ot.MathValueRecord)
3052            value = _mathValueRecord(value)
3053        setattr(mathConstants, conv.name, value)
3054    return mathConstants
3055
3056
3057def _buildMathGlyphInfo(
3058    glyphMap,
3059    italicsCorrections,
3060    topAccentAttachments,
3061    extendedShapes,
3062    mathKerns,
3063):
3064    if not any([extendedShapes, italicsCorrections, topAccentAttachments, mathKerns]):
3065        return None
3066
3067    info = ot.MathGlyphInfo()
3068    info.populateDefaults()
3069
3070    if italicsCorrections:
3071        coverage = buildCoverage(italicsCorrections.keys(), glyphMap)
3072        info.MathItalicsCorrectionInfo = ot.MathItalicsCorrectionInfo()
3073        info.MathItalicsCorrectionInfo.Coverage = coverage
3074        info.MathItalicsCorrectionInfo.ItalicsCorrectionCount = len(coverage.glyphs)
3075        info.MathItalicsCorrectionInfo.ItalicsCorrection = [
3076            _mathValueRecord(italicsCorrections[n]) for n in coverage.glyphs
3077        ]
3078
3079    if topAccentAttachments:
3080        coverage = buildCoverage(topAccentAttachments.keys(), glyphMap)
3081        info.MathTopAccentAttachment = ot.MathTopAccentAttachment()
3082        info.MathTopAccentAttachment.TopAccentCoverage = coverage
3083        info.MathTopAccentAttachment.TopAccentAttachmentCount = len(coverage.glyphs)
3084        info.MathTopAccentAttachment.TopAccentAttachment = [
3085            _mathValueRecord(topAccentAttachments[n]) for n in coverage.glyphs
3086        ]
3087
3088    if extendedShapes:
3089        info.ExtendedShapeCoverage = buildCoverage(extendedShapes, glyphMap)
3090
3091    if mathKerns:
3092        coverage = buildCoverage(mathKerns.keys(), glyphMap)
3093        info.MathKernInfo = ot.MathKernInfo()
3094        info.MathKernInfo.MathKernCoverage = coverage
3095        info.MathKernInfo.MathKernCount = len(coverage.glyphs)
3096        info.MathKernInfo.MathKernInfoRecords = []
3097        for glyph in coverage.glyphs:
3098            record = ot.MathKernInfoRecord()
3099            for side in {"TopRight", "TopLeft", "BottomRight", "BottomLeft"}:
3100                if side in mathKerns[glyph]:
3101                    correctionHeights, kernValues = mathKerns[glyph][side]
3102                    assert len(correctionHeights) == len(kernValues) - 1
3103                    kern = ot.MathKern()
3104                    kern.HeightCount = len(correctionHeights)
3105                    kern.CorrectionHeight = [
3106                        _mathValueRecord(h) for h in correctionHeights
3107                    ]
3108                    kern.KernValue = [_mathValueRecord(v) for v in kernValues]
3109                    setattr(record, f"{side}MathKern", kern)
3110            info.MathKernInfo.MathKernInfoRecords.append(record)
3111
3112    return info
3113
3114
3115def _buildMathVariants(
3116    glyphMap,
3117    minConnectorOverlap,
3118    vertGlyphVariants,
3119    horizGlyphVariants,
3120    vertGlyphAssembly,
3121    horizGlyphAssembly,
3122):
3123    if not any(
3124        [vertGlyphVariants, horizGlyphVariants, vertGlyphAssembly, horizGlyphAssembly]
3125    ):
3126        return None
3127
3128    variants = ot.MathVariants()
3129    variants.populateDefaults()
3130
3131    variants.MinConnectorOverlap = minConnectorOverlap
3132
3133    if vertGlyphVariants or vertGlyphAssembly:
3134        variants.VertGlyphCoverage, variants.VertGlyphConstruction = (
3135            _buildMathGlyphConstruction(
3136                glyphMap,
3137                vertGlyphVariants,
3138                vertGlyphAssembly,
3139            )
3140        )
3141
3142    if horizGlyphVariants or horizGlyphAssembly:
3143        variants.HorizGlyphCoverage, variants.HorizGlyphConstruction = (
3144            _buildMathGlyphConstruction(
3145                glyphMap,
3146                horizGlyphVariants,
3147                horizGlyphAssembly,
3148            )
3149        )
3150
3151    return variants
3152
3153
3154def _buildMathGlyphConstruction(glyphMap, variants, assemblies):
3155    glyphs = set()
3156    if variants:
3157        glyphs.update(variants.keys())
3158    if assemblies:
3159        glyphs.update(assemblies.keys())
3160    coverage = buildCoverage(glyphs, glyphMap)
3161    constructions = []
3162
3163    for glyphName in coverage.glyphs:
3164        construction = ot.MathGlyphConstruction()
3165        construction.populateDefaults()
3166
3167        if variants and glyphName in variants:
3168            construction.VariantCount = len(variants[glyphName])
3169            construction.MathGlyphVariantRecord = []
3170            for variantName, advance in variants[glyphName]:
3171                record = ot.MathGlyphVariantRecord()
3172                record.VariantGlyph = variantName
3173                record.AdvanceMeasurement = otRound(advance)
3174                construction.MathGlyphVariantRecord.append(record)
3175
3176        if assemblies and glyphName in assemblies:
3177            parts, ic = assemblies[glyphName]
3178            construction.GlyphAssembly = ot.GlyphAssembly()
3179            construction.GlyphAssembly.ItalicsCorrection = _mathValueRecord(ic)
3180            construction.GlyphAssembly.PartCount = len(parts)
3181            construction.GlyphAssembly.PartRecords = []
3182            for part in parts:
3183                part_name, flags, start, end, advance = part
3184                record = ot.GlyphPartRecord()
3185                record.glyph = part_name
3186                record.PartFlags = int(flags)
3187                record.StartConnectorLength = otRound(start)
3188                record.EndConnectorLength = otRound(end)
3189                record.FullAdvance = otRound(advance)
3190                construction.GlyphAssembly.PartRecords.append(record)
3191
3192        constructions.append(construction)
3193
3194    return coverage, constructions
3195
3196
3197def _mathValueRecord(value):
3198    value_record = ot.MathValueRecord()
3199    value_record.Value = otRound(value)
3200    return value_record
3201