blob: a16448759be03e327be7629707d1abfbd2a44fc7 [file] [log] [blame]
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 (
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 (
from fontTools.otlLib.error import OpenTypeLibError
from fontTools.varLib.varStore import OnlineVarStoreBuilder
from fontTools.varLib.builder import buildVarDevTable
from fontTools.varLib.featureVars import addFeatureVariationsRaw
from fontTools.varLib.models import normalizeValue
from collections import defaultdict
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.
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
debug: Whether to add source debugging information to the font in the
``Debg`` table
builder = Builder(font, featurefile), 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.
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
debug: Whether to add source debugging information to the font in the
``Debg`` table
featurefile = StringIO(tostr(features))
if filename: = filename
addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug)
class Builder(object):
supportedTables = frozenset(
for tag in [
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
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.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_ = {}
# 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_ = {}
def build(self, tables=None, debug=False):
if self.parseTree is None:
self.parseTree = Parser(self.file, self.glyphMap).parse()
# by default, build all the supported tables
if tables is None:
tables = self.supportedTables
tables = frozenset(tables)
unsupported = tables - self.supportedTables
if unsupported:
unsupported_string = ", ".join(sorted(unsupported))
raise NotImplementedError(
"The following tables were requested but are unsupported: "
if "GSUB" in tables:
if "head" in tables:
if "hhea" in tables:
if "vhea" in tables:
if "name" in tables:
if "OS/2" in tables:
if "STAT" in tables:
for tag in ("GPOS", "GSUB"):
if tag not in tables:
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"]
elif self.varstorebuilder:
raise FeatureLibError("Must save GDEF when compiling a variable font")
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):
def get_chained_lookup_(self, location, builder_class):
result = builder_class(self.font, location)
result.lookupflag = self.lookupflag_
result.markFilterSet = self.lookupflag_markFilterSet_
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):
if (
and type(self.cur_lookup_) == builder_class
and self.cur_lookup_.lookupflag == self.lookupflag_
and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_
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",
self.cur_lookup_ = builder_class(self.font, location)
self.cur_lookup_.lookupflag = self.lookupflag_
self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
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_:
alternates = {g: set(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":
raise FeatureLibError(
"Feature %s has not been defined" % name, location
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():
alternates.setdefault(glyph, set()).update(alts)
single = {
glyph: list(repl)[0] for glyph, repl in alternates.items() if len(repl) == 1
# TODO: Figure out the glyph alternate ordering used by makeotf.
multi = {
glyph: sorted(repl, key=self.font.getGlyphID)
for glyph, repl in alternates.items()
if len(repl) > 1
if not single and not multi:
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")
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
def build_head(self):
if not self.fontRevision_:
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.created = table.modified = 3406620153 # 2011-12-13 11:22:33
table.fontRevision = self.fontRevision_
def build_hhea(self):
if not self.hhea_:
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_:
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()
) = self.size_parameters_
if tag in self.featureNames_ids_:
params.SubfamilyNameID = self.featureNames_ids_[tag]
params.SubfamilyNameID = 0
elif tag in self.featureNames_:
if not self.featureNames_ids_:
# name table wasn't selected among the tables to build; skip
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_:
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)
def build_OS_2(self):
if not self.os2_:
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()
) = 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_:
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:
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.",
if isinstance(value, int):
self.stat_["ElidedFallbackNameID"] = value
elif isinstance(value, list):
self.stat_["ElidedFallbackName"] = value
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}".',
if designAxis.axisOrder in (r.axisOrder for r in self.stat_["DesignAxes"]):
raise FeatureLibError(
f"DesignAxis already defined for axis number {designAxis.axisOrder}.",
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.",
def build_STAT(self):
if not self.stat_:
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
"value": values[0],
"linkedValue": values[1],
"name": avr.names,
if len(values) == 3: # format2
nominal, minVal, maxVal = values
"nominalValue": nominal,
"rangeMinValue": minVal,
"rangeMaxValue": maxVal,
"name": avr.names,
"location": {i.tag: i.values[0] for i in avr.locations},
"name": avr.names,
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',
elif "ElidedFallbackName" in self.stat_:
nameID = self.stat_["ElidedFallbackName"]
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):
for j in range(i * 32, (i + 1) * 32):
if j in bits:
pages[i] += "1"
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 buildBASEAxis(self, axis):
if not axis:
bases, scripts = 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):
record = otTables.BaseScriptRecord()
record.BaseScriptTag = script[0]
record.BaseScript = otTables.BaseScript()
record.BaseScript.BaseLangSysCount = 0
record.BaseScript.BaseValues = otTables.BaseValues()
record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
record.BaseScript.BaseValues.BaseCoord = []
record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
for c in script[2]:
coord = otTables.BaseCoord()
coord.Format = 1
coord.Coordinate = c
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.VarData:
gdef.Version = 0x00010003
gdef.VarStore = store
varidx_map = store.optimize()
if 'GPOS' in self.font:
if any(
) or hasattr(gdef, "VarStore"):
result = newTable("GDEF")
result.table = gdef
return result
return None
def buildGDEFGlyphClassDef_(self):
if self.glyphClassDefs_:
classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()}
classes = {}
for lookup in self.lookups_:
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
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]
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:
lookup.lookup_index = len(lookups)
self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
otLookups = [ for l in lookups]
except OpenTypeLibError as e:
raise FeatureLibError(str(e), 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:
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.
lookup_indices = tuple(
[l.lookup_index for l in lookups if l.lookup_index is not None]
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:
for ix in lookup_indices:
self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
except KeyError:
"feaLib.Builder subclass needs upgrading to "
"stash debug information. See fonttools#2065."
feature_key = (feature_tag, lookup_indices)
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)
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
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
langrec.LangSysTag = lang
srec.Script.LangSysCount = len(srec.Script.LangSysRecord)
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 (
), 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:
assert b.lookup_index is not None
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():
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:
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",
if script == "DFLT":
if self.seen_non_DFLT_script_:
raise FeatureLibError(
'languagesystems using the "DFLT" script tag must '
"precede all other languagesystems",
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()),
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_)
return frozenset({("DFLT", "dflt")})
def start_feature(self, location, name):
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
if name == "aalt":
self.aalt_location_ = location
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
def start_lookup_block(self, location, name):
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",
self.cur_lookup_name_ = name
self.named_lookups_[name] = None
self.cur_lookup_ = None
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
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_,
if self.cur_feature_name_ is None:
raise FeatureLibError(
"Language statements are not allowed "
"within standalone lookup blocks",
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[:]
self.features_[key] = []
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"
% (
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),
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:
markAttachClass = self.getMarkAttachClass_(location, markAttach)
value = value | (markAttachClass << 8)
if markFilter:
markFilterSet = self.getMarkFilterSet_(location, markFilter)
value = value | 0x10
self.lookupflag_markFilterSet_ = markFilterSet
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_,
if self.cur_feature_name_ is None:
raise FeatureLibError(
"Script statements are not allowed " "within standalone lookup blocks",
if self.language_systems == {(script, "dflt")}:
# Nothing to do.
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:
[self.named_lookups_.get( for l in lookuplist]
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):
def add_cv_parameter(self, 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
self.cv_num_named_params_[tag] = 1
def add_cv_character(self, character, tag):
def set_base_axis(self, bases, scripts, vertical):
if vertical:
self.base_vert_axis_ = (bases, scripts)
self.base_horiz_axis_ = (bases, scripts)
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_,
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
# 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, set())
if prefix or suffix or forceChain:
self.add_single_subst_chained_(location, prefix, suffix, mapping)
lookup = self.get_lookup_(location, SingleSubstBuilder)
for (from_glyph, to_glyph) in mapping.items():
if from_glyph in lookup.mapping:
if to_glyph == lookup.mapping[from_glyph]:
"Removing duplicate single substitution from glyph"
' "%s" to "%s" at %s',
raise FeatureLibError(
'Already defined rule for replacing glyph "%s" by "%s"'
% (from_glyph, lookup.mapping[from_glyph]),
lookup.mapping[from_glyph] = to_glyph
# GSUB 2
def add_multiple_subst(
self, location, prefix, glyph, suffix, replacements, forceChain=False
if prefix or suffix or forceChain:
chain = self.get_lookup_(location, ChainContextSubstBuilder)
sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
sub.mapping[glyph] = replacements
chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub]))
lookup = self.get_lookup_(location, MultipleSubstBuilder)
if glyph in lookup.mapping:
if replacements == lookup.mapping[glyph]:
"Removing duplicate multiple substitution from glyph"
' "%s" to %s%s',
f" at {location}" if location else "",
raise FeatureLibError(
'Already defined substitution for glyph "%s"' % glyph, location
lookup.mapping[glyph] = 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, set())
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]))
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:
chain = self.get_lookup_(location, ChainContextSubstBuilder)
lookup = self.get_chained_lookup_(location, LigatureSubstBuilder)
chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [lookup]))
lookup = self.get_lookup_(location, LigatureSubstBuilder)
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"
for g in sorted(itertools.product(*glyphs)):
lookup.ligatures[g] = replacement
# 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)
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)
chain = self.get_lookup_(location, ChainContextSubstBuilder)
sub = chain.find_chainable_single_subst(set(mapping.keys()))
if sub is None:
sub = self.get_chained_lookup_(location, SingleSubstBuilder)
ChainContextualRule(prefix, [list(mapping.keys())], 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)
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:
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)
lookup.addClassPair(location, glyphclass1, v1, glyphclass2, 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)
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, {})[] = 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[] = self.makeOpenTypeAnchor(location, ligAnchor)
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, {})[
] = 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)
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)
chain = self.get_lookup_(location, ChainContextPosBuilder)
targets = []
for _, _, _, lookups in chain.rules:
subs = []
for glyphs, value in pos:
if value is None:
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)
for glyph in glyphs:
sub.add_pos(location, glyph, otValue)
assert len(pos) == len(subs), (pos, subs)
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, markClassDef.anchor)
lookupBuilder.marks[mark] = (, otMarkAnchor)
existingMarkClass = lookupBuilder.marks[mark][0]
if != existingMarkClass:
raise FeatureLibError(
"Glyph %s cannot be in both @%s and @%s"
% (mark, existingMarkClass,,
def add_subtable_break(self, 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),
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 add_ligatureCaretByPos_(self, location, glyphs, 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, key, value):
if not "fvar" in self.font:
raise FeatureLibError(
"Cannot add feature variations to a font without an 'fvar' table"
# 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()
self.conditionsets_[key] = value
def makeOpenTypeAnchor(self, location, anchor):
"""ast.Anchor --> otTables.Anchor"""
if anchor is None:
return None
variable = False
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))
for dim in ("x", "y"):
if not isinstance(getattr(anchor, dim), VariableScalar):
if getattr(anchor, dim+"DeviceTable") is not None:
raise FeatureLibError("Can't define a device coordinate and variable scalar", location)
if not self.varstorebuilder:
raise FeatureLibError("Can't define a variable scalar in a non-variable font", location)
varscalar = getattr(anchor,dim)
varscalar.axes = self.axes
default, index = varscalar.add_to_variation_store(self.varstorebuilder)
setattr(anchor, dim, default)
if index is not None and index != 0xFFFFFFFF:
if dim == "x":
deviceX = buildVarDevTable(index)
deviceY = buildVarDevTable(index)
variable = True
otlanchor = otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY)
if variable:
otlanchor.Format = 3
return otlanchor
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 = {}
variable = False
for astName, (otName, isDevice) in self._VALUEREC_ATTRS.items():
val = getattr(v, astName, None)
if not val:
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)
if not self.varstorebuilder:
raise FeatureLibError("Can't define a variable scalar in a non-variable font", location)
val.axes = self.axes
default, index = val.add_to_variation_store(self.varstorebuilder)
vr[otName] = default
if index is not None and index != 0xFFFFFFFF:
vr[otDeviceName] = buildVarDevTable(index)
variable = True
vr[otName] = val
if pairPosContext and not vr:
vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
valRec = otl.buildValue(vr)
return valRec