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