• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.misc.py23 import Tag, tostr
2from fontTools.misc import sstruct
3from fontTools.misc.textTools import binary2num, safeEval
4from fontTools.feaLib.error import FeatureLibError
5from fontTools.feaLib.lookupDebugInfo import (
6    LookupDebugInfo,
7    LOOKUP_DEBUG_INFO_KEY,
8    LOOKUP_DEBUG_ENV_VAR,
9)
10from fontTools.feaLib.parser import Parser
11from fontTools.feaLib.ast import FeatureFile
12from fontTools.otlLib import builder as otl
13from fontTools.otlLib.maxContextCalc import maxCtxFont
14from fontTools.ttLib import newTable, getTableModule
15from fontTools.ttLib.tables import otBase, otTables
16from fontTools.otlLib.builder import (
17    AlternateSubstBuilder,
18    ChainContextPosBuilder,
19    ChainContextSubstBuilder,
20    LigatureSubstBuilder,
21    MultipleSubstBuilder,
22    CursivePosBuilder,
23    MarkBasePosBuilder,
24    MarkLigPosBuilder,
25    MarkMarkPosBuilder,
26    ReverseChainSingleSubstBuilder,
27    SingleSubstBuilder,
28    ClassPairPosSubtableBuilder,
29    PairPosBuilder,
30    SinglePosBuilder,
31    ChainContextualRule,
32)
33from fontTools.otlLib.error import OpenTypeLibError
34from collections import defaultdict
35import itertools
36from io import StringIO
37import logging
38import warnings
39import os
40
41
42log = logging.getLogger(__name__)
43
44
45def addOpenTypeFeatures(font, featurefile, tables=None, debug=False):
46    """Add features from a file to a font. Note that this replaces any features
47    currently present.
48
49    Args:
50        font (feaLib.ttLib.TTFont): The font object.
51        featurefile: Either a path or file object (in which case we
52            parse it into an AST), or a pre-parsed AST instance.
53        tables: If passed, restrict the set of affected tables to those in the
54            list.
55        debug: Whether to add source debugging information to the font in the
56            ``Debg`` table
57
58    """
59    builder = Builder(font, featurefile)
60    builder.build(tables=tables, debug=debug)
61
62
63def addOpenTypeFeaturesFromString(
64    font, features, filename=None, tables=None, debug=False
65):
66    """Add features from a string to a font. Note that this replaces any
67    features currently present.
68
69    Args:
70        font (feaLib.ttLib.TTFont): The font object.
71        features: A string containing feature code.
72        filename: The directory containing ``filename`` is used as the root of
73            relative ``include()`` paths; if ``None`` is provided, the current
74            directory is assumed.
75        tables: If passed, restrict the set of affected tables to those in the
76            list.
77        debug: Whether to add source debugging information to the font in the
78            ``Debg`` table
79
80    """
81
82    featurefile = StringIO(tostr(features))
83    if filename:
84        featurefile.name = filename
85    addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug)
86
87
88class Builder(object):
89
90    supportedTables = frozenset(
91        Tag(tag)
92        for tag in [
93            "BASE",
94            "GDEF",
95            "GPOS",
96            "GSUB",
97            "OS/2",
98            "head",
99            "hhea",
100            "name",
101            "vhea",
102            "STAT",
103        ]
104    )
105
106    def __init__(self, font, featurefile):
107        self.font = font
108        # 'featurefile' can be either a path or file object (in which case we
109        # parse it into an AST), or a pre-parsed AST instance
110        if isinstance(featurefile, FeatureFile):
111            self.parseTree, self.file = featurefile, None
112        else:
113            self.parseTree, self.file = None, featurefile
114        self.glyphMap = font.getReverseGlyphMap()
115        self.default_language_systems_ = set()
116        self.script_ = None
117        self.lookupflag_ = 0
118        self.lookupflag_markFilterSet_ = None
119        self.language_systems = set()
120        self.seen_non_DFLT_script_ = False
121        self.named_lookups_ = {}
122        self.cur_lookup_ = None
123        self.cur_lookup_name_ = None
124        self.cur_feature_name_ = None
125        self.lookups_ = []
126        self.lookup_locations = {"GSUB": {}, "GPOS": {}}
127        self.features_ = {}  # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
128        self.required_features_ = {}  # ('latn', 'DEU ') --> 'scmp'
129        # for feature 'aalt'
130        self.aalt_features_ = []  # [(location, featureName)*], for 'aalt'
131        self.aalt_location_ = None
132        self.aalt_alternates_ = {}
133        # for 'featureNames'
134        self.featureNames_ = set()
135        self.featureNames_ids_ = {}
136        # for 'cvParameters'
137        self.cv_parameters_ = set()
138        self.cv_parameters_ids_ = {}
139        self.cv_num_named_params_ = {}
140        self.cv_characters_ = defaultdict(list)
141        # for feature 'size'
142        self.size_parameters_ = None
143        # for table 'head'
144        self.fontRevision_ = None  # 2.71
145        # for table 'name'
146        self.names_ = []
147        # for table 'BASE'
148        self.base_horiz_axis_ = None
149        self.base_vert_axis_ = None
150        # for table 'GDEF'
151        self.attachPoints_ = {}  # "a" --> {3, 7}
152        self.ligCaretCoords_ = {}  # "f_f_i" --> {300, 600}
153        self.ligCaretPoints_ = {}  # "f_f_i" --> {3, 7}
154        self.glyphClassDefs_ = {}  # "fi" --> (2, (file, line, column))
155        self.markAttach_ = {}  # "acute" --> (4, (file, line, column))
156        self.markAttachClassID_ = {}  # frozenset({"acute", "grave"}) --> 4
157        self.markFilterSets_ = {}  # frozenset({"acute", "grave"}) --> 4
158        # for table 'OS/2'
159        self.os2_ = {}
160        # for table 'hhea'
161        self.hhea_ = {}
162        # for table 'vhea'
163        self.vhea_ = {}
164        # for table 'STAT'
165        self.stat_ = {}
166
167    def build(self, tables=None, debug=False):
168        if self.parseTree is None:
169            self.parseTree = Parser(self.file, self.glyphMap).parse()
170        self.parseTree.build(self)
171        # by default, build all the supported tables
172        if tables is None:
173            tables = self.supportedTables
174        else:
175            tables = frozenset(tables)
176            unsupported = tables - self.supportedTables
177            if unsupported:
178                unsupported_string = ", ".join(sorted(unsupported))
179                raise NotImplementedError(
180                    "The following tables were requested but are unsupported: "
181                    f"{unsupported_string}."
182                )
183        if "GSUB" in tables:
184            self.build_feature_aalt_()
185        if "head" in tables:
186            self.build_head()
187        if "hhea" in tables:
188            self.build_hhea()
189        if "vhea" in tables:
190            self.build_vhea()
191        if "name" in tables:
192            self.build_name()
193        if "OS/2" in tables:
194            self.build_OS_2()
195        if "STAT" in tables:
196            self.build_STAT()
197        for tag in ("GPOS", "GSUB"):
198            if tag not in tables:
199                continue
200            table = self.makeTable(tag)
201            if (
202                table.ScriptList.ScriptCount > 0
203                or table.FeatureList.FeatureCount > 0
204                or table.LookupList.LookupCount > 0
205            ):
206                fontTable = self.font[tag] = newTable(tag)
207                fontTable.table = table
208            elif tag in self.font:
209                del self.font[tag]
210        if any(tag in self.font for tag in ("GPOS", "GSUB")) and "OS/2" in self.font:
211            self.font["OS/2"].usMaxContext = maxCtxFont(self.font)
212        if "GDEF" in tables:
213            gdef = self.buildGDEF()
214            if gdef:
215                self.font["GDEF"] = gdef
216            elif "GDEF" in self.font:
217                del self.font["GDEF"]
218        if "BASE" in tables:
219            base = self.buildBASE()
220            if base:
221                self.font["BASE"] = base
222            elif "BASE" in self.font:
223                del self.font["BASE"]
224        if debug or os.environ.get(LOOKUP_DEBUG_ENV_VAR):
225            self.buildDebg()
226
227    def get_chained_lookup_(self, location, builder_class):
228        result = builder_class(self.font, location)
229        result.lookupflag = self.lookupflag_
230        result.markFilterSet = self.lookupflag_markFilterSet_
231        self.lookups_.append(result)
232        return result
233
234    def add_lookup_to_feature_(self, lookup, feature_name):
235        for script, lang in self.language_systems:
236            key = (script, lang, feature_name)
237            self.features_.setdefault(key, []).append(lookup)
238
239    def get_lookup_(self, location, builder_class):
240        if (
241            self.cur_lookup_
242            and type(self.cur_lookup_) == builder_class
243            and self.cur_lookup_.lookupflag == self.lookupflag_
244            and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_
245        ):
246            return self.cur_lookup_
247        if self.cur_lookup_name_ and self.cur_lookup_:
248            raise FeatureLibError(
249                "Within a named lookup block, all rules must be of "
250                "the same lookup type and flag",
251                location,
252            )
253        self.cur_lookup_ = builder_class(self.font, location)
254        self.cur_lookup_.lookupflag = self.lookupflag_
255        self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
256        self.lookups_.append(self.cur_lookup_)
257        if self.cur_lookup_name_:
258            # We are starting a lookup rule inside a named lookup block.
259            self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_
260        if self.cur_feature_name_:
261            # We are starting a lookup rule inside a feature. This includes
262            # lookup rules inside named lookups inside features.
263            self.add_lookup_to_feature_(self.cur_lookup_, self.cur_feature_name_)
264        return self.cur_lookup_
265
266    def build_feature_aalt_(self):
267        if not self.aalt_features_ and not self.aalt_alternates_:
268            return
269        alternates = {g: set(a) for g, a in self.aalt_alternates_.items()}
270        for location, name in self.aalt_features_ + [(None, "aalt")]:
271            feature = [
272                (script, lang, feature, lookups)
273                for (script, lang, feature), lookups in self.features_.items()
274                if feature == name
275            ]
276            # "aalt" does not have to specify its own lookups, but it might.
277            if not feature and name != "aalt":
278                raise FeatureLibError(
279                    "Feature %s has not been defined" % name, location
280                )
281            for script, lang, feature, lookups in feature:
282                for lookuplist in lookups:
283                    if not isinstance(lookuplist, list):
284                        lookuplist = [lookuplist]
285                    for lookup in lookuplist:
286                        for glyph, alts in lookup.getAlternateGlyphs().items():
287                            alternates.setdefault(glyph, set()).update(alts)
288        single = {
289            glyph: list(repl)[0] for glyph, repl in alternates.items() if len(repl) == 1
290        }
291        # TODO: Figure out the glyph alternate ordering used by makeotf.
292        # https://github.com/fonttools/fonttools/issues/836
293        multi = {
294            glyph: sorted(repl, key=self.font.getGlyphID)
295            for glyph, repl in alternates.items()
296            if len(repl) > 1
297        }
298        if not single and not multi:
299            return
300        self.features_ = {
301            (script, lang, feature): lookups
302            for (script, lang, feature), lookups in self.features_.items()
303            if feature != "aalt"
304        }
305        old_lookups = self.lookups_
306        self.lookups_ = []
307        self.start_feature(self.aalt_location_, "aalt")
308        if single:
309            single_lookup = self.get_lookup_(location, SingleSubstBuilder)
310            single_lookup.mapping = single
311        if multi:
312            multi_lookup = self.get_lookup_(location, AlternateSubstBuilder)
313            multi_lookup.alternates = multi
314        self.end_feature()
315        self.lookups_.extend(old_lookups)
316
317    def build_head(self):
318        if not self.fontRevision_:
319            return
320        table = self.font.get("head")
321        if not table:  # this only happens for unit tests
322            table = self.font["head"] = newTable("head")
323            table.decompile(b"\0" * 54, self.font)
324            table.tableVersion = 1.0
325            table.created = table.modified = 3406620153  # 2011-12-13 11:22:33
326        table.fontRevision = self.fontRevision_
327
328    def build_hhea(self):
329        if not self.hhea_:
330            return
331        table = self.font.get("hhea")
332        if not table:  # this only happens for unit tests
333            table = self.font["hhea"] = newTable("hhea")
334            table.decompile(b"\0" * 36, self.font)
335            table.tableVersion = 0x00010000
336        if "caretoffset" in self.hhea_:
337            table.caretOffset = self.hhea_["caretoffset"]
338        if "ascender" in self.hhea_:
339            table.ascent = self.hhea_["ascender"]
340        if "descender" in self.hhea_:
341            table.descent = self.hhea_["descender"]
342        if "linegap" in self.hhea_:
343            table.lineGap = self.hhea_["linegap"]
344
345    def build_vhea(self):
346        if not self.vhea_:
347            return
348        table = self.font.get("vhea")
349        if not table:  # this only happens for unit tests
350            table = self.font["vhea"] = newTable("vhea")
351            table.decompile(b"\0" * 36, self.font)
352            table.tableVersion = 0x00011000
353        if "verttypoascender" in self.vhea_:
354            table.ascent = self.vhea_["verttypoascender"]
355        if "verttypodescender" in self.vhea_:
356            table.descent = self.vhea_["verttypodescender"]
357        if "verttypolinegap" in self.vhea_:
358            table.lineGap = self.vhea_["verttypolinegap"]
359
360    def get_user_name_id(self, table):
361        # Try to find first unused font-specific name id
362        nameIDs = [name.nameID for name in table.names]
363        for user_name_id in range(256, 32767):
364            if user_name_id not in nameIDs:
365                return user_name_id
366
367    def buildFeatureParams(self, tag):
368        params = None
369        if tag == "size":
370            params = otTables.FeatureParamsSize()
371            (
372                params.DesignSize,
373                params.SubfamilyID,
374                params.RangeStart,
375                params.RangeEnd,
376            ) = self.size_parameters_
377            if tag in self.featureNames_ids_:
378                params.SubfamilyNameID = self.featureNames_ids_[tag]
379            else:
380                params.SubfamilyNameID = 0
381        elif tag in self.featureNames_:
382            if not self.featureNames_ids_:
383                # name table wasn't selected among the tables to build; skip
384                pass
385            else:
386                assert tag in self.featureNames_ids_
387                params = otTables.FeatureParamsStylisticSet()
388                params.Version = 0
389                params.UINameID = self.featureNames_ids_[tag]
390        elif tag in self.cv_parameters_:
391            params = otTables.FeatureParamsCharacterVariants()
392            params.Format = 0
393            params.FeatUILabelNameID = self.cv_parameters_ids_.get(
394                (tag, "FeatUILabelNameID"), 0
395            )
396            params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get(
397                (tag, "FeatUITooltipTextNameID"), 0
398            )
399            params.SampleTextNameID = self.cv_parameters_ids_.get(
400                (tag, "SampleTextNameID"), 0
401            )
402            params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0)
403            params.FirstParamUILabelNameID = self.cv_parameters_ids_.get(
404                (tag, "ParamUILabelNameID_0"), 0
405            )
406            params.CharCount = len(self.cv_characters_[tag])
407            params.Character = self.cv_characters_[tag]
408        return params
409
410    def build_name(self):
411        if not self.names_:
412            return
413        table = self.font.get("name")
414        if not table:  # this only happens for unit tests
415            table = self.font["name"] = newTable("name")
416            table.names = []
417        for name in self.names_:
418            nameID, platformID, platEncID, langID, string = name
419            # For featureNames block, nameID is 'feature tag'
420            # For cvParameters blocks, nameID is ('feature tag', 'block name')
421            if not isinstance(nameID, int):
422                tag = nameID
423                if tag in self.featureNames_:
424                    if tag not in self.featureNames_ids_:
425                        self.featureNames_ids_[tag] = self.get_user_name_id(table)
426                        assert self.featureNames_ids_[tag] is not None
427                    nameID = self.featureNames_ids_[tag]
428                elif tag[0] in self.cv_parameters_:
429                    if tag not in self.cv_parameters_ids_:
430                        self.cv_parameters_ids_[tag] = self.get_user_name_id(table)
431                        assert self.cv_parameters_ids_[tag] is not None
432                    nameID = self.cv_parameters_ids_[tag]
433            table.setName(string, nameID, platformID, platEncID, langID)
434
435    def build_OS_2(self):
436        if not self.os2_:
437            return
438        table = self.font.get("OS/2")
439        if not table:  # this only happens for unit tests
440            table = self.font["OS/2"] = newTable("OS/2")
441            data = b"\0" * sstruct.calcsize(getTableModule("OS/2").OS2_format_0)
442            table.decompile(data, self.font)
443        version = 0
444        if "fstype" in self.os2_:
445            table.fsType = self.os2_["fstype"]
446        if "panose" in self.os2_:
447            panose = getTableModule("OS/2").Panose()
448            (
449                panose.bFamilyType,
450                panose.bSerifStyle,
451                panose.bWeight,
452                panose.bProportion,
453                panose.bContrast,
454                panose.bStrokeVariation,
455                panose.bArmStyle,
456                panose.bLetterForm,
457                panose.bMidline,
458                panose.bXHeight,
459            ) = self.os2_["panose"]
460            table.panose = panose
461        if "typoascender" in self.os2_:
462            table.sTypoAscender = self.os2_["typoascender"]
463        if "typodescender" in self.os2_:
464            table.sTypoDescender = self.os2_["typodescender"]
465        if "typolinegap" in self.os2_:
466            table.sTypoLineGap = self.os2_["typolinegap"]
467        if "winascent" in self.os2_:
468            table.usWinAscent = self.os2_["winascent"]
469        if "windescent" in self.os2_:
470            table.usWinDescent = self.os2_["windescent"]
471        if "vendor" in self.os2_:
472            table.achVendID = safeEval("'''" + self.os2_["vendor"] + "'''")
473        if "weightclass" in self.os2_:
474            table.usWeightClass = self.os2_["weightclass"]
475        if "widthclass" in self.os2_:
476            table.usWidthClass = self.os2_["widthclass"]
477        if "unicoderange" in self.os2_:
478            table.setUnicodeRanges(self.os2_["unicoderange"])
479        if "codepagerange" in self.os2_:
480            pages = self.build_codepages_(self.os2_["codepagerange"])
481            table.ulCodePageRange1, table.ulCodePageRange2 = pages
482            version = 1
483        if "xheight" in self.os2_:
484            table.sxHeight = self.os2_["xheight"]
485            version = 2
486        if "capheight" in self.os2_:
487            table.sCapHeight = self.os2_["capheight"]
488            version = 2
489        if "loweropsize" in self.os2_:
490            table.usLowerOpticalPointSize = self.os2_["loweropsize"]
491            version = 5
492        if "upperopsize" in self.os2_:
493            table.usUpperOpticalPointSize = self.os2_["upperopsize"]
494            version = 5
495
496        def checkattr(table, attrs):
497            for attr in attrs:
498                if not hasattr(table, attr):
499                    setattr(table, attr, 0)
500
501        table.version = max(version, table.version)
502        # this only happens for unit tests
503        if version >= 1:
504            checkattr(table, ("ulCodePageRange1", "ulCodePageRange2"))
505        if version >= 2:
506            checkattr(
507                table,
508                (
509                    "sxHeight",
510                    "sCapHeight",
511                    "usDefaultChar",
512                    "usBreakChar",
513                    "usMaxContext",
514                ),
515            )
516        if version >= 5:
517            checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))
518
519    def setElidedFallbackName(self, value, location):
520        # ElidedFallbackName is a convenience method for setting
521        # ElidedFallbackNameID so only one can be allowed
522        for token in ("ElidedFallbackName", "ElidedFallbackNameID"):
523            if token in self.stat_:
524                raise FeatureLibError(
525                    f"{token} is already set.",
526                    location,
527                )
528        if isinstance(value, int):
529            self.stat_["ElidedFallbackNameID"] = value
530        elif isinstance(value, list):
531            self.stat_["ElidedFallbackName"] = value
532        else:
533            raise AssertionError(value)
534
535    def addDesignAxis(self, designAxis, location):
536        if "DesignAxes" not in self.stat_:
537            self.stat_["DesignAxes"] = []
538        if designAxis.tag in (r.tag for r in self.stat_["DesignAxes"]):
539            raise FeatureLibError(
540                f'DesignAxis already defined for tag "{designAxis.tag}".',
541                location,
542            )
543        if designAxis.axisOrder in (r.axisOrder for r in self.stat_["DesignAxes"]):
544            raise FeatureLibError(
545                f"DesignAxis already defined for axis number {designAxis.axisOrder}.",
546                location,
547            )
548        self.stat_["DesignAxes"].append(designAxis)
549
550    def addAxisValueRecord(self, axisValueRecord, location):
551        if "AxisValueRecords" not in self.stat_:
552            self.stat_["AxisValueRecords"] = []
553        # Check for duplicate AxisValueRecords
554        for record_ in self.stat_["AxisValueRecords"]:
555            if (
556                {n.asFea() for n in record_.names}
557                == {n.asFea() for n in axisValueRecord.names}
558                and {n.asFea() for n in record_.locations}
559                == {n.asFea() for n in axisValueRecord.locations}
560                and record_.flags == axisValueRecord.flags
561            ):
562                raise FeatureLibError(
563                    "An AxisValueRecord with these values is already defined.",
564                    location,
565                )
566        self.stat_["AxisValueRecords"].append(axisValueRecord)
567
568    def build_STAT(self):
569        if not self.stat_:
570            return
571
572        axes = self.stat_.get("DesignAxes")
573        if not axes:
574            raise FeatureLibError("DesignAxes not defined", None)
575        axisValueRecords = self.stat_.get("AxisValueRecords")
576        axisValues = {}
577        format4_locations = []
578        for tag in axes:
579            axisValues[tag.tag] = []
580        if axisValueRecords is not None:
581            for avr in axisValueRecords:
582                valuesDict = {}
583                if avr.flags > 0:
584                    valuesDict["flags"] = avr.flags
585                if len(avr.locations) == 1:
586                    location = avr.locations[0]
587                    values = location.values
588                    if len(values) == 1:  # format1
589                        valuesDict.update({"value": values[0], "name": avr.names})
590                    if len(values) == 2:  # format3
591                        valuesDict.update(
592                            {
593                                "value": values[0],
594                                "linkedValue": values[1],
595                                "name": avr.names,
596                            }
597                        )
598                    if len(values) == 3:  # format2
599                        nominal, minVal, maxVal = values
600                        valuesDict.update(
601                            {
602                                "nominalValue": nominal,
603                                "rangeMinValue": minVal,
604                                "rangeMaxValue": maxVal,
605                                "name": avr.names,
606                            }
607                        )
608                    axisValues[location.tag].append(valuesDict)
609                else:
610                    valuesDict.update(
611                        {
612                            "location": {i.tag: i.values[0] for i in avr.locations},
613                            "name": avr.names,
614                        }
615                    )
616                    format4_locations.append(valuesDict)
617
618        designAxes = [
619            {
620                "ordering": a.axisOrder,
621                "tag": a.tag,
622                "name": a.names,
623                "values": axisValues[a.tag],
624            }
625            for a in axes
626        ]
627
628        nameTable = self.font.get("name")
629        if not nameTable:  # this only happens for unit tests
630            nameTable = self.font["name"] = newTable("name")
631            nameTable.names = []
632
633        if "ElidedFallbackNameID" in self.stat_:
634            nameID = self.stat_["ElidedFallbackNameID"]
635            name = nameTable.getDebugName(nameID)
636            if not name:
637                raise FeatureLibError(
638                    f"ElidedFallbackNameID {nameID} points "
639                    "to a nameID that does not exist in the "
640                    '"name" table',
641                    None,
642                )
643        elif "ElidedFallbackName" in self.stat_:
644            nameID = self.stat_["ElidedFallbackName"]
645
646        otl.buildStatTable(
647            self.font,
648            designAxes,
649            locations=format4_locations,
650            elidedFallbackName=nameID,
651        )
652
653    def build_codepages_(self, pages):
654        pages2bits = {
655            1252: 0,
656            1250: 1,
657            1251: 2,
658            1253: 3,
659            1254: 4,
660            1255: 5,
661            1256: 6,
662            1257: 7,
663            1258: 8,
664            874: 16,
665            932: 17,
666            936: 18,
667            949: 19,
668            950: 20,
669            1361: 21,
670            869: 48,
671            866: 49,
672            865: 50,
673            864: 51,
674            863: 52,
675            862: 53,
676            861: 54,
677            860: 55,
678            857: 56,
679            855: 57,
680            852: 58,
681            775: 59,
682            737: 60,
683            708: 61,
684            850: 62,
685            437: 63,
686        }
687        bits = [pages2bits[p] for p in pages if p in pages2bits]
688        pages = []
689        for i in range(2):
690            pages.append("")
691            for j in range(i * 32, (i + 1) * 32):
692                if j in bits:
693                    pages[i] += "1"
694                else:
695                    pages[i] += "0"
696        return [binary2num(p[::-1]) for p in pages]
697
698    def buildBASE(self):
699        if not self.base_horiz_axis_ and not self.base_vert_axis_:
700            return None
701        base = otTables.BASE()
702        base.Version = 0x00010000
703        base.HorizAxis = self.buildBASEAxis(self.base_horiz_axis_)
704        base.VertAxis = self.buildBASEAxis(self.base_vert_axis_)
705
706        result = newTable("BASE")
707        result.table = base
708        return result
709
710    def buildBASEAxis(self, axis):
711        if not axis:
712            return
713        bases, scripts = axis
714        axis = otTables.Axis()
715        axis.BaseTagList = otTables.BaseTagList()
716        axis.BaseTagList.BaselineTag = bases
717        axis.BaseTagList.BaseTagCount = len(bases)
718        axis.BaseScriptList = otTables.BaseScriptList()
719        axis.BaseScriptList.BaseScriptRecord = []
720        axis.BaseScriptList.BaseScriptCount = len(scripts)
721        for script in sorted(scripts):
722            record = otTables.BaseScriptRecord()
723            record.BaseScriptTag = script[0]
724            record.BaseScript = otTables.BaseScript()
725            record.BaseScript.BaseLangSysCount = 0
726            record.BaseScript.BaseValues = otTables.BaseValues()
727            record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
728            record.BaseScript.BaseValues.BaseCoord = []
729            record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
730            for c in script[2]:
731                coord = otTables.BaseCoord()
732                coord.Format = 1
733                coord.Coordinate = c
734                record.BaseScript.BaseValues.BaseCoord.append(coord)
735            axis.BaseScriptList.BaseScriptRecord.append(record)
736        return axis
737
738    def buildGDEF(self):
739        gdef = otTables.GDEF()
740        gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_()
741        gdef.AttachList = otl.buildAttachList(self.attachPoints_, self.glyphMap)
742        gdef.LigCaretList = otl.buildLigCaretList(
743            self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap
744        )
745        gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
746        gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
747        gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
748        if any(
749            (
750                gdef.GlyphClassDef,
751                gdef.AttachList,
752                gdef.LigCaretList,
753                gdef.MarkAttachClassDef,
754                gdef.MarkGlyphSetsDef,
755            )
756        ):
757            result = newTable("GDEF")
758            result.table = gdef
759            return result
760        else:
761            return None
762
763    def buildGDEFGlyphClassDef_(self):
764        if self.glyphClassDefs_:
765            classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()}
766        else:
767            classes = {}
768            for lookup in self.lookups_:
769                classes.update(lookup.inferGlyphClasses())
770            for markClass in self.parseTree.markClasses.values():
771                for markClassDef in markClass.definitions:
772                    for glyph in markClassDef.glyphSet():
773                        classes[glyph] = 3
774        if classes:
775            result = otTables.GlyphClassDef()
776            result.classDefs = classes
777            return result
778        else:
779            return None
780
781    def buildGDEFMarkAttachClassDef_(self):
782        classDefs = {g: c for g, (c, _) in self.markAttach_.items()}
783        if not classDefs:
784            return None
785        result = otTables.MarkAttachClassDef()
786        result.classDefs = classDefs
787        return result
788
789    def buildGDEFMarkGlyphSetsDef_(self):
790        sets = []
791        for glyphs, id_ in sorted(
792            self.markFilterSets_.items(), key=lambda item: item[1]
793        ):
794            sets.append(glyphs)
795        return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)
796
797    def buildDebg(self):
798        if "Debg" not in self.font:
799            self.font["Debg"] = newTable("Debg")
800            self.font["Debg"].data = {}
801        self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations
802
803    def buildLookups_(self, tag):
804        assert tag in ("GPOS", "GSUB"), tag
805        for lookup in self.lookups_:
806            lookup.lookup_index = None
807        lookups = []
808        for lookup in self.lookups_:
809            if lookup.table != tag:
810                continue
811            lookup.lookup_index = len(lookups)
812            self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
813                location=str(lookup.location),
814                name=self.get_lookup_name_(lookup),
815                feature=None,
816            )
817            lookups.append(lookup)
818        try:
819            otLookups = [l.build() for l in lookups]
820        except OpenTypeLibError as e:
821            raise FeatureLibError(str(e), e.location) from e
822        return otLookups
823
824    def makeTable(self, tag):
825        table = getattr(otTables, tag, None)()
826        table.Version = 0x00010000
827        table.ScriptList = otTables.ScriptList()
828        table.ScriptList.ScriptRecord = []
829        table.FeatureList = otTables.FeatureList()
830        table.FeatureList.FeatureRecord = []
831        table.LookupList = otTables.LookupList()
832        table.LookupList.Lookup = self.buildLookups_(tag)
833
834        # Build a table for mapping (tag, lookup_indices) to feature_index.
835        # For example, ('liga', (2,3,7)) --> 23.
836        feature_indices = {}
837        required_feature_indices = {}  # ('latn', 'DEU') --> 23
838        scripts = {}  # 'latn' --> {'DEU': [23, 24]} for feature #23,24
839        # Sort the feature table by feature tag:
840        # https://github.com/fonttools/fonttools/issues/568
841        sortFeatureTag = lambda f: (f[0][2], f[0][1], f[0][0], f[1])
842        for key, lookups in sorted(self.features_.items(), key=sortFeatureTag):
843            script, lang, feature_tag = key
844            # l.lookup_index will be None when a lookup is not needed
845            # for the table under construction. For example, substitution
846            # rules will have no lookup_index while building GPOS tables.
847            lookup_indices = tuple(
848                [l.lookup_index for l in lookups if l.lookup_index is not None]
849            )
850
851            size_feature = tag == "GPOS" and feature_tag == "size"
852            if len(lookup_indices) == 0 and not size_feature:
853                continue
854
855            for ix in lookup_indices:
856                try:
857                    self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
858                        str(ix)
859                    ]._replace(feature=key)
860                except KeyError:
861                    warnings.warn(
862                        "feaLib.Builder subclass needs upgrading to "
863                        "stash debug information. See fonttools#2065."
864                    )
865
866            feature_key = (feature_tag, lookup_indices)
867            feature_index = feature_indices.get(feature_key)
868            if feature_index is None:
869                feature_index = len(table.FeatureList.FeatureRecord)
870                frec = otTables.FeatureRecord()
871                frec.FeatureTag = feature_tag
872                frec.Feature = otTables.Feature()
873                frec.Feature.FeatureParams = self.buildFeatureParams(feature_tag)
874                frec.Feature.LookupListIndex = list(lookup_indices)
875                frec.Feature.LookupCount = len(lookup_indices)
876                table.FeatureList.FeatureRecord.append(frec)
877                feature_indices[feature_key] = feature_index
878            scripts.setdefault(script, {}).setdefault(lang, []).append(feature_index)
879            if self.required_features_.get((script, lang)) == feature_tag:
880                required_feature_indices[(script, lang)] = feature_index
881
882        # Build ScriptList.
883        for script, lang_features in sorted(scripts.items()):
884            srec = otTables.ScriptRecord()
885            srec.ScriptTag = script
886            srec.Script = otTables.Script()
887            srec.Script.DefaultLangSys = None
888            srec.Script.LangSysRecord = []
889            for lang, feature_indices in sorted(lang_features.items()):
890                langrec = otTables.LangSysRecord()
891                langrec.LangSys = otTables.LangSys()
892                langrec.LangSys.LookupOrder = None
893
894                req_feature_index = required_feature_indices.get((script, lang))
895                if req_feature_index is None:
896                    langrec.LangSys.ReqFeatureIndex = 0xFFFF
897                else:
898                    langrec.LangSys.ReqFeatureIndex = req_feature_index
899
900                langrec.LangSys.FeatureIndex = [
901                    i for i in feature_indices if i != req_feature_index
902                ]
903                langrec.LangSys.FeatureCount = len(langrec.LangSys.FeatureIndex)
904
905                if lang == "dflt":
906                    srec.Script.DefaultLangSys = langrec.LangSys
907                else:
908                    langrec.LangSysTag = lang
909                    srec.Script.LangSysRecord.append(langrec)
910            srec.Script.LangSysCount = len(srec.Script.LangSysRecord)
911            table.ScriptList.ScriptRecord.append(srec)
912
913        table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord)
914        table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
915        table.LookupList.LookupCount = len(table.LookupList.Lookup)
916        return table
917
918    def get_lookup_name_(self, lookup):
919        rev = {v: k for k, v in self.named_lookups_.items()}
920        if lookup in rev:
921            return rev[lookup]
922        return None
923
924    def add_language_system(self, location, script, language):
925        # OpenType Feature File Specification, section 4.b.i
926        if script == "DFLT" and language == "dflt" and self.default_language_systems_:
927            raise FeatureLibError(
928                'If "languagesystem DFLT dflt" is present, it must be '
929                "the first of the languagesystem statements",
930                location,
931            )
932        if script == "DFLT":
933            if self.seen_non_DFLT_script_:
934                raise FeatureLibError(
935                    'languagesystems using the "DFLT" script tag must '
936                    "precede all other languagesystems",
937                    location,
938                )
939        else:
940            self.seen_non_DFLT_script_ = True
941        if (script, language) in self.default_language_systems_:
942            raise FeatureLibError(
943                '"languagesystem %s %s" has already been specified'
944                % (script.strip(), language.strip()),
945                location,
946            )
947        self.default_language_systems_.add((script, language))
948
949    def get_default_language_systems_(self):
950        # OpenType Feature File specification, 4.b.i. languagesystem:
951        # If no "languagesystem" statement is present, then the
952        # implementation must behave exactly as though the following
953        # statement were present at the beginning of the feature file:
954        # languagesystem DFLT dflt;
955        if self.default_language_systems_:
956            return frozenset(self.default_language_systems_)
957        else:
958            return frozenset({("DFLT", "dflt")})
959
960    def start_feature(self, location, name):
961        self.language_systems = self.get_default_language_systems_()
962        self.script_ = "DFLT"
963        self.cur_lookup_ = None
964        self.cur_feature_name_ = name
965        self.lookupflag_ = 0
966        self.lookupflag_markFilterSet_ = None
967        if name == "aalt":
968            self.aalt_location_ = location
969
970    def end_feature(self):
971        assert self.cur_feature_name_ is not None
972        self.cur_feature_name_ = None
973        self.language_systems = None
974        self.cur_lookup_ = None
975        self.lookupflag_ = 0
976        self.lookupflag_markFilterSet_ = None
977
978    def start_lookup_block(self, location, name):
979        if name in self.named_lookups_:
980            raise FeatureLibError(
981                'Lookup "%s" has already been defined' % name, location
982            )
983        if self.cur_feature_name_ == "aalt":
984            raise FeatureLibError(
985                "Lookup blocks cannot be placed inside 'aalt' features; "
986                "move it out, and then refer to it with a lookup statement",
987                location,
988            )
989        self.cur_lookup_name_ = name
990        self.named_lookups_[name] = None
991        self.cur_lookup_ = None
992        if self.cur_feature_name_ is None:
993            self.lookupflag_ = 0
994            self.lookupflag_markFilterSet_ = None
995
996    def end_lookup_block(self):
997        assert self.cur_lookup_name_ is not None
998        self.cur_lookup_name_ = None
999        self.cur_lookup_ = None
1000        if self.cur_feature_name_ is None:
1001            self.lookupflag_ = 0
1002            self.lookupflag_markFilterSet_ = None
1003
1004    def add_lookup_call(self, lookup_name):
1005        assert lookup_name in self.named_lookups_, lookup_name
1006        self.cur_lookup_ = None
1007        lookup = self.named_lookups_[lookup_name]
1008        self.add_lookup_to_feature_(lookup, self.cur_feature_name_)
1009
1010    def set_font_revision(self, location, revision):
1011        self.fontRevision_ = revision
1012
1013    def set_language(self, location, language, include_default, required):
1014        assert len(language) == 4
1015        if self.cur_feature_name_ in ("aalt", "size"):
1016            raise FeatureLibError(
1017                "Language statements are not allowed "
1018                'within "feature %s"' % self.cur_feature_name_,
1019                location,
1020            )
1021        if self.cur_feature_name_ is None:
1022            raise FeatureLibError(
1023                "Language statements are not allowed "
1024                "within standalone lookup blocks",
1025                location,
1026            )
1027        self.cur_lookup_ = None
1028
1029        key = (self.script_, language, self.cur_feature_name_)
1030        lookups = self.features_.get((key[0], "dflt", key[2]))
1031        if (language == "dflt" or include_default) and lookups:
1032            self.features_[key] = lookups[:]
1033        else:
1034            self.features_[key] = []
1035        self.language_systems = frozenset([(self.script_, language)])
1036
1037        if required:
1038            key = (self.script_, language)
1039            if key in self.required_features_:
1040                raise FeatureLibError(
1041                    "Language %s (script %s) has already "
1042                    "specified feature %s as its required feature"
1043                    % (
1044                        language.strip(),
1045                        self.script_.strip(),
1046                        self.required_features_[key].strip(),
1047                    ),
1048                    location,
1049                )
1050            self.required_features_[key] = self.cur_feature_name_
1051
1052    def getMarkAttachClass_(self, location, glyphs):
1053        glyphs = frozenset(glyphs)
1054        id_ = self.markAttachClassID_.get(glyphs)
1055        if id_ is not None:
1056            return id_
1057        id_ = len(self.markAttachClassID_) + 1
1058        self.markAttachClassID_[glyphs] = id_
1059        for glyph in glyphs:
1060            if glyph in self.markAttach_:
1061                _, loc = self.markAttach_[glyph]
1062                raise FeatureLibError(
1063                    "Glyph %s already has been assigned "
1064                    "a MarkAttachmentType at %s" % (glyph, loc),
1065                    location,
1066                )
1067            self.markAttach_[glyph] = (id_, location)
1068        return id_
1069
1070    def getMarkFilterSet_(self, location, glyphs):
1071        glyphs = frozenset(glyphs)
1072        id_ = self.markFilterSets_.get(glyphs)
1073        if id_ is not None:
1074            return id_
1075        id_ = len(self.markFilterSets_)
1076        self.markFilterSets_[glyphs] = id_
1077        return id_
1078
1079    def set_lookup_flag(self, location, value, markAttach, markFilter):
1080        value = value & 0xFF
1081        if markAttach:
1082            markAttachClass = self.getMarkAttachClass_(location, markAttach)
1083            value = value | (markAttachClass << 8)
1084        if markFilter:
1085            markFilterSet = self.getMarkFilterSet_(location, markFilter)
1086            value = value | 0x10
1087            self.lookupflag_markFilterSet_ = markFilterSet
1088        else:
1089            self.lookupflag_markFilterSet_ = None
1090        self.lookupflag_ = value
1091
1092    def set_script(self, location, script):
1093        if self.cur_feature_name_ in ("aalt", "size"):
1094            raise FeatureLibError(
1095                "Script statements are not allowed "
1096                'within "feature %s"' % self.cur_feature_name_,
1097                location,
1098            )
1099        if self.cur_feature_name_ is None:
1100            raise FeatureLibError(
1101                "Script statements are not allowed " "within standalone lookup blocks",
1102                location,
1103            )
1104        if self.language_systems == {(script, "dflt")}:
1105            # Nothing to do.
1106            return
1107        self.cur_lookup_ = None
1108        self.script_ = script
1109        self.lookupflag_ = 0
1110        self.lookupflag_markFilterSet_ = None
1111        self.set_language(location, "dflt", include_default=True, required=False)
1112
1113    def find_lookup_builders_(self, lookups):
1114        """Helper for building chain contextual substitutions
1115
1116        Given a list of lookup names, finds the LookupBuilder for each name.
1117        If an input name is None, it gets mapped to a None LookupBuilder.
1118        """
1119        lookup_builders = []
1120        for lookuplist in lookups:
1121            if lookuplist is not None:
1122                lookup_builders.append(
1123                    [self.named_lookups_.get(l.name) for l in lookuplist]
1124                )
1125            else:
1126                lookup_builders.append(None)
1127        return lookup_builders
1128
1129    def add_attach_points(self, location, glyphs, contourPoints):
1130        for glyph in glyphs:
1131            self.attachPoints_.setdefault(glyph, set()).update(contourPoints)
1132
1133    def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups):
1134        lookup = self.get_lookup_(location, ChainContextPosBuilder)
1135        lookup.rules.append(
1136            ChainContextualRule(
1137                prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
1138            )
1139        )
1140
1141    def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
1142        lookup = self.get_lookup_(location, ChainContextSubstBuilder)
1143        lookup.rules.append(
1144            ChainContextualRule(
1145                prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
1146            )
1147        )
1148
1149    def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
1150        if self.cur_feature_name_ == "aalt":
1151            alts = self.aalt_alternates_.setdefault(glyph, set())
1152            alts.update(replacement)
1153            return
1154        if prefix or suffix:
1155            chain = self.get_lookup_(location, ChainContextSubstBuilder)
1156            lookup = self.get_chained_lookup_(location, AlternateSubstBuilder)
1157            chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [lookup]))
1158        else:
1159            lookup = self.get_lookup_(location, AlternateSubstBuilder)
1160        if glyph in lookup.alternates:
1161            raise FeatureLibError(
1162                'Already defined alternates for glyph "%s"' % glyph, location
1163            )
1164        lookup.alternates[glyph] = replacement
1165
1166    def add_feature_reference(self, location, featureName):
1167        if self.cur_feature_name_ != "aalt":
1168            raise FeatureLibError(
1169                'Feature references are only allowed inside "feature aalt"', location
1170            )
1171        self.aalt_features_.append((location, featureName))
1172
1173    def add_featureName(self, tag):
1174        self.featureNames_.add(tag)
1175
1176    def add_cv_parameter(self, tag):
1177        self.cv_parameters_.add(tag)
1178
1179    def add_to_cv_num_named_params(self, tag):
1180        """Adds new items to ``self.cv_num_named_params_``
1181        or increments the count of existing items."""
1182        if tag in self.cv_num_named_params_:
1183            self.cv_num_named_params_[tag] += 1
1184        else:
1185            self.cv_num_named_params_[tag] = 1
1186
1187    def add_cv_character(self, character, tag):
1188        self.cv_characters_[tag].append(character)
1189
1190    def set_base_axis(self, bases, scripts, vertical):
1191        if vertical:
1192            self.base_vert_axis_ = (bases, scripts)
1193        else:
1194            self.base_horiz_axis_ = (bases, scripts)
1195
1196    def set_size_parameters(
1197        self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
1198    ):
1199        if self.cur_feature_name_ != "size":
1200            raise FeatureLibError(
1201                "Parameters statements are not allowed "
1202                'within "feature %s"' % self.cur_feature_name_,
1203                location,
1204            )
1205        self.size_parameters_ = [DesignSize, SubfamilyID, RangeStart, RangeEnd]
1206        for script, lang in self.language_systems:
1207            key = (script, lang, self.cur_feature_name_)
1208            self.features_.setdefault(key, [])
1209
1210    def add_ligature_subst(
1211        self, location, prefix, glyphs, suffix, replacement, forceChain
1212    ):
1213        if prefix or suffix or forceChain:
1214            chain = self.get_lookup_(location, ChainContextSubstBuilder)
1215            lookup = self.get_chained_lookup_(location, LigatureSubstBuilder)
1216            chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [lookup]))
1217        else:
1218            lookup = self.get_lookup_(location, LigatureSubstBuilder)
1219
1220        # OpenType feature file syntax, section 5.d, "Ligature substitution":
1221        # "Since the OpenType specification does not allow ligature
1222        # substitutions to be specified on target sequences that contain
1223        # glyph classes, the implementation software will enumerate
1224        # all specific glyph sequences if glyph classes are detected"
1225        for g in sorted(itertools.product(*glyphs)):
1226            lookup.ligatures[g] = replacement
1227
1228    def add_multiple_subst(
1229        self, location, prefix, glyph, suffix, replacements, forceChain=False
1230    ):
1231        if prefix or suffix or forceChain:
1232            chain = self.get_lookup_(location, ChainContextSubstBuilder)
1233            sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
1234            sub.mapping[glyph] = replacements
1235            chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub]))
1236            return
1237        lookup = self.get_lookup_(location, MultipleSubstBuilder)
1238        if glyph in lookup.mapping:
1239            if replacements == lookup.mapping[glyph]:
1240                log.info(
1241                    "Removing duplicate multiple substitution from glyph"
1242                    ' "%s" to %s%s',
1243                    glyph,
1244                    replacements,
1245                    f" at {location}" if location else "",
1246                )
1247            else:
1248                raise FeatureLibError(
1249                    'Already defined substitution for glyph "%s"' % glyph, location
1250                )
1251        lookup.mapping[glyph] = replacements
1252
1253    def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping):
1254        lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
1255        lookup.rules.append((old_prefix, old_suffix, mapping))
1256
1257    def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
1258        if self.cur_feature_name_ == "aalt":
1259            for (from_glyph, to_glyph) in mapping.items():
1260                alts = self.aalt_alternates_.setdefault(from_glyph, set())
1261                alts.add(to_glyph)
1262            return
1263        if prefix or suffix or forceChain:
1264            self.add_single_subst_chained_(location, prefix, suffix, mapping)
1265            return
1266        lookup = self.get_lookup_(location, SingleSubstBuilder)
1267        for (from_glyph, to_glyph) in mapping.items():
1268            if from_glyph in lookup.mapping:
1269                if to_glyph == lookup.mapping[from_glyph]:
1270                    log.info(
1271                        "Removing duplicate single substitution from glyph"
1272                        ' "%s" to "%s" at %s',
1273                        from_glyph,
1274                        to_glyph,
1275                        location,
1276                    )
1277                else:
1278                    raise FeatureLibError(
1279                        'Already defined rule for replacing glyph "%s" by "%s"'
1280                        % (from_glyph, lookup.mapping[from_glyph]),
1281                        location,
1282                    )
1283            lookup.mapping[from_glyph] = to_glyph
1284
1285    def add_single_subst_chained_(self, location, prefix, suffix, mapping):
1286        # https://github.com/fonttools/fonttools/issues/512
1287        chain = self.get_lookup_(location, ChainContextSubstBuilder)
1288        sub = chain.find_chainable_single_subst(set(mapping.keys()))
1289        if sub is None:
1290            sub = self.get_chained_lookup_(location, SingleSubstBuilder)
1291        sub.mapping.update(mapping)
1292        chain.rules.append(
1293            ChainContextualRule(prefix, [list(mapping.keys())], suffix, [sub])
1294        )
1295
1296    def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor):
1297        lookup = self.get_lookup_(location, CursivePosBuilder)
1298        lookup.add_attachment(
1299            location,
1300            glyphclass,
1301            makeOpenTypeAnchor(entryAnchor),
1302            makeOpenTypeAnchor(exitAnchor),
1303        )
1304
1305    def add_marks_(self, location, lookupBuilder, marks):
1306        """Helper for add_mark_{base,liga,mark}_pos."""
1307        for _, markClass in marks:
1308            for markClassDef in markClass.definitions:
1309                for mark in markClassDef.glyphs.glyphSet():
1310                    if mark not in lookupBuilder.marks:
1311                        otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor)
1312                        lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
1313                    else:
1314                        existingMarkClass = lookupBuilder.marks[mark][0]
1315                        if markClass.name != existingMarkClass:
1316                            raise FeatureLibError(
1317                                "Glyph %s cannot be in both @%s and @%s"
1318                                % (mark, existingMarkClass, markClass.name),
1319                                location,
1320                            )
1321
1322    def add_mark_base_pos(self, location, bases, marks):
1323        builder = self.get_lookup_(location, MarkBasePosBuilder)
1324        self.add_marks_(location, builder, marks)
1325        for baseAnchor, markClass in marks:
1326            otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
1327            for base in bases:
1328                builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor
1329
1330    def add_mark_lig_pos(self, location, ligatures, components):
1331        builder = self.get_lookup_(location, MarkLigPosBuilder)
1332        componentAnchors = []
1333        for marks in components:
1334            anchors = {}
1335            self.add_marks_(location, builder, marks)
1336            for ligAnchor, markClass in marks:
1337                anchors[markClass.name] = makeOpenTypeAnchor(ligAnchor)
1338            componentAnchors.append(anchors)
1339        for glyph in ligatures:
1340            builder.ligatures[glyph] = componentAnchors
1341
1342    def add_mark_mark_pos(self, location, baseMarks, marks):
1343        builder = self.get_lookup_(location, MarkMarkPosBuilder)
1344        self.add_marks_(location, builder, marks)
1345        for baseAnchor, markClass in marks:
1346            otBaseAnchor = makeOpenTypeAnchor(baseAnchor)
1347            for baseMark in baseMarks:
1348                builder.baseMarks.setdefault(baseMark, {})[
1349                    markClass.name
1350                ] = otBaseAnchor
1351
1352    def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
1353        lookup = self.get_lookup_(location, PairPosBuilder)
1354        v1 = makeOpenTypeValueRecord(value1, pairPosContext=True)
1355        v2 = makeOpenTypeValueRecord(value2, pairPosContext=True)
1356        lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2)
1357
1358    def add_subtable_break(self, location):
1359        self.cur_lookup_.add_subtable_break(location)
1360
1361    def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
1362        lookup = self.get_lookup_(location, PairPosBuilder)
1363        v1 = makeOpenTypeValueRecord(value1, pairPosContext=True)
1364        v2 = makeOpenTypeValueRecord(value2, pairPosContext=True)
1365        lookup.addGlyphPair(location, glyph1, v1, glyph2, v2)
1366
1367    def add_single_pos(self, location, prefix, suffix, pos, forceChain):
1368        if prefix or suffix or forceChain:
1369            self.add_single_pos_chained_(location, prefix, suffix, pos)
1370        else:
1371            lookup = self.get_lookup_(location, SinglePosBuilder)
1372            for glyphs, value in pos:
1373                otValueRecord = makeOpenTypeValueRecord(value, pairPosContext=False)
1374                for glyph in glyphs:
1375                    try:
1376                        lookup.add_pos(location, glyph, otValueRecord)
1377                    except OpenTypeLibError as e:
1378                        raise FeatureLibError(str(e), e.location) from e
1379
1380    def add_single_pos_chained_(self, location, prefix, suffix, pos):
1381        # https://github.com/fonttools/fonttools/issues/514
1382        chain = self.get_lookup_(location, ChainContextPosBuilder)
1383        targets = []
1384        for _, _, _, lookups in chain.rules:
1385            targets.extend(lookups)
1386        subs = []
1387        for glyphs, value in pos:
1388            if value is None:
1389                subs.append(None)
1390                continue
1391            otValue = makeOpenTypeValueRecord(value, pairPosContext=False)
1392            sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
1393            if sub is None:
1394                sub = self.get_chained_lookup_(location, SinglePosBuilder)
1395                targets.append(sub)
1396            for glyph in glyphs:
1397                sub.add_pos(location, glyph, otValue)
1398            subs.append(sub)
1399        assert len(pos) == len(subs), (pos, subs)
1400        chain.rules.append(
1401            ChainContextualRule(prefix, [g for g, v in pos], suffix, subs)
1402        )
1403
1404    def setGlyphClass_(self, location, glyph, glyphClass):
1405        oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None))
1406        if oldClass and oldClass != glyphClass:
1407            raise FeatureLibError(
1408                "Glyph %s was assigned to a different class at %s"
1409                % (glyph, oldLocation),
1410                location,
1411            )
1412        self.glyphClassDefs_[glyph] = (glyphClass, location)
1413
1414    def add_glyphClassDef(
1415        self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs
1416    ):
1417        for glyph in baseGlyphs:
1418            self.setGlyphClass_(location, glyph, 1)
1419        for glyph in ligatureGlyphs:
1420            self.setGlyphClass_(location, glyph, 2)
1421        for glyph in markGlyphs:
1422            self.setGlyphClass_(location, glyph, 3)
1423        for glyph in componentGlyphs:
1424            self.setGlyphClass_(location, glyph, 4)
1425
1426    def add_ligatureCaretByIndex_(self, location, glyphs, carets):
1427        for glyph in glyphs:
1428            if glyph not in self.ligCaretPoints_:
1429                self.ligCaretPoints_[glyph] = carets
1430
1431    def add_ligatureCaretByPos_(self, location, glyphs, carets):
1432        for glyph in glyphs:
1433            if glyph not in self.ligCaretCoords_:
1434                self.ligCaretCoords_[glyph] = carets
1435
1436    def add_name_record(self, location, nameID, platformID, platEncID, langID, string):
1437        self.names_.append([nameID, platformID, platEncID, langID, string])
1438
1439    def add_os2_field(self, key, value):
1440        self.os2_[key] = value
1441
1442    def add_hhea_field(self, key, value):
1443        self.hhea_[key] = value
1444
1445    def add_vhea_field(self, key, value):
1446        self.vhea_[key] = value
1447
1448
1449def makeOpenTypeAnchor(anchor):
1450    """ast.Anchor --> otTables.Anchor"""
1451    if anchor is None:
1452        return None
1453    deviceX, deviceY = None, None
1454    if anchor.xDeviceTable is not None:
1455        deviceX = otl.buildDevice(dict(anchor.xDeviceTable))
1456    if anchor.yDeviceTable is not None:
1457        deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
1458    return otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY)
1459
1460
1461_VALUEREC_ATTRS = {
1462    name[0].lower() + name[1:]: (name, isDevice)
1463    for _, name, isDevice, _ in otBase.valueRecordFormat
1464    if not name.startswith("Reserved")
1465}
1466
1467
1468def makeOpenTypeValueRecord(v, pairPosContext):
1469    """ast.ValueRecord --> otBase.ValueRecord"""
1470    if not v:
1471        return None
1472
1473    vr = {}
1474    for astName, (otName, isDevice) in _VALUEREC_ATTRS.items():
1475        val = getattr(v, astName, None)
1476        if val:
1477            vr[otName] = otl.buildDevice(dict(val)) if isDevice else val
1478    if pairPosContext and not vr:
1479        vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
1480    valRec = otl.buildValue(vr)
1481    return valRec
1482