You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			1809 lines
		
	
	
		
			72 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			1809 lines
		
	
	
		
			72 KiB
		
	
	
	
		
			Python
		
	
from fontTools.misc import sstruct
 | 
						|
from fontTools.misc.textTools import Tag, tostr, binary2num, safeEval
 | 
						|
from fontTools.feaLib.error import FeatureLibError
 | 
						|
from fontTools.feaLib.lookupDebugInfo import (
 | 
						|
    LookupDebugInfo,
 | 
						|
    LOOKUP_DEBUG_INFO_KEY,
 | 
						|
    LOOKUP_DEBUG_ENV_VAR,
 | 
						|
)
 | 
						|
from fontTools.feaLib.parser import Parser
 | 
						|
from fontTools.feaLib.ast import FeatureFile
 | 
						|
from fontTools.feaLib.variableScalar import VariableScalar
 | 
						|
from fontTools.otlLib import builder as otl
 | 
						|
from fontTools.otlLib.maxContextCalc import maxCtxFont
 | 
						|
from fontTools.ttLib import newTable, getTableModule
 | 
						|
from fontTools.ttLib.tables import otBase, otTables
 | 
						|
from fontTools.otlLib.builder import (
 | 
						|
    AlternateSubstBuilder,
 | 
						|
    ChainContextPosBuilder,
 | 
						|
    ChainContextSubstBuilder,
 | 
						|
    LigatureSubstBuilder,
 | 
						|
    MultipleSubstBuilder,
 | 
						|
    CursivePosBuilder,
 | 
						|
    MarkBasePosBuilder,
 | 
						|
    MarkLigPosBuilder,
 | 
						|
    MarkMarkPosBuilder,
 | 
						|
    ReverseChainSingleSubstBuilder,
 | 
						|
    SingleSubstBuilder,
 | 
						|
    ClassPairPosSubtableBuilder,
 | 
						|
    PairPosBuilder,
 | 
						|
    SinglePosBuilder,
 | 
						|
    ChainContextualRule,
 | 
						|
    AnySubstBuilder,
 | 
						|
)
 | 
						|
from fontTools.otlLib.error import OpenTypeLibError
 | 
						|
from fontTools.varLib.errors import VarLibError
 | 
						|
from fontTools.varLib.varStore import OnlineVarStoreBuilder
 | 
						|
from fontTools.varLib.builder import buildVarDevTable
 | 
						|
from fontTools.varLib.featureVars import addFeatureVariationsRaw
 | 
						|
from fontTools.varLib.models import normalizeValue, piecewiseLinearMap
 | 
						|
from collections import defaultdict
 | 
						|
import copy
 | 
						|
import itertools
 | 
						|
from io import StringIO
 | 
						|
import logging
 | 
						|
import warnings
 | 
						|
import os
 | 
						|
 | 
						|
 | 
						|
log = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
def addOpenTypeFeatures(font, featurefile, tables=None, debug=False):
 | 
						|
    """Add features from a file to a font. Note that this replaces any features
 | 
						|
    currently present.
 | 
						|
 | 
						|
    Args:
 | 
						|
        font (feaLib.ttLib.TTFont): The font object.
 | 
						|
        featurefile: Either a path or file object (in which case we
 | 
						|
            parse it into an AST), or a pre-parsed AST instance.
 | 
						|
        tables: If passed, restrict the set of affected tables to those in the
 | 
						|
            list.
 | 
						|
        debug: Whether to add source debugging information to the font in the
 | 
						|
            ``Debg`` table
 | 
						|
 | 
						|
    """
 | 
						|
    builder = Builder(font, featurefile)
 | 
						|
    builder.build(tables=tables, debug=debug)
 | 
						|
 | 
						|
 | 
						|
def addOpenTypeFeaturesFromString(
 | 
						|
    font, features, filename=None, tables=None, debug=False
 | 
						|
):
 | 
						|
    """Add features from a string to a font. Note that this replaces any
 | 
						|
    features currently present.
 | 
						|
 | 
						|
    Args:
 | 
						|
        font (feaLib.ttLib.TTFont): The font object.
 | 
						|
        features: A string containing feature code.
 | 
						|
        filename: The directory containing ``filename`` is used as the root of
 | 
						|
            relative ``include()`` paths; if ``None`` is provided, the current
 | 
						|
            directory is assumed.
 | 
						|
        tables: If passed, restrict the set of affected tables to those in the
 | 
						|
            list.
 | 
						|
        debug: Whether to add source debugging information to the font in the
 | 
						|
            ``Debg`` table
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    featurefile = StringIO(tostr(features))
 | 
						|
    if filename:
 | 
						|
        featurefile.name = filename
 | 
						|
    addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug)
 | 
						|
 | 
						|
 | 
						|
class Builder(object):
 | 
						|
    supportedTables = frozenset(
 | 
						|
        Tag(tag)
 | 
						|
        for tag in [
 | 
						|
            "BASE",
 | 
						|
            "GDEF",
 | 
						|
            "GPOS",
 | 
						|
            "GSUB",
 | 
						|
            "OS/2",
 | 
						|
            "head",
 | 
						|
            "hhea",
 | 
						|
            "name",
 | 
						|
            "vhea",
 | 
						|
            "STAT",
 | 
						|
        ]
 | 
						|
    )
 | 
						|
 | 
						|
    def __init__(self, font, featurefile):
 | 
						|
        self.font = font
 | 
						|
        # 'featurefile' can be either a path or file object (in which case we
 | 
						|
        # parse it into an AST), or a pre-parsed AST instance
 | 
						|
        if isinstance(featurefile, FeatureFile):
 | 
						|
            self.parseTree, self.file = featurefile, None
 | 
						|
        else:
 | 
						|
            self.parseTree, self.file = None, featurefile
 | 
						|
        self.glyphMap = font.getReverseGlyphMap()
 | 
						|
        self.varstorebuilder = None
 | 
						|
        if "fvar" in font:
 | 
						|
            self.axes = font["fvar"].axes
 | 
						|
            self.varstorebuilder = OnlineVarStoreBuilder(
 | 
						|
                [ax.axisTag for ax in self.axes]
 | 
						|
            )
 | 
						|
        self.default_language_systems_ = set()
 | 
						|
        self.script_ = None
 | 
						|
        self.lookupflag_ = 0
 | 
						|
        self.lookupflag_markFilterSet_ = None
 | 
						|
        self.use_extension_ = False
 | 
						|
        self.language_systems = set()
 | 
						|
        self.seen_non_DFLT_script_ = False
 | 
						|
        self.named_lookups_ = {}
 | 
						|
        self.cur_lookup_ = None
 | 
						|
        self.cur_lookup_name_ = None
 | 
						|
        self.cur_feature_name_ = None
 | 
						|
        self.lookups_ = []
 | 
						|
        self.lookup_locations = {"GSUB": {}, "GPOS": {}}
 | 
						|
        self.features_ = {}  # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
 | 
						|
        self.required_features_ = {}  # ('latn', 'DEU ') --> 'scmp'
 | 
						|
        self.feature_variations_ = {}
 | 
						|
        # for feature 'aalt'
 | 
						|
        self.aalt_features_ = []  # [(location, featureName)*], for 'aalt'
 | 
						|
        self.aalt_location_ = None
 | 
						|
        self.aalt_alternates_ = {}
 | 
						|
        self.aalt_use_extension_ = False
 | 
						|
        # for 'featureNames'
 | 
						|
        self.featureNames_ = set()
 | 
						|
        self.featureNames_ids_ = {}
 | 
						|
        # for 'cvParameters'
 | 
						|
        self.cv_parameters_ = set()
 | 
						|
        self.cv_parameters_ids_ = {}
 | 
						|
        self.cv_num_named_params_ = {}
 | 
						|
        self.cv_characters_ = defaultdict(list)
 | 
						|
        # for feature 'size'
 | 
						|
        self.size_parameters_ = None
 | 
						|
        # for table 'head'
 | 
						|
        self.fontRevision_ = None  # 2.71
 | 
						|
        # for table 'name'
 | 
						|
        self.names_ = []
 | 
						|
        # for table 'BASE'
 | 
						|
        self.base_horiz_axis_ = None
 | 
						|
        self.base_vert_axis_ = None
 | 
						|
        # for table 'GDEF'
 | 
						|
        self.attachPoints_ = {}  # "a" --> {3, 7}
 | 
						|
        self.ligCaretCoords_ = {}  # "f_f_i" --> {300, 600}
 | 
						|
        self.ligCaretPoints_ = {}  # "f_f_i" --> {3, 7}
 | 
						|
        self.glyphClassDefs_ = {}  # "fi" --> (2, (file, line, column))
 | 
						|
        self.markAttach_ = {}  # "acute" --> (4, (file, line, column))
 | 
						|
        self.markAttachClassID_ = {}  # frozenset({"acute", "grave"}) --> 4
 | 
						|
        self.markFilterSets_ = {}  # frozenset({"acute", "grave"}) --> 4
 | 
						|
        # for table 'OS/2'
 | 
						|
        self.os2_ = {}
 | 
						|
        # for table 'hhea'
 | 
						|
        self.hhea_ = {}
 | 
						|
        # for table 'vhea'
 | 
						|
        self.vhea_ = {}
 | 
						|
        # for table 'STAT'
 | 
						|
        self.stat_ = {}
 | 
						|
        # for conditionsets
 | 
						|
        self.conditionsets_ = {}
 | 
						|
        # We will often use exactly the same locations (i.e. the font's masters)
 | 
						|
        # for a large number of variable scalars. Instead of creating a model
 | 
						|
        # for each, let's share the models.
 | 
						|
        self.model_cache = {}
 | 
						|
 | 
						|
    def build(self, tables=None, debug=False):
 | 
						|
        if self.parseTree is None:
 | 
						|
            self.parseTree = Parser(self.file, self.glyphMap).parse()
 | 
						|
        self.parseTree.build(self)
 | 
						|
        # by default, build all the supported tables
 | 
						|
        if tables is None:
 | 
						|
            tables = self.supportedTables
 | 
						|
        else:
 | 
						|
            tables = frozenset(tables)
 | 
						|
            unsupported = tables - self.supportedTables
 | 
						|
            if unsupported:
 | 
						|
                unsupported_string = ", ".join(sorted(unsupported))
 | 
						|
                raise NotImplementedError(
 | 
						|
                    "The following tables were requested but are unsupported: "
 | 
						|
                    f"{unsupported_string}."
 | 
						|
                )
 | 
						|
        if "GSUB" in tables:
 | 
						|
            self.build_feature_aalt_()
 | 
						|
        if "head" in tables:
 | 
						|
            self.build_head()
 | 
						|
        if "hhea" in tables:
 | 
						|
            self.build_hhea()
 | 
						|
        if "vhea" in tables:
 | 
						|
            self.build_vhea()
 | 
						|
        if "name" in tables:
 | 
						|
            self.build_name()
 | 
						|
        if "OS/2" in tables:
 | 
						|
            self.build_OS_2()
 | 
						|
        if "STAT" in tables:
 | 
						|
            self.build_STAT()
 | 
						|
        for tag in ("GPOS", "GSUB"):
 | 
						|
            if tag not in tables:
 | 
						|
                continue
 | 
						|
            table = self.makeTable(tag)
 | 
						|
            if self.feature_variations_:
 | 
						|
                self.makeFeatureVariations(table, tag)
 | 
						|
            if (
 | 
						|
                table.ScriptList.ScriptCount > 0
 | 
						|
                or table.FeatureList.FeatureCount > 0
 | 
						|
                or table.LookupList.LookupCount > 0
 | 
						|
            ):
 | 
						|
                fontTable = self.font[tag] = newTable(tag)
 | 
						|
                fontTable.table = table
 | 
						|
            elif tag in self.font:
 | 
						|
                del self.font[tag]
 | 
						|
        if any(tag in self.font for tag in ("GPOS", "GSUB")) and "OS/2" in self.font:
 | 
						|
            self.font["OS/2"].usMaxContext = maxCtxFont(self.font)
 | 
						|
        if "GDEF" in tables:
 | 
						|
            gdef = self.buildGDEF()
 | 
						|
            if gdef:
 | 
						|
                self.font["GDEF"] = gdef
 | 
						|
            elif "GDEF" in self.font:
 | 
						|
                del self.font["GDEF"]
 | 
						|
        if "BASE" in tables:
 | 
						|
            base = self.buildBASE()
 | 
						|
            if base:
 | 
						|
                self.font["BASE"] = base
 | 
						|
            elif "BASE" in self.font:
 | 
						|
                del self.font["BASE"]
 | 
						|
        if debug or os.environ.get(LOOKUP_DEBUG_ENV_VAR):
 | 
						|
            self.buildDebg()
 | 
						|
 | 
						|
    def get_chained_lookup_(self, location, builder_class):
 | 
						|
        result = builder_class(self.font, location)
 | 
						|
        result.lookupflag = self.lookupflag_
 | 
						|
        result.markFilterSet = self.lookupflag_markFilterSet_
 | 
						|
        result.extension = self.use_extension_
 | 
						|
        self.lookups_.append(result)
 | 
						|
        return result
 | 
						|
 | 
						|
    def add_lookup_to_feature_(self, lookup, feature_name):
 | 
						|
        for script, lang in self.language_systems:
 | 
						|
            key = (script, lang, feature_name)
 | 
						|
            self.features_.setdefault(key, []).append(lookup)
 | 
						|
 | 
						|
    def get_lookup_(self, location, builder_class, mapping=None):
 | 
						|
        if (
 | 
						|
            self.cur_lookup_
 | 
						|
            and type(self.cur_lookup_) == builder_class
 | 
						|
            and self.cur_lookup_.lookupflag == self.lookupflag_
 | 
						|
            and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_
 | 
						|
            and self.cur_lookup_.can_add_mapping(mapping)
 | 
						|
        ):
 | 
						|
            return self.cur_lookup_
 | 
						|
        if self.cur_lookup_name_ and self.cur_lookup_:
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Within a named lookup block, all rules must be of "
 | 
						|
                "the same lookup type and flag",
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        self.cur_lookup_ = builder_class(self.font, location)
 | 
						|
        self.cur_lookup_.lookupflag = self.lookupflag_
 | 
						|
        self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
 | 
						|
        self.cur_lookup_.extension = self.use_extension_
 | 
						|
        self.lookups_.append(self.cur_lookup_)
 | 
						|
        if self.cur_lookup_name_:
 | 
						|
            # We are starting a lookup rule inside a named lookup block.
 | 
						|
            self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_
 | 
						|
        if self.cur_feature_name_:
 | 
						|
            # We are starting a lookup rule inside a feature. This includes
 | 
						|
            # lookup rules inside named lookups inside features.
 | 
						|
            self.add_lookup_to_feature_(self.cur_lookup_, self.cur_feature_name_)
 | 
						|
        return self.cur_lookup_
 | 
						|
 | 
						|
    def build_feature_aalt_(self):
 | 
						|
        if not self.aalt_features_ and not self.aalt_alternates_:
 | 
						|
            return
 | 
						|
        # > alternate glyphs will be sorted in the order that the source features
 | 
						|
        # > are named in the aalt definition, not the order of the feature definitions
 | 
						|
        # > in the file. Alternates defined explicitly ... will precede all others.
 | 
						|
        # https://github.com/fonttools/fonttools/issues/836
 | 
						|
        alternates = {g: list(a) for g, a in self.aalt_alternates_.items()}
 | 
						|
        for location, name in self.aalt_features_ + [(None, "aalt")]:
 | 
						|
            feature = [
 | 
						|
                (script, lang, feature, lookups)
 | 
						|
                for (script, lang, feature), lookups in self.features_.items()
 | 
						|
                if feature == name
 | 
						|
            ]
 | 
						|
            # "aalt" does not have to specify its own lookups, but it might.
 | 
						|
            if not feature and name != "aalt":
 | 
						|
                warnings.warn("%s: Feature %s has not been defined" % (location, name))
 | 
						|
                continue
 | 
						|
            for script, lang, feature, lookups in feature:
 | 
						|
                for lookuplist in lookups:
 | 
						|
                    if not isinstance(lookuplist, list):
 | 
						|
                        lookuplist = [lookuplist]
 | 
						|
                    for lookup in lookuplist:
 | 
						|
                        for glyph, alts in lookup.getAlternateGlyphs().items():
 | 
						|
                            alts_for_glyph = alternates.setdefault(glyph, [])
 | 
						|
                            alts_for_glyph.extend(
 | 
						|
                                g for g in alts if g not in alts_for_glyph
 | 
						|
                            )
 | 
						|
        single = {
 | 
						|
            glyph: repl[0] for glyph, repl in alternates.items() if len(repl) == 1
 | 
						|
        }
 | 
						|
        multi = {glyph: repl for glyph, repl in alternates.items() if len(repl) > 1}
 | 
						|
        if not single and not multi:
 | 
						|
            return
 | 
						|
        self.features_ = {
 | 
						|
            (script, lang, feature): lookups
 | 
						|
            for (script, lang, feature), lookups in self.features_.items()
 | 
						|
            if feature != "aalt"
 | 
						|
        }
 | 
						|
        old_lookups = self.lookups_
 | 
						|
        self.lookups_ = []
 | 
						|
        self.start_feature(self.aalt_location_, "aalt", self.aalt_use_extension_)
 | 
						|
        if single:
 | 
						|
            single_lookup = self.get_lookup_(location, SingleSubstBuilder)
 | 
						|
            single_lookup.mapping = single
 | 
						|
        if multi:
 | 
						|
            multi_lookup = self.get_lookup_(location, AlternateSubstBuilder)
 | 
						|
            multi_lookup.alternates = multi
 | 
						|
        self.end_feature()
 | 
						|
        self.lookups_.extend(old_lookups)
 | 
						|
 | 
						|
    def build_head(self):
 | 
						|
        if not self.fontRevision_:
 | 
						|
            return
 | 
						|
        table = self.font.get("head")
 | 
						|
        if not table:  # this only happens for unit tests
 | 
						|
            table = self.font["head"] = newTable("head")
 | 
						|
            table.decompile(b"\0" * 54, self.font)
 | 
						|
            table.tableVersion = 1.0
 | 
						|
            table.magicNumber = 0x5F0F3CF5
 | 
						|
            table.created = table.modified = 3406620153  # 2011-12-13 11:22:33
 | 
						|
        table.fontRevision = self.fontRevision_
 | 
						|
 | 
						|
    def build_hhea(self):
 | 
						|
        if not self.hhea_:
 | 
						|
            return
 | 
						|
        table = self.font.get("hhea")
 | 
						|
        if not table:  # this only happens for unit tests
 | 
						|
            table = self.font["hhea"] = newTable("hhea")
 | 
						|
            table.decompile(b"\0" * 36, self.font)
 | 
						|
            table.tableVersion = 0x00010000
 | 
						|
        if "caretoffset" in self.hhea_:
 | 
						|
            table.caretOffset = self.hhea_["caretoffset"]
 | 
						|
        if "ascender" in self.hhea_:
 | 
						|
            table.ascent = self.hhea_["ascender"]
 | 
						|
        if "descender" in self.hhea_:
 | 
						|
            table.descent = self.hhea_["descender"]
 | 
						|
        if "linegap" in self.hhea_:
 | 
						|
            table.lineGap = self.hhea_["linegap"]
 | 
						|
 | 
						|
    def build_vhea(self):
 | 
						|
        if not self.vhea_:
 | 
						|
            return
 | 
						|
        table = self.font.get("vhea")
 | 
						|
        if not table:  # this only happens for unit tests
 | 
						|
            table = self.font["vhea"] = newTable("vhea")
 | 
						|
            table.decompile(b"\0" * 36, self.font)
 | 
						|
            table.tableVersion = 0x00011000
 | 
						|
        if "verttypoascender" in self.vhea_:
 | 
						|
            table.ascent = self.vhea_["verttypoascender"]
 | 
						|
        if "verttypodescender" in self.vhea_:
 | 
						|
            table.descent = self.vhea_["verttypodescender"]
 | 
						|
        if "verttypolinegap" in self.vhea_:
 | 
						|
            table.lineGap = self.vhea_["verttypolinegap"]
 | 
						|
 | 
						|
    def get_user_name_id(self, table):
 | 
						|
        # Try to find first unused font-specific name id
 | 
						|
        nameIDs = [name.nameID for name in table.names]
 | 
						|
        for user_name_id in range(256, 32767):
 | 
						|
            if user_name_id not in nameIDs:
 | 
						|
                return user_name_id
 | 
						|
 | 
						|
    def buildFeatureParams(self, tag):
 | 
						|
        params = None
 | 
						|
        if tag == "size":
 | 
						|
            params = otTables.FeatureParamsSize()
 | 
						|
            (
 | 
						|
                params.DesignSize,
 | 
						|
                params.SubfamilyID,
 | 
						|
                params.RangeStart,
 | 
						|
                params.RangeEnd,
 | 
						|
            ) = self.size_parameters_
 | 
						|
            if tag in self.featureNames_ids_:
 | 
						|
                params.SubfamilyNameID = self.featureNames_ids_[tag]
 | 
						|
            else:
 | 
						|
                params.SubfamilyNameID = 0
 | 
						|
        elif tag in self.featureNames_:
 | 
						|
            if not self.featureNames_ids_:
 | 
						|
                # name table wasn't selected among the tables to build; skip
 | 
						|
                pass
 | 
						|
            else:
 | 
						|
                assert tag in self.featureNames_ids_
 | 
						|
                params = otTables.FeatureParamsStylisticSet()
 | 
						|
                params.Version = 0
 | 
						|
                params.UINameID = self.featureNames_ids_[tag]
 | 
						|
        elif tag in self.cv_parameters_:
 | 
						|
            params = otTables.FeatureParamsCharacterVariants()
 | 
						|
            params.Format = 0
 | 
						|
            params.FeatUILabelNameID = self.cv_parameters_ids_.get(
 | 
						|
                (tag, "FeatUILabelNameID"), 0
 | 
						|
            )
 | 
						|
            params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get(
 | 
						|
                (tag, "FeatUITooltipTextNameID"), 0
 | 
						|
            )
 | 
						|
            params.SampleTextNameID = self.cv_parameters_ids_.get(
 | 
						|
                (tag, "SampleTextNameID"), 0
 | 
						|
            )
 | 
						|
            params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0)
 | 
						|
            params.FirstParamUILabelNameID = self.cv_parameters_ids_.get(
 | 
						|
                (tag, "ParamUILabelNameID_0"), 0
 | 
						|
            )
 | 
						|
            params.CharCount = len(self.cv_characters_[tag])
 | 
						|
            params.Character = self.cv_characters_[tag]
 | 
						|
        return params
 | 
						|
 | 
						|
    def build_name(self):
 | 
						|
        if not self.names_:
 | 
						|
            return
 | 
						|
        table = self.font.get("name")
 | 
						|
        if not table:  # this only happens for unit tests
 | 
						|
            table = self.font["name"] = newTable("name")
 | 
						|
            table.names = []
 | 
						|
        for name in self.names_:
 | 
						|
            nameID, platformID, platEncID, langID, string = name
 | 
						|
            # For featureNames block, nameID is 'feature tag'
 | 
						|
            # For cvParameters blocks, nameID is ('feature tag', 'block name')
 | 
						|
            if not isinstance(nameID, int):
 | 
						|
                tag = nameID
 | 
						|
                if tag in self.featureNames_:
 | 
						|
                    if tag not in self.featureNames_ids_:
 | 
						|
                        self.featureNames_ids_[tag] = self.get_user_name_id(table)
 | 
						|
                        assert self.featureNames_ids_[tag] is not None
 | 
						|
                    nameID = self.featureNames_ids_[tag]
 | 
						|
                elif tag[0] in self.cv_parameters_:
 | 
						|
                    if tag not in self.cv_parameters_ids_:
 | 
						|
                        self.cv_parameters_ids_[tag] = self.get_user_name_id(table)
 | 
						|
                        assert self.cv_parameters_ids_[tag] is not None
 | 
						|
                    nameID = self.cv_parameters_ids_[tag]
 | 
						|
            table.setName(string, nameID, platformID, platEncID, langID)
 | 
						|
        table.names.sort()
 | 
						|
 | 
						|
    def build_OS_2(self):
 | 
						|
        if not self.os2_:
 | 
						|
            return
 | 
						|
        table = self.font.get("OS/2")
 | 
						|
        if not table:  # this only happens for unit tests
 | 
						|
            table = self.font["OS/2"] = newTable("OS/2")
 | 
						|
            data = b"\0" * sstruct.calcsize(getTableModule("OS/2").OS2_format_0)
 | 
						|
            table.decompile(data, self.font)
 | 
						|
        version = 0
 | 
						|
        if "fstype" in self.os2_:
 | 
						|
            table.fsType = self.os2_["fstype"]
 | 
						|
        if "panose" in self.os2_:
 | 
						|
            panose = getTableModule("OS/2").Panose()
 | 
						|
            (
 | 
						|
                panose.bFamilyType,
 | 
						|
                panose.bSerifStyle,
 | 
						|
                panose.bWeight,
 | 
						|
                panose.bProportion,
 | 
						|
                panose.bContrast,
 | 
						|
                panose.bStrokeVariation,
 | 
						|
                panose.bArmStyle,
 | 
						|
                panose.bLetterForm,
 | 
						|
                panose.bMidline,
 | 
						|
                panose.bXHeight,
 | 
						|
            ) = self.os2_["panose"]
 | 
						|
            table.panose = panose
 | 
						|
        if "typoascender" in self.os2_:
 | 
						|
            table.sTypoAscender = self.os2_["typoascender"]
 | 
						|
        if "typodescender" in self.os2_:
 | 
						|
            table.sTypoDescender = self.os2_["typodescender"]
 | 
						|
        if "typolinegap" in self.os2_:
 | 
						|
            table.sTypoLineGap = self.os2_["typolinegap"]
 | 
						|
        if "winascent" in self.os2_:
 | 
						|
            table.usWinAscent = self.os2_["winascent"]
 | 
						|
        if "windescent" in self.os2_:
 | 
						|
            table.usWinDescent = self.os2_["windescent"]
 | 
						|
        if "vendor" in self.os2_:
 | 
						|
            table.achVendID = safeEval("'''" + self.os2_["vendor"] + "'''")
 | 
						|
        if "weightclass" in self.os2_:
 | 
						|
            table.usWeightClass = self.os2_["weightclass"]
 | 
						|
        if "widthclass" in self.os2_:
 | 
						|
            table.usWidthClass = self.os2_["widthclass"]
 | 
						|
        if "unicoderange" in self.os2_:
 | 
						|
            table.setUnicodeRanges(self.os2_["unicoderange"])
 | 
						|
        if "codepagerange" in self.os2_:
 | 
						|
            pages = self.build_codepages_(self.os2_["codepagerange"])
 | 
						|
            table.ulCodePageRange1, table.ulCodePageRange2 = pages
 | 
						|
            version = 1
 | 
						|
        if "xheight" in self.os2_:
 | 
						|
            table.sxHeight = self.os2_["xheight"]
 | 
						|
            version = 2
 | 
						|
        if "capheight" in self.os2_:
 | 
						|
            table.sCapHeight = self.os2_["capheight"]
 | 
						|
            version = 2
 | 
						|
        if "loweropsize" in self.os2_:
 | 
						|
            table.usLowerOpticalPointSize = self.os2_["loweropsize"]
 | 
						|
            version = 5
 | 
						|
        if "upperopsize" in self.os2_:
 | 
						|
            table.usUpperOpticalPointSize = self.os2_["upperopsize"]
 | 
						|
            version = 5
 | 
						|
 | 
						|
        def checkattr(table, attrs):
 | 
						|
            for attr in attrs:
 | 
						|
                if not hasattr(table, attr):
 | 
						|
                    setattr(table, attr, 0)
 | 
						|
 | 
						|
        table.version = max(version, table.version)
 | 
						|
        # this only happens for unit tests
 | 
						|
        if version >= 1:
 | 
						|
            checkattr(table, ("ulCodePageRange1", "ulCodePageRange2"))
 | 
						|
        if version >= 2:
 | 
						|
            checkattr(
 | 
						|
                table,
 | 
						|
                (
 | 
						|
                    "sxHeight",
 | 
						|
                    "sCapHeight",
 | 
						|
                    "usDefaultChar",
 | 
						|
                    "usBreakChar",
 | 
						|
                    "usMaxContext",
 | 
						|
                ),
 | 
						|
            )
 | 
						|
        if version >= 5:
 | 
						|
            checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))
 | 
						|
 | 
						|
    def setElidedFallbackName(self, value, location):
 | 
						|
        # ElidedFallbackName is a convenience method for setting
 | 
						|
        # ElidedFallbackNameID so only one can be allowed
 | 
						|
        for token in ("ElidedFallbackName", "ElidedFallbackNameID"):
 | 
						|
            if token in self.stat_:
 | 
						|
                raise FeatureLibError(
 | 
						|
                    f"{token} is already set.",
 | 
						|
                    location,
 | 
						|
                )
 | 
						|
        if isinstance(value, int):
 | 
						|
            self.stat_["ElidedFallbackNameID"] = value
 | 
						|
        elif isinstance(value, list):
 | 
						|
            self.stat_["ElidedFallbackName"] = value
 | 
						|
        else:
 | 
						|
            raise AssertionError(value)
 | 
						|
 | 
						|
    def addDesignAxis(self, designAxis, location):
 | 
						|
        if "DesignAxes" not in self.stat_:
 | 
						|
            self.stat_["DesignAxes"] = []
 | 
						|
        if designAxis.tag in (r.tag for r in self.stat_["DesignAxes"]):
 | 
						|
            raise FeatureLibError(
 | 
						|
                f'DesignAxis already defined for tag "{designAxis.tag}".',
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        if designAxis.axisOrder in (r.axisOrder for r in self.stat_["DesignAxes"]):
 | 
						|
            raise FeatureLibError(
 | 
						|
                f"DesignAxis already defined for axis number {designAxis.axisOrder}.",
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        self.stat_["DesignAxes"].append(designAxis)
 | 
						|
 | 
						|
    def addAxisValueRecord(self, axisValueRecord, location):
 | 
						|
        if "AxisValueRecords" not in self.stat_:
 | 
						|
            self.stat_["AxisValueRecords"] = []
 | 
						|
        # Check for duplicate AxisValueRecords
 | 
						|
        for record_ in self.stat_["AxisValueRecords"]:
 | 
						|
            if (
 | 
						|
                {n.asFea() for n in record_.names}
 | 
						|
                == {n.asFea() for n in axisValueRecord.names}
 | 
						|
                and {n.asFea() for n in record_.locations}
 | 
						|
                == {n.asFea() for n in axisValueRecord.locations}
 | 
						|
                and record_.flags == axisValueRecord.flags
 | 
						|
            ):
 | 
						|
                raise FeatureLibError(
 | 
						|
                    "An AxisValueRecord with these values is already defined.",
 | 
						|
                    location,
 | 
						|
                )
 | 
						|
        self.stat_["AxisValueRecords"].append(axisValueRecord)
 | 
						|
 | 
						|
    def build_STAT(self):
 | 
						|
        if not self.stat_:
 | 
						|
            return
 | 
						|
 | 
						|
        axes = self.stat_.get("DesignAxes")
 | 
						|
        if not axes:
 | 
						|
            raise FeatureLibError("DesignAxes not defined", None)
 | 
						|
        axisValueRecords = self.stat_.get("AxisValueRecords")
 | 
						|
        axisValues = {}
 | 
						|
        format4_locations = []
 | 
						|
        for tag in axes:
 | 
						|
            axisValues[tag.tag] = []
 | 
						|
        if axisValueRecords is not None:
 | 
						|
            for avr in axisValueRecords:
 | 
						|
                valuesDict = {}
 | 
						|
                if avr.flags > 0:
 | 
						|
                    valuesDict["flags"] = avr.flags
 | 
						|
                if len(avr.locations) == 1:
 | 
						|
                    location = avr.locations[0]
 | 
						|
                    values = location.values
 | 
						|
                    if len(values) == 1:  # format1
 | 
						|
                        valuesDict.update({"value": values[0], "name": avr.names})
 | 
						|
                    if len(values) == 2:  # format3
 | 
						|
                        valuesDict.update(
 | 
						|
                            {
 | 
						|
                                "value": values[0],
 | 
						|
                                "linkedValue": values[1],
 | 
						|
                                "name": avr.names,
 | 
						|
                            }
 | 
						|
                        )
 | 
						|
                    if len(values) == 3:  # format2
 | 
						|
                        nominal, minVal, maxVal = values
 | 
						|
                        valuesDict.update(
 | 
						|
                            {
 | 
						|
                                "nominalValue": nominal,
 | 
						|
                                "rangeMinValue": minVal,
 | 
						|
                                "rangeMaxValue": maxVal,
 | 
						|
                                "name": avr.names,
 | 
						|
                            }
 | 
						|
                        )
 | 
						|
                    axisValues[location.tag].append(valuesDict)
 | 
						|
                else:
 | 
						|
                    valuesDict.update(
 | 
						|
                        {
 | 
						|
                            "location": {i.tag: i.values[0] for i in avr.locations},
 | 
						|
                            "name": avr.names,
 | 
						|
                        }
 | 
						|
                    )
 | 
						|
                    format4_locations.append(valuesDict)
 | 
						|
 | 
						|
        designAxes = [
 | 
						|
            {
 | 
						|
                "ordering": a.axisOrder,
 | 
						|
                "tag": a.tag,
 | 
						|
                "name": a.names,
 | 
						|
                "values": axisValues[a.tag],
 | 
						|
            }
 | 
						|
            for a in axes
 | 
						|
        ]
 | 
						|
 | 
						|
        nameTable = self.font.get("name")
 | 
						|
        if not nameTable:  # this only happens for unit tests
 | 
						|
            nameTable = self.font["name"] = newTable("name")
 | 
						|
            nameTable.names = []
 | 
						|
 | 
						|
        if "ElidedFallbackNameID" in self.stat_:
 | 
						|
            nameID = self.stat_["ElidedFallbackNameID"]
 | 
						|
            name = nameTable.getDebugName(nameID)
 | 
						|
            if not name:
 | 
						|
                raise FeatureLibError(
 | 
						|
                    f"ElidedFallbackNameID {nameID} points "
 | 
						|
                    "to a nameID that does not exist in the "
 | 
						|
                    '"name" table',
 | 
						|
                    None,
 | 
						|
                )
 | 
						|
        elif "ElidedFallbackName" in self.stat_:
 | 
						|
            nameID = self.stat_["ElidedFallbackName"]
 | 
						|
 | 
						|
        otl.buildStatTable(
 | 
						|
            self.font,
 | 
						|
            designAxes,
 | 
						|
            locations=format4_locations,
 | 
						|
            elidedFallbackName=nameID,
 | 
						|
        )
 | 
						|
 | 
						|
    def build_codepages_(self, pages):
 | 
						|
        pages2bits = {
 | 
						|
            1252: 0,
 | 
						|
            1250: 1,
 | 
						|
            1251: 2,
 | 
						|
            1253: 3,
 | 
						|
            1254: 4,
 | 
						|
            1255: 5,
 | 
						|
            1256: 6,
 | 
						|
            1257: 7,
 | 
						|
            1258: 8,
 | 
						|
            874: 16,
 | 
						|
            932: 17,
 | 
						|
            936: 18,
 | 
						|
            949: 19,
 | 
						|
            950: 20,
 | 
						|
            1361: 21,
 | 
						|
            869: 48,
 | 
						|
            866: 49,
 | 
						|
            865: 50,
 | 
						|
            864: 51,
 | 
						|
            863: 52,
 | 
						|
            862: 53,
 | 
						|
            861: 54,
 | 
						|
            860: 55,
 | 
						|
            857: 56,
 | 
						|
            855: 57,
 | 
						|
            852: 58,
 | 
						|
            775: 59,
 | 
						|
            737: 60,
 | 
						|
            708: 61,
 | 
						|
            850: 62,
 | 
						|
            437: 63,
 | 
						|
        }
 | 
						|
        bits = [pages2bits[p] for p in pages if p in pages2bits]
 | 
						|
        pages = []
 | 
						|
        for i in range(2):
 | 
						|
            pages.append("")
 | 
						|
            for j in range(i * 32, (i + 1) * 32):
 | 
						|
                if j in bits:
 | 
						|
                    pages[i] += "1"
 | 
						|
                else:
 | 
						|
                    pages[i] += "0"
 | 
						|
        return [binary2num(p[::-1]) for p in pages]
 | 
						|
 | 
						|
    def buildBASE(self):
 | 
						|
        if not self.base_horiz_axis_ and not self.base_vert_axis_:
 | 
						|
            return None
 | 
						|
        base = otTables.BASE()
 | 
						|
        base.Version = 0x00010000
 | 
						|
        base.HorizAxis = self.buildBASEAxis(self.base_horiz_axis_)
 | 
						|
        base.VertAxis = self.buildBASEAxis(self.base_vert_axis_)
 | 
						|
 | 
						|
        result = newTable("BASE")
 | 
						|
        result.table = base
 | 
						|
        return result
 | 
						|
 | 
						|
    def buildBASECoord(self, c):
 | 
						|
        coord = otTables.BaseCoord()
 | 
						|
        coord.Format = 1
 | 
						|
        coord.Coordinate = c
 | 
						|
        return coord
 | 
						|
 | 
						|
    def buildBASEAxis(self, axis):
 | 
						|
        if not axis:
 | 
						|
            return
 | 
						|
        bases, scripts, minmax = axis
 | 
						|
        axis = otTables.Axis()
 | 
						|
        axis.BaseTagList = otTables.BaseTagList()
 | 
						|
        axis.BaseTagList.BaselineTag = bases
 | 
						|
        axis.BaseTagList.BaseTagCount = len(bases)
 | 
						|
        axis.BaseScriptList = otTables.BaseScriptList()
 | 
						|
        axis.BaseScriptList.BaseScriptRecord = []
 | 
						|
        axis.BaseScriptList.BaseScriptCount = len(scripts)
 | 
						|
        for script in sorted(scripts):
 | 
						|
            minmax_for_script = [
 | 
						|
                record[1:] for record in minmax if record[0] == script[0]
 | 
						|
            ]
 | 
						|
            record = otTables.BaseScriptRecord()
 | 
						|
            record.BaseScriptTag = script[0]
 | 
						|
            record.BaseScript = otTables.BaseScript()
 | 
						|
            record.BaseScript.BaseValues = otTables.BaseValues()
 | 
						|
            record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
 | 
						|
            record.BaseScript.BaseValues.BaseCoord = []
 | 
						|
            record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
 | 
						|
            record.BaseScript.BaseLangSysRecord = []
 | 
						|
 | 
						|
            for c in script[2]:
 | 
						|
                record.BaseScript.BaseValues.BaseCoord.append(self.buildBASECoord(c))
 | 
						|
            for language, min_coord, max_coord in minmax_for_script:
 | 
						|
                minmax_record = otTables.MinMax()
 | 
						|
                minmax_record.MinCoord = self.buildBASECoord(min_coord)
 | 
						|
                minmax_record.MaxCoord = self.buildBASECoord(max_coord)
 | 
						|
                minmax_record.FeatMinMaxCount = 0
 | 
						|
                if language == "dflt":
 | 
						|
                    record.BaseScript.DefaultMinMax = minmax_record
 | 
						|
                else:
 | 
						|
                    lang_record = otTables.BaseLangSysRecord()
 | 
						|
                    lang_record.BaseLangSysTag = language
 | 
						|
                    lang_record.MinMax = minmax_record
 | 
						|
                    record.BaseScript.BaseLangSysRecord.append(lang_record)
 | 
						|
            record.BaseScript.BaseLangSysCount = len(
 | 
						|
                record.BaseScript.BaseLangSysRecord
 | 
						|
            )
 | 
						|
            axis.BaseScriptList.BaseScriptRecord.append(record)
 | 
						|
        return axis
 | 
						|
 | 
						|
    def buildGDEF(self):
 | 
						|
        gdef = otTables.GDEF()
 | 
						|
        gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_()
 | 
						|
        gdef.AttachList = otl.buildAttachList(self.attachPoints_, self.glyphMap)
 | 
						|
        gdef.LigCaretList = otl.buildLigCaretList(
 | 
						|
            self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap
 | 
						|
        )
 | 
						|
        gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
 | 
						|
        gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
 | 
						|
        gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
 | 
						|
        if self.varstorebuilder:
 | 
						|
            store = self.varstorebuilder.finish()
 | 
						|
            if store:
 | 
						|
                gdef.Version = 0x00010003
 | 
						|
                gdef.VarStore = store
 | 
						|
                varidx_map = store.optimize()
 | 
						|
 | 
						|
                gdef.remap_device_varidxes(varidx_map)
 | 
						|
                if "GPOS" in self.font:
 | 
						|
                    self.font["GPOS"].table.remap_device_varidxes(varidx_map)
 | 
						|
            self.model_cache.clear()
 | 
						|
        if any(
 | 
						|
            (
 | 
						|
                gdef.GlyphClassDef,
 | 
						|
                gdef.AttachList,
 | 
						|
                gdef.LigCaretList,
 | 
						|
                gdef.MarkAttachClassDef,
 | 
						|
                gdef.MarkGlyphSetsDef,
 | 
						|
            )
 | 
						|
        ) or hasattr(gdef, "VarStore"):
 | 
						|
            result = newTable("GDEF")
 | 
						|
            result.table = gdef
 | 
						|
            return result
 | 
						|
        else:
 | 
						|
            return None
 | 
						|
 | 
						|
    def buildGDEFGlyphClassDef_(self):
 | 
						|
        if self.glyphClassDefs_:
 | 
						|
            classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()}
 | 
						|
        else:
 | 
						|
            classes = {}
 | 
						|
            for lookup in self.lookups_:
 | 
						|
                classes.update(lookup.inferGlyphClasses())
 | 
						|
            for markClass in self.parseTree.markClasses.values():
 | 
						|
                for markClassDef in markClass.definitions:
 | 
						|
                    for glyph in markClassDef.glyphSet():
 | 
						|
                        classes[glyph] = 3
 | 
						|
        if classes:
 | 
						|
            result = otTables.GlyphClassDef()
 | 
						|
            result.classDefs = classes
 | 
						|
            return result
 | 
						|
        else:
 | 
						|
            return None
 | 
						|
 | 
						|
    def buildGDEFMarkAttachClassDef_(self):
 | 
						|
        classDefs = {g: c for g, (c, _) in self.markAttach_.items()}
 | 
						|
        if not classDefs:
 | 
						|
            return None
 | 
						|
        result = otTables.MarkAttachClassDef()
 | 
						|
        result.classDefs = classDefs
 | 
						|
        return result
 | 
						|
 | 
						|
    def buildGDEFMarkGlyphSetsDef_(self):
 | 
						|
        sets = []
 | 
						|
        for glyphs, id_ in sorted(
 | 
						|
            self.markFilterSets_.items(), key=lambda item: item[1]
 | 
						|
        ):
 | 
						|
            sets.append(glyphs)
 | 
						|
        return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)
 | 
						|
 | 
						|
    def buildDebg(self):
 | 
						|
        if "Debg" not in self.font:
 | 
						|
            self.font["Debg"] = newTable("Debg")
 | 
						|
            self.font["Debg"].data = {}
 | 
						|
        self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations
 | 
						|
 | 
						|
    def buildLookups_(self, tag):
 | 
						|
        assert tag in ("GPOS", "GSUB"), tag
 | 
						|
        for lookup in self.lookups_:
 | 
						|
            lookup.lookup_index = None
 | 
						|
        lookups = []
 | 
						|
        for lookup in self.lookups_:
 | 
						|
            if lookup.table != tag:
 | 
						|
                continue
 | 
						|
            name = self.get_lookup_name_(lookup)
 | 
						|
            resolved = lookup.promote_lookup_type(is_named_lookup=name is not None)
 | 
						|
            if resolved is None:
 | 
						|
                raise FeatureLibError(
 | 
						|
                    "Within a named lookup block, all rules must be of "
 | 
						|
                    "the same lookup type and flag",
 | 
						|
                    lookup.location,
 | 
						|
                )
 | 
						|
            for l in resolved:
 | 
						|
                lookup.lookup_index = len(lookups)
 | 
						|
                self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
 | 
						|
                    location=str(lookup.location),
 | 
						|
                    name=name,
 | 
						|
                    feature=None,
 | 
						|
                )
 | 
						|
                lookups.append(l)
 | 
						|
        otLookups = []
 | 
						|
        for l in lookups:
 | 
						|
            try:
 | 
						|
                otLookups.append(l.build())
 | 
						|
            except OpenTypeLibError as e:
 | 
						|
                raise FeatureLibError(str(e), e.location) from e
 | 
						|
            except Exception as e:
 | 
						|
                location = self.lookup_locations[tag][str(l.lookup_index)].location
 | 
						|
                raise FeatureLibError(str(e), location) from e
 | 
						|
        return otLookups
 | 
						|
 | 
						|
    def makeTable(self, tag):
 | 
						|
        table = getattr(otTables, tag, None)()
 | 
						|
        table.Version = 0x00010000
 | 
						|
        table.ScriptList = otTables.ScriptList()
 | 
						|
        table.ScriptList.ScriptRecord = []
 | 
						|
        table.FeatureList = otTables.FeatureList()
 | 
						|
        table.FeatureList.FeatureRecord = []
 | 
						|
        table.LookupList = otTables.LookupList()
 | 
						|
        table.LookupList.Lookup = self.buildLookups_(tag)
 | 
						|
 | 
						|
        # Build a table for mapping (tag, lookup_indices) to feature_index.
 | 
						|
        # For example, ('liga', (2,3,7)) --> 23.
 | 
						|
        feature_indices = {}
 | 
						|
        required_feature_indices = {}  # ('latn', 'DEU') --> 23
 | 
						|
        scripts = {}  # 'latn' --> {'DEU': [23, 24]} for feature #23,24
 | 
						|
        # Sort the feature table by feature tag:
 | 
						|
        # https://github.com/fonttools/fonttools/issues/568
 | 
						|
        sortFeatureTag = lambda f: (f[0][2], f[0][1], f[0][0], f[1])
 | 
						|
        for key, lookups in sorted(self.features_.items(), key=sortFeatureTag):
 | 
						|
            script, lang, feature_tag = key
 | 
						|
            # l.lookup_index will be None when a lookup is not needed
 | 
						|
            # for the table under construction. For example, substitution
 | 
						|
            # rules will have no lookup_index while building GPOS tables.
 | 
						|
            # We also deduplicate lookup indices, as they only get applied once
 | 
						|
            # within a given feature:
 | 
						|
            # https://github.com/fonttools/fonttools/issues/2946
 | 
						|
            lookup_indices = tuple(
 | 
						|
                dict.fromkeys(
 | 
						|
                    l.lookup_index for l in lookups if l.lookup_index is not None
 | 
						|
                )
 | 
						|
            )
 | 
						|
            # order doesn't matter, but lookup_indices preserves it.
 | 
						|
            # We want to combine identical sets of lookups (order doesn't matter)
 | 
						|
            # but also respect the order provided by the user (although there's
 | 
						|
            # a reasonable argument to just sort and dedupe, which fontc does)
 | 
						|
            lookup_key = frozenset(lookup_indices)
 | 
						|
 | 
						|
            size_feature = tag == "GPOS" and feature_tag == "size"
 | 
						|
            force_feature = self.any_feature_variations(feature_tag, tag)
 | 
						|
            if len(lookup_indices) == 0 and not size_feature and not force_feature:
 | 
						|
                continue
 | 
						|
 | 
						|
            for ix in lookup_indices:
 | 
						|
                try:
 | 
						|
                    self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
 | 
						|
                        str(ix)
 | 
						|
                    ]._replace(feature=key)
 | 
						|
                except KeyError:
 | 
						|
                    warnings.warn(
 | 
						|
                        "feaLib.Builder subclass needs upgrading to "
 | 
						|
                        "stash debug information. See fonttools#2065."
 | 
						|
                    )
 | 
						|
 | 
						|
            feature_key = (feature_tag, lookup_key)
 | 
						|
            feature_index = feature_indices.get(feature_key)
 | 
						|
            if feature_index is None:
 | 
						|
                feature_index = len(table.FeatureList.FeatureRecord)
 | 
						|
                frec = otTables.FeatureRecord()
 | 
						|
                frec.FeatureTag = feature_tag
 | 
						|
                frec.Feature = otTables.Feature()
 | 
						|
                frec.Feature.FeatureParams = self.buildFeatureParams(feature_tag)
 | 
						|
                frec.Feature.LookupListIndex = list(lookup_indices)
 | 
						|
                frec.Feature.LookupCount = len(lookup_indices)
 | 
						|
                table.FeatureList.FeatureRecord.append(frec)
 | 
						|
                feature_indices[feature_key] = feature_index
 | 
						|
            scripts.setdefault(script, {}).setdefault(lang, []).append(feature_index)
 | 
						|
            if self.required_features_.get((script, lang)) == feature_tag:
 | 
						|
                required_feature_indices[(script, lang)] = feature_index
 | 
						|
 | 
						|
        # Build ScriptList.
 | 
						|
        for script, lang_features in sorted(scripts.items()):
 | 
						|
            srec = otTables.ScriptRecord()
 | 
						|
            srec.ScriptTag = script
 | 
						|
            srec.Script = otTables.Script()
 | 
						|
            srec.Script.DefaultLangSys = None
 | 
						|
            srec.Script.LangSysRecord = []
 | 
						|
            for lang, feature_indices in sorted(lang_features.items()):
 | 
						|
                langrec = otTables.LangSysRecord()
 | 
						|
                langrec.LangSys = otTables.LangSys()
 | 
						|
                langrec.LangSys.LookupOrder = None
 | 
						|
 | 
						|
                req_feature_index = required_feature_indices.get((script, lang))
 | 
						|
                if req_feature_index is None:
 | 
						|
                    langrec.LangSys.ReqFeatureIndex = 0xFFFF
 | 
						|
                else:
 | 
						|
                    langrec.LangSys.ReqFeatureIndex = req_feature_index
 | 
						|
 | 
						|
                langrec.LangSys.FeatureIndex = [
 | 
						|
                    i for i in feature_indices if i != req_feature_index
 | 
						|
                ]
 | 
						|
                langrec.LangSys.FeatureCount = len(langrec.LangSys.FeatureIndex)
 | 
						|
 | 
						|
                if lang == "dflt":
 | 
						|
                    srec.Script.DefaultLangSys = langrec.LangSys
 | 
						|
                else:
 | 
						|
                    langrec.LangSysTag = lang
 | 
						|
                    srec.Script.LangSysRecord.append(langrec)
 | 
						|
            srec.Script.LangSysCount = len(srec.Script.LangSysRecord)
 | 
						|
            table.ScriptList.ScriptRecord.append(srec)
 | 
						|
 | 
						|
        table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord)
 | 
						|
        table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
 | 
						|
        table.LookupList.LookupCount = len(table.LookupList.Lookup)
 | 
						|
        return table
 | 
						|
 | 
						|
    def makeFeatureVariations(self, table, table_tag):
 | 
						|
        feature_vars = {}
 | 
						|
        has_any_variations = False
 | 
						|
        # Sort out which lookups to build, gather their indices
 | 
						|
        for (_, _, feature_tag), variations in self.feature_variations_.items():
 | 
						|
            feature_vars[feature_tag] = []
 | 
						|
            for conditionset, builders in variations.items():
 | 
						|
                raw_conditionset = self.conditionsets_[conditionset]
 | 
						|
                indices = []
 | 
						|
                for b in builders:
 | 
						|
                    if b.table != table_tag:
 | 
						|
                        continue
 | 
						|
                    assert b.lookup_index is not None
 | 
						|
                    indices.append(b.lookup_index)
 | 
						|
                    has_any_variations = True
 | 
						|
                feature_vars[feature_tag].append((raw_conditionset, indices))
 | 
						|
 | 
						|
        if has_any_variations:
 | 
						|
            for feature_tag, conditions_and_lookups in feature_vars.items():
 | 
						|
                addFeatureVariationsRaw(
 | 
						|
                    self.font, table, conditions_and_lookups, feature_tag
 | 
						|
                )
 | 
						|
 | 
						|
    def any_feature_variations(self, feature_tag, table_tag):
 | 
						|
        for (_, _, feature), variations in self.feature_variations_.items():
 | 
						|
            if feature != feature_tag:
 | 
						|
                continue
 | 
						|
            for conditionset, builders in variations.items():
 | 
						|
                if any(b.table == table_tag for b in builders):
 | 
						|
                    return True
 | 
						|
        return False
 | 
						|
 | 
						|
    def get_lookup_name_(self, lookup):
 | 
						|
        rev = {v: k for k, v in self.named_lookups_.items()}
 | 
						|
        if lookup in rev:
 | 
						|
            return rev[lookup]
 | 
						|
        return None
 | 
						|
 | 
						|
    def add_language_system(self, location, script, language):
 | 
						|
        # OpenType Feature File Specification, section 4.b.i
 | 
						|
        if script == "DFLT" and language == "dflt" and self.default_language_systems_:
 | 
						|
            raise FeatureLibError(
 | 
						|
                'If "languagesystem DFLT dflt" is present, it must be '
 | 
						|
                "the first of the languagesystem statements",
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        if script == "DFLT":
 | 
						|
            if self.seen_non_DFLT_script_:
 | 
						|
                raise FeatureLibError(
 | 
						|
                    'languagesystems using the "DFLT" script tag must '
 | 
						|
                    "precede all other languagesystems",
 | 
						|
                    location,
 | 
						|
                )
 | 
						|
        else:
 | 
						|
            self.seen_non_DFLT_script_ = True
 | 
						|
        if (script, language) in self.default_language_systems_:
 | 
						|
            raise FeatureLibError(
 | 
						|
                '"languagesystem %s %s" has already been specified'
 | 
						|
                % (script.strip(), language.strip()),
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        self.default_language_systems_.add((script, language))
 | 
						|
 | 
						|
    def get_default_language_systems_(self):
 | 
						|
        # OpenType Feature File specification, 4.b.i. languagesystem:
 | 
						|
        # If no "languagesystem" statement is present, then the
 | 
						|
        # implementation must behave exactly as though the following
 | 
						|
        # statement were present at the beginning of the feature file:
 | 
						|
        # languagesystem DFLT dflt;
 | 
						|
        if self.default_language_systems_:
 | 
						|
            return frozenset(self.default_language_systems_)
 | 
						|
        else:
 | 
						|
            return frozenset({("DFLT", "dflt")})
 | 
						|
 | 
						|
    def start_feature(self, location, name, use_extension=False):
 | 
						|
        if use_extension and name != "aalt":
 | 
						|
            raise FeatureLibError(
 | 
						|
                "'useExtension' keyword for feature blocks is allowed only for 'aalt' feature",
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        self.language_systems = self.get_default_language_systems_()
 | 
						|
        self.script_ = "DFLT"
 | 
						|
        self.cur_lookup_ = None
 | 
						|
        self.cur_feature_name_ = name
 | 
						|
        self.lookupflag_ = 0
 | 
						|
        self.lookupflag_markFilterSet_ = None
 | 
						|
        self.use_extension_ = use_extension
 | 
						|
        if name == "aalt":
 | 
						|
            self.aalt_location_ = location
 | 
						|
            self.aalt_use_extension_ = use_extension
 | 
						|
 | 
						|
    def end_feature(self):
 | 
						|
        assert self.cur_feature_name_ is not None
 | 
						|
        self.cur_feature_name_ = None
 | 
						|
        self.language_systems = None
 | 
						|
        self.cur_lookup_ = None
 | 
						|
        self.lookupflag_ = 0
 | 
						|
        self.lookupflag_markFilterSet_ = None
 | 
						|
        self.use_extension_ = False
 | 
						|
 | 
						|
    def start_lookup_block(self, location, name, use_extension=False):
 | 
						|
        if name in self.named_lookups_:
 | 
						|
            raise FeatureLibError(
 | 
						|
                'Lookup "%s" has already been defined' % name, location
 | 
						|
            )
 | 
						|
        if self.cur_feature_name_ == "aalt":
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Lookup blocks cannot be placed inside 'aalt' features; "
 | 
						|
                "move it out, and then refer to it with a lookup statement",
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        self.cur_lookup_name_ = name
 | 
						|
        self.named_lookups_[name] = None
 | 
						|
        self.cur_lookup_ = None
 | 
						|
        self.use_extension_ = use_extension
 | 
						|
        if self.cur_feature_name_ is None:
 | 
						|
            self.lookupflag_ = 0
 | 
						|
            self.lookupflag_markFilterSet_ = None
 | 
						|
 | 
						|
    def end_lookup_block(self):
 | 
						|
        assert self.cur_lookup_name_ is not None
 | 
						|
        self.cur_lookup_name_ = None
 | 
						|
        self.cur_lookup_ = None
 | 
						|
        self.use_extension_ = False
 | 
						|
        if self.cur_feature_name_ is None:
 | 
						|
            self.lookupflag_ = 0
 | 
						|
            self.lookupflag_markFilterSet_ = None
 | 
						|
 | 
						|
    def add_lookup_call(self, lookup_name):
 | 
						|
        assert lookup_name in self.named_lookups_, lookup_name
 | 
						|
        self.cur_lookup_ = None
 | 
						|
        lookup = self.named_lookups_[lookup_name]
 | 
						|
        if lookup is not None:  # skip empty named lookup
 | 
						|
            self.add_lookup_to_feature_(lookup, self.cur_feature_name_)
 | 
						|
 | 
						|
    def set_font_revision(self, location, revision):
 | 
						|
        self.fontRevision_ = revision
 | 
						|
 | 
						|
    def set_language(self, location, language, include_default, required):
 | 
						|
        assert len(language) == 4
 | 
						|
        if self.cur_feature_name_ in ("aalt", "size"):
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Language statements are not allowed "
 | 
						|
                'within "feature %s"' % self.cur_feature_name_,
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        if self.cur_feature_name_ is None:
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Language statements are not allowed "
 | 
						|
                "within standalone lookup blocks",
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        self.cur_lookup_ = None
 | 
						|
 | 
						|
        key = (self.script_, language, self.cur_feature_name_)
 | 
						|
        lookups = self.features_.get((key[0], "dflt", key[2]))
 | 
						|
        if (language == "dflt" or include_default) and lookups:
 | 
						|
            self.features_[key] = lookups[:]
 | 
						|
        else:
 | 
						|
            # if we aren't including default we need to manually remove the
 | 
						|
            # default lookups, which were added to all declared langsystems
 | 
						|
            # as they were encountered (we don't remove all lookups because
 | 
						|
            # we want to allow duplicate script/lang statements;
 | 
						|
            # see https://github.com/fonttools/fonttools/issues/3748
 | 
						|
            cur_lookups = self.features_.get(key, [])
 | 
						|
            self.features_[key] = [x for x in cur_lookups if x not in lookups]
 | 
						|
        self.language_systems = frozenset([(self.script_, language)])
 | 
						|
 | 
						|
        if required:
 | 
						|
            key = (self.script_, language)
 | 
						|
            if key in self.required_features_:
 | 
						|
                raise FeatureLibError(
 | 
						|
                    "Language %s (script %s) has already "
 | 
						|
                    "specified feature %s as its required feature"
 | 
						|
                    % (
 | 
						|
                        language.strip(),
 | 
						|
                        self.script_.strip(),
 | 
						|
                        self.required_features_[key].strip(),
 | 
						|
                    ),
 | 
						|
                    location,
 | 
						|
                )
 | 
						|
            self.required_features_[key] = self.cur_feature_name_
 | 
						|
 | 
						|
    def getMarkAttachClass_(self, location, glyphs):
 | 
						|
        glyphs = frozenset(glyphs)
 | 
						|
        id_ = self.markAttachClassID_.get(glyphs)
 | 
						|
        if id_ is not None:
 | 
						|
            return id_
 | 
						|
        id_ = len(self.markAttachClassID_) + 1
 | 
						|
        self.markAttachClassID_[glyphs] = id_
 | 
						|
        for glyph in glyphs:
 | 
						|
            if glyph in self.markAttach_:
 | 
						|
                _, loc = self.markAttach_[glyph]
 | 
						|
                raise FeatureLibError(
 | 
						|
                    "Glyph %s already has been assigned "
 | 
						|
                    "a MarkAttachmentType at %s" % (glyph, loc),
 | 
						|
                    location,
 | 
						|
                )
 | 
						|
            self.markAttach_[glyph] = (id_, location)
 | 
						|
        return id_
 | 
						|
 | 
						|
    def getMarkFilterSet_(self, location, glyphs):
 | 
						|
        glyphs = frozenset(glyphs)
 | 
						|
        id_ = self.markFilterSets_.get(glyphs)
 | 
						|
        if id_ is not None:
 | 
						|
            return id_
 | 
						|
        id_ = len(self.markFilterSets_)
 | 
						|
        self.markFilterSets_[glyphs] = id_
 | 
						|
        return id_
 | 
						|
 | 
						|
    def set_lookup_flag(self, location, value, markAttach, markFilter):
 | 
						|
        value = value & 0xFF
 | 
						|
        if markAttach is not None:
 | 
						|
            markAttachClass = self.getMarkAttachClass_(location, markAttach)
 | 
						|
            value = value | (markAttachClass << 8)
 | 
						|
        if markFilter is not None:
 | 
						|
            markFilterSet = self.getMarkFilterSet_(location, markFilter)
 | 
						|
            value = value | 0x10
 | 
						|
            self.lookupflag_markFilterSet_ = markFilterSet
 | 
						|
        else:
 | 
						|
            self.lookupflag_markFilterSet_ = None
 | 
						|
        self.lookupflag_ = value
 | 
						|
 | 
						|
    def set_script(self, location, script):
 | 
						|
        if self.cur_feature_name_ in ("aalt", "size"):
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Script statements are not allowed "
 | 
						|
                'within "feature %s"' % self.cur_feature_name_,
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        if self.cur_feature_name_ is None:
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Script statements are not allowed " "within standalone lookup blocks",
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        if self.language_systems == {(script, "dflt")}:
 | 
						|
            # Nothing to do.
 | 
						|
            return
 | 
						|
        self.cur_lookup_ = None
 | 
						|
        self.script_ = script
 | 
						|
        self.lookupflag_ = 0
 | 
						|
        self.lookupflag_markFilterSet_ = None
 | 
						|
        self.set_language(location, "dflt", include_default=True, required=False)
 | 
						|
 | 
						|
    def find_lookup_builders_(self, lookups):
 | 
						|
        """Helper for building chain contextual substitutions
 | 
						|
 | 
						|
        Given a list of lookup names, finds the LookupBuilder for each name.
 | 
						|
        If an input name is None, it gets mapped to a None LookupBuilder.
 | 
						|
        """
 | 
						|
        lookup_builders = []
 | 
						|
        for lookuplist in lookups:
 | 
						|
            if lookuplist is not None:
 | 
						|
                lookup_builders.append(
 | 
						|
                    [self.named_lookups_.get(l.name) for l in lookuplist]
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                lookup_builders.append(None)
 | 
						|
        return lookup_builders
 | 
						|
 | 
						|
    def add_attach_points(self, location, glyphs, contourPoints):
 | 
						|
        for glyph in glyphs:
 | 
						|
            self.attachPoints_.setdefault(glyph, set()).update(contourPoints)
 | 
						|
 | 
						|
    def add_feature_reference(self, location, featureName):
 | 
						|
        if self.cur_feature_name_ != "aalt":
 | 
						|
            raise FeatureLibError(
 | 
						|
                'Feature references are only allowed inside "feature aalt"', location
 | 
						|
            )
 | 
						|
        self.aalt_features_.append((location, featureName))
 | 
						|
 | 
						|
    def add_featureName(self, tag):
 | 
						|
        self.featureNames_.add(tag)
 | 
						|
 | 
						|
    def add_cv_parameter(self, tag):
 | 
						|
        self.cv_parameters_.add(tag)
 | 
						|
 | 
						|
    def add_to_cv_num_named_params(self, tag):
 | 
						|
        """Adds new items to ``self.cv_num_named_params_``
 | 
						|
        or increments the count of existing items."""
 | 
						|
        if tag in self.cv_num_named_params_:
 | 
						|
            self.cv_num_named_params_[tag] += 1
 | 
						|
        else:
 | 
						|
            self.cv_num_named_params_[tag] = 1
 | 
						|
 | 
						|
    def add_cv_character(self, character, tag):
 | 
						|
        self.cv_characters_[tag].append(character)
 | 
						|
 | 
						|
    def set_base_axis(self, bases, scripts, vertical, minmax=[]):
 | 
						|
        if vertical:
 | 
						|
            self.base_vert_axis_ = (bases, scripts, minmax)
 | 
						|
        else:
 | 
						|
            self.base_horiz_axis_ = (bases, scripts, minmax)
 | 
						|
 | 
						|
    def set_size_parameters(
 | 
						|
        self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
 | 
						|
    ):
 | 
						|
        if self.cur_feature_name_ != "size":
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Parameters statements are not allowed "
 | 
						|
                'within "feature %s"' % self.cur_feature_name_,
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        self.size_parameters_ = [DesignSize, SubfamilyID, RangeStart, RangeEnd]
 | 
						|
        for script, lang in self.language_systems:
 | 
						|
            key = (script, lang, self.cur_feature_name_)
 | 
						|
            self.features_.setdefault(key, [])
 | 
						|
 | 
						|
    # GSUB rules
 | 
						|
 | 
						|
    def add_any_subst_(self, location, mapping):
 | 
						|
        lookup = self.get_lookup_(location, AnySubstBuilder, mapping=mapping)
 | 
						|
        for key, value in mapping.items():
 | 
						|
            if key in lookup.mapping:
 | 
						|
                if value == lookup.mapping[key]:
 | 
						|
                    log.info(
 | 
						|
                        'Removing duplicate substitution from "%s" to "%s" at %s',
 | 
						|
                        ", ".join(key),
 | 
						|
                        ", ".join(value),
 | 
						|
                        location,
 | 
						|
                    )
 | 
						|
                else:
 | 
						|
                    raise FeatureLibError(
 | 
						|
                        'Already defined substitution for "%s"' % ", ".join(key),
 | 
						|
                        location,
 | 
						|
                    )
 | 
						|
            lookup.mapping[key] = value
 | 
						|
 | 
						|
    # GSUB 1
 | 
						|
    def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
 | 
						|
        if self.cur_feature_name_ == "aalt":
 | 
						|
            for from_glyph, to_glyph in mapping.items():
 | 
						|
                alts = self.aalt_alternates_.setdefault(from_glyph, [])
 | 
						|
                if to_glyph not in alts:
 | 
						|
                    alts.append(to_glyph)
 | 
						|
            return
 | 
						|
        if prefix or suffix or forceChain:
 | 
						|
            self.add_single_subst_chained_(location, prefix, suffix, mapping)
 | 
						|
            return
 | 
						|
 | 
						|
        self.add_any_subst_(
 | 
						|
            location,
 | 
						|
            {(key,): (value,) for key, value in mapping.items()},
 | 
						|
        )
 | 
						|
 | 
						|
    # GSUB 2
 | 
						|
    def add_multiple_subst(
 | 
						|
        self, location, prefix, glyph, suffix, replacements, forceChain=False
 | 
						|
    ):
 | 
						|
        if prefix or suffix or forceChain:
 | 
						|
            self.add_multi_subst_chained_(location, prefix, glyph, suffix, replacements)
 | 
						|
            return
 | 
						|
        self.add_any_subst_(
 | 
						|
            location,
 | 
						|
            {(glyph,): tuple(replacements)},
 | 
						|
        )
 | 
						|
 | 
						|
    # GSUB 3
 | 
						|
    def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
 | 
						|
        if self.cur_feature_name_ == "aalt":
 | 
						|
            alts = self.aalt_alternates_.setdefault(glyph, [])
 | 
						|
            alts.extend(g for g in replacement if g not in alts)
 | 
						|
            return
 | 
						|
        if prefix or suffix:
 | 
						|
            chain = self.get_lookup_(location, ChainContextSubstBuilder)
 | 
						|
            lookup = self.get_chained_lookup_(location, AlternateSubstBuilder)
 | 
						|
            chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [lookup]))
 | 
						|
        else:
 | 
						|
            lookup = self.get_lookup_(location, AlternateSubstBuilder)
 | 
						|
        if glyph in lookup.alternates:
 | 
						|
            raise FeatureLibError(
 | 
						|
                'Already defined alternates for glyph "%s"' % glyph, location
 | 
						|
            )
 | 
						|
        # We allow empty replacement glyphs here.
 | 
						|
        lookup.alternates[glyph] = replacement
 | 
						|
 | 
						|
    # GSUB 4
 | 
						|
    def add_ligature_subst(
 | 
						|
        self, location, prefix, glyphs, suffix, replacement, forceChain
 | 
						|
    ):
 | 
						|
        if prefix or suffix or forceChain:
 | 
						|
            self.add_ligature_subst_chained_(
 | 
						|
                location, prefix, glyphs, suffix, replacement
 | 
						|
            )
 | 
						|
            return
 | 
						|
        if not all(glyphs):
 | 
						|
            raise FeatureLibError("Empty glyph class in substitution", location)
 | 
						|
 | 
						|
        # OpenType feature file syntax, section 5.d, "Ligature substitution":
 | 
						|
        # "Since the OpenType specification does not allow ligature
 | 
						|
        # substitutions to be specified on target sequences that contain
 | 
						|
        # glyph classes, the implementation software will enumerate
 | 
						|
        # all specific glyph sequences if glyph classes are detected"
 | 
						|
        self.add_any_subst_(
 | 
						|
            location,
 | 
						|
            {g: (replacement,) for g in itertools.product(*glyphs)},
 | 
						|
        )
 | 
						|
 | 
						|
    # GSUB 5/6
 | 
						|
    def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
 | 
						|
        if not all(glyphs) or not all(prefix) or not all(suffix):
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Empty glyph class in contextual substitution", location
 | 
						|
            )
 | 
						|
        lookup = self.get_lookup_(location, ChainContextSubstBuilder)
 | 
						|
        lookup.rules.append(
 | 
						|
            ChainContextualRule(
 | 
						|
                prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
 | 
						|
            )
 | 
						|
        )
 | 
						|
 | 
						|
    def add_single_subst_chained_(self, location, prefix, suffix, mapping):
 | 
						|
        if not mapping or not all(prefix) or not all(suffix):
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Empty glyph class in contextual substitution", location
 | 
						|
            )
 | 
						|
        # https://github.com/fonttools/fonttools/issues/512
 | 
						|
        # https://github.com/fonttools/fonttools/issues/2150
 | 
						|
        chain = self.get_lookup_(location, ChainContextSubstBuilder)
 | 
						|
        sub = chain.find_chainable_subst(mapping, SingleSubstBuilder)
 | 
						|
        if sub is None:
 | 
						|
            sub = self.get_chained_lookup_(location, SingleSubstBuilder)
 | 
						|
        sub.mapping.update(mapping)
 | 
						|
        chain.rules.append(
 | 
						|
            ChainContextualRule(prefix, [list(mapping.keys())], suffix, [sub])
 | 
						|
        )
 | 
						|
 | 
						|
    def add_multi_subst_chained_(self, location, prefix, glyph, suffix, replacements):
 | 
						|
        if not all(prefix) or not all(suffix):
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Empty glyph class in contextual substitution", location
 | 
						|
            )
 | 
						|
        # https://github.com/fonttools/fonttools/issues/3551
 | 
						|
        chain = self.get_lookup_(location, ChainContextSubstBuilder)
 | 
						|
        sub = chain.find_chainable_subst({glyph: replacements}, MultipleSubstBuilder)
 | 
						|
        if sub is None:
 | 
						|
            sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
 | 
						|
        sub.mapping[glyph] = replacements
 | 
						|
        chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub]))
 | 
						|
 | 
						|
    def add_ligature_subst_chained_(
 | 
						|
        self, location, prefix, glyphs, suffix, replacement
 | 
						|
    ):
 | 
						|
        # https://github.com/fonttools/fonttools/issues/3701
 | 
						|
        if not all(prefix) or not all(suffix):
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Empty glyph class in contextual substitution", location
 | 
						|
            )
 | 
						|
        chain = self.get_lookup_(location, ChainContextSubstBuilder)
 | 
						|
        sub = chain.find_chainable_ligature_subst(glyphs, replacement)
 | 
						|
        if sub is None:
 | 
						|
            sub = self.get_chained_lookup_(location, LigatureSubstBuilder)
 | 
						|
 | 
						|
        for g in itertools.product(*glyphs):
 | 
						|
            existing = sub.ligatures.get(g, replacement)
 | 
						|
            if existing != replacement:
 | 
						|
                raise FeatureLibError(
 | 
						|
                    f"Conflicting ligature sub rules: '{g}' maps to '{existing}' and '{replacement}'",
 | 
						|
                    location,
 | 
						|
                )
 | 
						|
 | 
						|
            sub.ligatures[g] = replacement
 | 
						|
 | 
						|
        chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [sub]))
 | 
						|
 | 
						|
    # GSUB 8
 | 
						|
    def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping):
 | 
						|
        if not mapping:
 | 
						|
            raise FeatureLibError("Empty glyph class in substitution", location)
 | 
						|
        lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
 | 
						|
        lookup.rules.append((old_prefix, old_suffix, mapping))
 | 
						|
 | 
						|
    # GPOS rules
 | 
						|
 | 
						|
    # GPOS 1
 | 
						|
    def add_single_pos(self, location, prefix, suffix, pos, forceChain):
 | 
						|
        if prefix or suffix or forceChain:
 | 
						|
            self.add_single_pos_chained_(location, prefix, suffix, pos)
 | 
						|
        else:
 | 
						|
            lookup = self.get_lookup_(location, SinglePosBuilder)
 | 
						|
            for glyphs, value in pos:
 | 
						|
                if not glyphs:
 | 
						|
                    raise FeatureLibError(
 | 
						|
                        "Empty glyph class in positioning rule", location
 | 
						|
                    )
 | 
						|
                otValueRecord = self.makeOpenTypeValueRecord(
 | 
						|
                    location, value, pairPosContext=False
 | 
						|
                )
 | 
						|
                for glyph in glyphs:
 | 
						|
                    try:
 | 
						|
                        lookup.add_pos(location, glyph, otValueRecord)
 | 
						|
                    except OpenTypeLibError as e:
 | 
						|
                        raise FeatureLibError(str(e), e.location) from e
 | 
						|
 | 
						|
    # GPOS 2
 | 
						|
    def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
 | 
						|
        if not glyphclass1 or not glyphclass2:
 | 
						|
            raise FeatureLibError("Empty glyph class in positioning rule", location)
 | 
						|
        lookup = self.get_lookup_(location, PairPosBuilder)
 | 
						|
        v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
 | 
						|
        v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
 | 
						|
        cls1 = tuple(sorted(set(glyphclass1)))
 | 
						|
        cls2 = tuple(sorted(set(glyphclass2)))
 | 
						|
        lookup.addClassPair(location, cls1, v1, cls2, v2)
 | 
						|
 | 
						|
    def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
 | 
						|
        if not glyph1 or not glyph2:
 | 
						|
            raise FeatureLibError("Empty glyph class in positioning rule", location)
 | 
						|
        lookup = self.get_lookup_(location, PairPosBuilder)
 | 
						|
        v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
 | 
						|
        v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
 | 
						|
        lookup.addGlyphPair(location, glyph1, v1, glyph2, v2)
 | 
						|
 | 
						|
    # GPOS 3
 | 
						|
    def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor):
 | 
						|
        if not glyphclass:
 | 
						|
            raise FeatureLibError("Empty glyph class in positioning rule", location)
 | 
						|
        lookup = self.get_lookup_(location, CursivePosBuilder)
 | 
						|
        lookup.add_attachment(
 | 
						|
            location,
 | 
						|
            glyphclass,
 | 
						|
            self.makeOpenTypeAnchor(location, entryAnchor),
 | 
						|
            self.makeOpenTypeAnchor(location, exitAnchor),
 | 
						|
        )
 | 
						|
 | 
						|
    # GPOS 4
 | 
						|
    def add_mark_base_pos(self, location, bases, marks):
 | 
						|
        builder = self.get_lookup_(location, MarkBasePosBuilder)
 | 
						|
        self.add_marks_(location, builder, marks)
 | 
						|
        if not bases:
 | 
						|
            raise FeatureLibError("Empty glyph class in positioning rule", location)
 | 
						|
        for baseAnchor, markClass in marks:
 | 
						|
            otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
 | 
						|
            for base in bases:
 | 
						|
                builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor
 | 
						|
 | 
						|
    # GPOS 5
 | 
						|
    def add_mark_lig_pos(self, location, ligatures, components):
 | 
						|
        builder = self.get_lookup_(location, MarkLigPosBuilder)
 | 
						|
        componentAnchors = []
 | 
						|
        if not ligatures:
 | 
						|
            raise FeatureLibError("Empty glyph class in positioning rule", location)
 | 
						|
        for marks in components:
 | 
						|
            anchors = {}
 | 
						|
            self.add_marks_(location, builder, marks)
 | 
						|
            for ligAnchor, markClass in marks:
 | 
						|
                anchors[markClass.name] = self.makeOpenTypeAnchor(location, ligAnchor)
 | 
						|
            componentAnchors.append(anchors)
 | 
						|
        for glyph in ligatures:
 | 
						|
            builder.ligatures[glyph] = componentAnchors
 | 
						|
 | 
						|
    # GPOS 6
 | 
						|
    def add_mark_mark_pos(self, location, baseMarks, marks):
 | 
						|
        builder = self.get_lookup_(location, MarkMarkPosBuilder)
 | 
						|
        self.add_marks_(location, builder, marks)
 | 
						|
        if not baseMarks:
 | 
						|
            raise FeatureLibError("Empty glyph class in positioning rule", location)
 | 
						|
        for baseAnchor, markClass in marks:
 | 
						|
            otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
 | 
						|
            for baseMark in baseMarks:
 | 
						|
                builder.baseMarks.setdefault(baseMark, {})[
 | 
						|
                    markClass.name
 | 
						|
                ] = otBaseAnchor
 | 
						|
 | 
						|
    # GPOS 7/8
 | 
						|
    def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups):
 | 
						|
        if not all(glyphs) or not all(prefix) or not all(suffix):
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Empty glyph class in contextual positioning rule", location
 | 
						|
            )
 | 
						|
        lookup = self.get_lookup_(location, ChainContextPosBuilder)
 | 
						|
        lookup.rules.append(
 | 
						|
            ChainContextualRule(
 | 
						|
                prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
 | 
						|
            )
 | 
						|
        )
 | 
						|
 | 
						|
    def add_single_pos_chained_(self, location, prefix, suffix, pos):
 | 
						|
        if not pos or not all(prefix) or not all(suffix):
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Empty glyph class in contextual positioning rule", location
 | 
						|
            )
 | 
						|
        # https://github.com/fonttools/fonttools/issues/514
 | 
						|
        chain = self.get_lookup_(location, ChainContextPosBuilder)
 | 
						|
        targets = []
 | 
						|
        for _, _, _, lookups in chain.rules:
 | 
						|
            targets.extend(lookups)
 | 
						|
        subs = []
 | 
						|
        for glyphs, value in pos:
 | 
						|
            if value is None:
 | 
						|
                subs.append(None)
 | 
						|
                continue
 | 
						|
            otValue = self.makeOpenTypeValueRecord(
 | 
						|
                location, value, pairPosContext=False
 | 
						|
            )
 | 
						|
            sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
 | 
						|
            if sub is None:
 | 
						|
                sub = self.get_chained_lookup_(location, SinglePosBuilder)
 | 
						|
                targets.append(sub)
 | 
						|
            for glyph in glyphs:
 | 
						|
                sub.add_pos(location, glyph, otValue)
 | 
						|
            subs.append(sub)
 | 
						|
        assert len(pos) == len(subs), (pos, subs)
 | 
						|
        chain.rules.append(
 | 
						|
            ChainContextualRule(prefix, [g for g, v in pos], suffix, subs)
 | 
						|
        )
 | 
						|
 | 
						|
    def add_marks_(self, location, lookupBuilder, marks):
 | 
						|
        """Helper for add_mark_{base,liga,mark}_pos."""
 | 
						|
        for _, markClass in marks:
 | 
						|
            for markClassDef in markClass.definitions:
 | 
						|
                for mark in markClassDef.glyphs.glyphSet():
 | 
						|
                    if mark not in lookupBuilder.marks:
 | 
						|
                        otMarkAnchor = self.makeOpenTypeAnchor(
 | 
						|
                            location, copy.deepcopy(markClassDef.anchor)
 | 
						|
                        )
 | 
						|
                        lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
 | 
						|
                    else:
 | 
						|
                        existingMarkClass = lookupBuilder.marks[mark][0]
 | 
						|
                        if markClass.name != existingMarkClass:
 | 
						|
                            raise FeatureLibError(
 | 
						|
                                "Glyph %s cannot be in both @%s and @%s"
 | 
						|
                                % (mark, existingMarkClass, markClass.name),
 | 
						|
                                location,
 | 
						|
                            )
 | 
						|
 | 
						|
    def add_subtable_break(self, location):
 | 
						|
        self.cur_lookup_.add_subtable_break(location)
 | 
						|
 | 
						|
    def setGlyphClass_(self, location, glyph, glyphClass):
 | 
						|
        oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None))
 | 
						|
        if oldClass and oldClass != glyphClass:
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Glyph %s was assigned to a different class at %s"
 | 
						|
                % (glyph, oldLocation),
 | 
						|
                location,
 | 
						|
            )
 | 
						|
        self.glyphClassDefs_[glyph] = (glyphClass, location)
 | 
						|
 | 
						|
    def add_glyphClassDef(
 | 
						|
        self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs
 | 
						|
    ):
 | 
						|
        for glyph in baseGlyphs:
 | 
						|
            self.setGlyphClass_(location, glyph, 1)
 | 
						|
        for glyph in ligatureGlyphs:
 | 
						|
            self.setGlyphClass_(location, glyph, 2)
 | 
						|
        for glyph in markGlyphs:
 | 
						|
            self.setGlyphClass_(location, glyph, 3)
 | 
						|
        for glyph in componentGlyphs:
 | 
						|
            self.setGlyphClass_(location, glyph, 4)
 | 
						|
 | 
						|
    def add_ligatureCaretByIndex_(self, location, glyphs, carets):
 | 
						|
        for glyph in glyphs:
 | 
						|
            if glyph not in self.ligCaretPoints_:
 | 
						|
                self.ligCaretPoints_[glyph] = carets
 | 
						|
 | 
						|
    def makeLigCaret(self, location, caret):
 | 
						|
        if not isinstance(caret, VariableScalar):
 | 
						|
            return caret
 | 
						|
        default, device = self.makeVariablePos(location, caret)
 | 
						|
        if device is not None:
 | 
						|
            return (default, device)
 | 
						|
        return default
 | 
						|
 | 
						|
    def add_ligatureCaretByPos_(self, location, glyphs, carets):
 | 
						|
        carets = [self.makeLigCaret(location, caret) for caret in carets]
 | 
						|
        for glyph in glyphs:
 | 
						|
            if glyph not in self.ligCaretCoords_:
 | 
						|
                self.ligCaretCoords_[glyph] = carets
 | 
						|
 | 
						|
    def add_name_record(self, location, nameID, platformID, platEncID, langID, string):
 | 
						|
        self.names_.append([nameID, platformID, platEncID, langID, string])
 | 
						|
 | 
						|
    def add_os2_field(self, key, value):
 | 
						|
        self.os2_[key] = value
 | 
						|
 | 
						|
    def add_hhea_field(self, key, value):
 | 
						|
        self.hhea_[key] = value
 | 
						|
 | 
						|
    def add_vhea_field(self, key, value):
 | 
						|
        self.vhea_[key] = value
 | 
						|
 | 
						|
    def add_conditionset(self, location, key, value):
 | 
						|
        if "fvar" not in self.font:
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Cannot add feature variations to a font without an 'fvar' table",
 | 
						|
                location,
 | 
						|
            )
 | 
						|
 | 
						|
        # Normalize
 | 
						|
        axisMap = {
 | 
						|
            axis.axisTag: (axis.minValue, axis.defaultValue, axis.maxValue)
 | 
						|
            for axis in self.axes
 | 
						|
        }
 | 
						|
 | 
						|
        value = {
 | 
						|
            tag: (
 | 
						|
                normalizeValue(bottom, axisMap[tag]),
 | 
						|
                normalizeValue(top, axisMap[tag]),
 | 
						|
            )
 | 
						|
            for tag, (bottom, top) in value.items()
 | 
						|
        }
 | 
						|
 | 
						|
        # NOTE: This might result in rounding errors (off-by-ones) compared to
 | 
						|
        # rules in Designspace files, since we're working with what's in the
 | 
						|
        # `avar` table rather than the original values.
 | 
						|
        if "avar" in self.font:
 | 
						|
            mapping = self.font["avar"].segments
 | 
						|
            value = {
 | 
						|
                axis: tuple(
 | 
						|
                    piecewiseLinearMap(v, mapping[axis]) if axis in mapping else v
 | 
						|
                    for v in condition_range
 | 
						|
                )
 | 
						|
                for axis, condition_range in value.items()
 | 
						|
            }
 | 
						|
 | 
						|
        self.conditionsets_[key] = value
 | 
						|
 | 
						|
    def makeVariablePos(self, location, varscalar):
 | 
						|
        if not self.varstorebuilder:
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Can't define a variable scalar in a non-variable font", location
 | 
						|
            )
 | 
						|
 | 
						|
        varscalar.axes = self.axes
 | 
						|
        if not varscalar.does_vary:
 | 
						|
            return varscalar.default, None
 | 
						|
 | 
						|
        try:
 | 
						|
            default, index = varscalar.add_to_variation_store(
 | 
						|
                self.varstorebuilder, self.model_cache, self.font.get("avar")
 | 
						|
            )
 | 
						|
        except VarLibError as e:
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Failed to compute deltas for variable scalar", location
 | 
						|
            ) from e
 | 
						|
 | 
						|
        device = None
 | 
						|
        if index is not None and index != 0xFFFFFFFF:
 | 
						|
            device = buildVarDevTable(index)
 | 
						|
 | 
						|
        return default, device
 | 
						|
 | 
						|
    def makeAnchorPos(self, varscalar, deviceTable, location):
 | 
						|
        device = None
 | 
						|
        if not isinstance(varscalar, VariableScalar):
 | 
						|
            if deviceTable is not None:
 | 
						|
                device = otl.buildDevice(dict(deviceTable))
 | 
						|
            return varscalar, device
 | 
						|
        default, device = self.makeVariablePos(location, varscalar)
 | 
						|
        if device is not None and deviceTable is not None:
 | 
						|
            raise FeatureLibError(
 | 
						|
                "Can't define a device coordinate and variable scalar", location
 | 
						|
            )
 | 
						|
        return default, device
 | 
						|
 | 
						|
    def makeOpenTypeAnchor(self, location, anchor):
 | 
						|
        """ast.Anchor --> otTables.Anchor"""
 | 
						|
        if anchor is None:
 | 
						|
            return None
 | 
						|
        deviceX, deviceY = None, None
 | 
						|
        if anchor.xDeviceTable is not None:
 | 
						|
            deviceX = otl.buildDevice(dict(anchor.xDeviceTable))
 | 
						|
        if anchor.yDeviceTable is not None:
 | 
						|
            deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
 | 
						|
        x, deviceX = self.makeAnchorPos(anchor.x, anchor.xDeviceTable, location)
 | 
						|
        y, deviceY = self.makeAnchorPos(anchor.y, anchor.yDeviceTable, location)
 | 
						|
        otlanchor = otl.buildAnchor(x, y, anchor.contourpoint, deviceX, deviceY)
 | 
						|
        return otlanchor
 | 
						|
 | 
						|
    _VALUEREC_ATTRS = {
 | 
						|
        name[0].lower() + name[1:]: (name, isDevice)
 | 
						|
        for _, name, isDevice, _ in otBase.valueRecordFormat
 | 
						|
        if not name.startswith("Reserved")
 | 
						|
    }
 | 
						|
 | 
						|
    def makeOpenTypeValueRecord(self, location, v, pairPosContext):
 | 
						|
        """ast.ValueRecord --> otBase.ValueRecord"""
 | 
						|
        if not v:
 | 
						|
            return None
 | 
						|
 | 
						|
        vr = {}
 | 
						|
        for astName, (otName, isDevice) in self._VALUEREC_ATTRS.items():
 | 
						|
            val = getattr(v, astName, None)
 | 
						|
            if not val:
 | 
						|
                continue
 | 
						|
            if isDevice:
 | 
						|
                vr[otName] = otl.buildDevice(dict(val))
 | 
						|
            elif isinstance(val, VariableScalar):
 | 
						|
                otDeviceName = otName[0:4] + "Device"
 | 
						|
                feaDeviceName = otDeviceName[0].lower() + otDeviceName[1:]
 | 
						|
                if getattr(v, feaDeviceName):
 | 
						|
                    raise FeatureLibError(
 | 
						|
                        "Can't define a device coordinate and variable scalar", location
 | 
						|
                    )
 | 
						|
                vr[otName], device = self.makeVariablePos(location, val)
 | 
						|
                if device is not None:
 | 
						|
                    vr[otDeviceName] = device
 | 
						|
            else:
 | 
						|
                vr[otName] = val
 | 
						|
 | 
						|
        if pairPosContext and not vr:
 | 
						|
            vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
 | 
						|
        valRec = otl.buildValue(vr)
 | 
						|
        return valRec
 |