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