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