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