|
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, |
|
) |
|
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, piecewiseLinearMap |
|
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. |
|
|
|
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 |
|
|
|
|
|
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.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_ = {} |
|
self.required_features_ = {} |
|
self.feature_variations_ = {} |
|
|
|
self.aalt_features_ = [] |
|
self.aalt_location_ = None |
|
self.aalt_alternates_ = {} |
|
|
|
self.featureNames_ = set() |
|
self.featureNames_ids_ = {} |
|
|
|
self.cv_parameters_ = set() |
|
self.cv_parameters_ids_ = {} |
|
self.cv_num_named_params_ = {} |
|
self.cv_characters_ = defaultdict(list) |
|
|
|
self.size_parameters_ = None |
|
|
|
self.fontRevision_ = None |
|
|
|
self.names_ = [] |
|
|
|
self.base_horiz_axis_ = None |
|
self.base_vert_axis_ = None |
|
|
|
self.attachPoints_ = {} |
|
self.ligCaretCoords_ = {} |
|
self.ligCaretPoints_ = {} |
|
self.glyphClassDefs_ = {} |
|
self.markAttach_ = {} |
|
self.markAttachClassID_ = {} |
|
self.markFilterSets_ = {} |
|
|
|
self.os2_ = {} |
|
|
|
self.hhea_ = {} |
|
|
|
self.vhea_ = {} |
|
|
|
self.stat_ = {} |
|
|
|
self.conditionsets_ = {} |
|
|
|
|
|
|
|
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) |
|
|
|
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_ |
|
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): |
|
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_ |
|
): |
|
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.lookups_.append(self.cur_lookup_) |
|
if self.cur_lookup_name_: |
|
|
|
self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_ |
|
if self.cur_feature_name_: |
|
|
|
|
|
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 |
|
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 |
|
] |
|
|
|
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(): |
|
alternates.setdefault(glyph, set()).update(alts) |
|
single = { |
|
glyph: list(repl)[0] for glyph, repl in alternates.items() if len(repl) == 1 |
|
} |
|
|
|
|
|
multi = { |
|
glyph: sorted(repl, key=self.font.getGlyphID) |
|
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") |
|
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: |
|
table = self.font["head"] = newTable("head") |
|
table.decompile(b"\0" * 54, self.font) |
|
table.tableVersion = 1.0 |
|
table.created = table.modified = 3406620153 |
|
table.fontRevision = self.fontRevision_ |
|
|
|
def build_hhea(self): |
|
if not self.hhea_: |
|
return |
|
table = self.font.get("hhea") |
|
if not table: |
|
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: |
|
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): |
|
|
|
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_: |
|
|
|
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: |
|
table = self.font["name"] = newTable("name") |
|
table.names = [] |
|
for name in self.names_: |
|
nameID, platformID, platEncID, langID, string = 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: |
|
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) |
|
|
|
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): |
|
|
|
|
|
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"] = [] |
|
|
|
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: |
|
valuesDict.update({"value": values[0], "name": avr.names}) |
|
if len(values) == 2: |
|
valuesDict.update( |
|
{ |
|
"value": values[0], |
|
"linkedValue": values[1], |
|
"name": avr.names, |
|
} |
|
) |
|
if len(values) == 3: |
|
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: |
|
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 buildBASEAxis(self, axis): |
|
if not axis: |
|
return |
|
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 |
|
record.BaseScript.BaseValues.BaseCoord.append(coord) |
|
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 |
|
lookup.lookup_index = len(lookups) |
|
self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo( |
|
location=str(lookup.location), |
|
name=self.get_lookup_name_(lookup), |
|
feature=None, |
|
) |
|
lookups.append(lookup) |
|
try: |
|
otLookups = [l.build() 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) |
|
|
|
|
|
|
|
feature_indices = {} |
|
required_feature_indices = {} |
|
scripts = {} |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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: |
|
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_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) |
|
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 |
|
|
|
|
|
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 |
|
|
|
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): |
|
|
|
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): |
|
|
|
|
|
|
|
|
|
|
|
if self.default_language_systems_: |
|
return frozenset(self.default_language_systems_) |
|
else: |
|
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", |
|
location, |
|
) |
|
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: |
|
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: |
|
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" |
|
% ( |
|
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: |
|
markAttachClass = self.getMarkAttachClass_(location, markAttach) |
|
value = value | (markAttachClass << 8) |
|
if markFilter: |
|
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")}: |
|
|
|
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): |
|
if vertical: |
|
self.base_vert_axis_ = (bases, scripts) |
|
else: |
|
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_, |
|
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, []) |
|
|
|
|
|
|
|
|
|
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()) |
|
alts.add(to_glyph) |
|
return |
|
if prefix or suffix or forceChain: |
|
self.add_single_subst_chained_(location, prefix, suffix, mapping) |
|
return |
|
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]: |
|
log.info( |
|
"Removing duplicate single substitution from glyph" |
|
' "%s" to "%s" at %s', |
|
from_glyph, |
|
to_glyph, |
|
location, |
|
) |
|
else: |
|
raise FeatureLibError( |
|
'Already defined rule for replacing glyph "%s" by "%s"' |
|
% (from_glyph, lookup.mapping[from_glyph]), |
|
location, |
|
) |
|
lookup.mapping[from_glyph] = to_glyph |
|
|
|
|
|
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])) |
|
return |
|
lookup = self.get_lookup_(location, MultipleSubstBuilder) |
|
if glyph in lookup.mapping: |
|
if replacements == lookup.mapping[glyph]: |
|
log.info( |
|
"Removing duplicate multiple substitution from glyph" |
|
' "%s" to %s%s', |
|
glyph, |
|
replacements, |
|
f" at {location}" if location else "", |
|
) |
|
else: |
|
raise FeatureLibError( |
|
'Already defined substitution for glyph "%s"' % glyph, location |
|
) |
|
lookup.mapping[glyph] = replacements |
|
|
|
|
|
def add_alternate_subst(self, location, prefix, glyph, suffix, replacement): |
|
if self.cur_feature_name_ == "aalt": |
|
alts = self.aalt_alternates_.setdefault(glyph, set()) |
|
alts.update(replacement) |
|
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 |
|
) |
|
|
|
lookup.alternates[glyph] = replacement |
|
|
|
|
|
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])) |
|
else: |
|
lookup = self.get_lookup_(location, LigatureSubstBuilder) |
|
|
|
if not all(glyphs): |
|
raise FeatureLibError("Empty glyph class in substitution", location) |
|
|
|
|
|
|
|
|
|
|
|
|
|
for g in sorted(itertools.product(*glyphs)): |
|
lookup.ligatures[g] = replacement |
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
chain = self.get_lookup_(location, ChainContextSubstBuilder) |
|
sub = chain.find_chainable_single_subst(mapping) |
|
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_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)) |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
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), |
|
) |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
) |
|
|
|
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, 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, |
|
) |
|
|
|
|
|
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() |
|
} |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
default, index = varscalar.add_to_variation_store( |
|
self.varstorebuilder, self.model_cache, self.font.get("avar") |
|
) |
|
|
|
device = None |
|
if index is not None and index != 0xFFFFFFFF: |
|
device = buildVarDevTable(index) |
|
|
|
return default, device |
|
|
|
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"): |
|
varscalar = getattr(anchor, dim) |
|
if not isinstance(varscalar, VariableScalar): |
|
continue |
|
if getattr(anchor, dim + "DeviceTable") is not None: |
|
raise FeatureLibError( |
|
"Can't define a device coordinate and variable scalar", location |
|
) |
|
default, device = self.makeVariablePos(location, varscalar) |
|
setattr(anchor, dim, default) |
|
if device is not None: |
|
if dim == "x": |
|
deviceX = device |
|
else: |
|
deviceY = device |
|
variable = True |
|
|
|
otlanchor = otl.buildAnchor( |
|
anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY |
|
) |
|
if variable: |
|
otlanchor.Format = 3 |
|
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 |
|
|