aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/feaLib
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/feaLib')
-rw-r--r--Lib/fontTools/feaLib/ast.py50
-rw-r--r--Lib/fontTools/feaLib/builder.py171
-rw-r--r--Lib/fontTools/feaLib/lexer.py8
-rw-r--r--Lib/fontTools/feaLib/lookupDebugInfo.py3
-rw-r--r--Lib/fontTools/feaLib/parser.py89
-rw-r--r--Lib/fontTools/feaLib/variableScalar.py37
6 files changed, 226 insertions, 132 deletions
diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index 1273343d..17c6cc3f 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -912,14 +912,11 @@ class IgnoreSubstStatement(Statement):
contexts = []
for prefix, glyphs, suffix in self.chainContexts:
res = ""
- if len(prefix) or len(suffix):
- if len(prefix):
- res += " ".join(map(asFea, prefix)) + " "
- res += " ".join(g.asFea() + "'" for g in glyphs)
- if len(suffix):
- res += " " + " ".join(map(asFea, suffix))
- else:
- res += " ".join(map(asFea, glyphs))
+ if len(prefix):
+ res += " ".join(map(asFea, prefix)) + " "
+ res += " ".join(g.asFea() + "'" for g in glyphs)
+ if len(suffix):
+ res += " " + " ".join(map(asFea, suffix))
contexts.append(res)
return "ignore sub " + ", ".join(contexts) + ";"
@@ -1259,25 +1256,34 @@ class MultipleSubstStatement(Statement):
"""Calls the builder object's ``add_multiple_subst`` callback."""
prefix = [p.glyphSet() for p in self.prefix]
suffix = [s.glyphSet() for s in self.suffix]
- if not self.replacement and hasattr(self.glyph, "glyphSet"):
- for glyph in self.glyph.glyphSet():
+ if hasattr(self.glyph, "glyphSet"):
+ originals = self.glyph.glyphSet()
+ else:
+ originals = [self.glyph]
+ count = len(originals)
+ replaces = []
+ for r in self.replacement:
+ if hasattr(r, "glyphSet"):
+ replace = r.glyphSet()
+ else:
+ replace = [r]
+ if len(replace) == 1 and len(replace) != count:
+ replace = replace * count
+ replaces.append(replace)
+ replaces = list(zip(*replaces))
+
+ seen_originals = set()
+ for i, original in enumerate(originals):
+ if original not in seen_originals:
+ seen_originals.add(original)
builder.add_multiple_subst(
self.location,
prefix,
- glyph,
+ original,
suffix,
- self.replacement,
+ replaces and replaces[i] or (),
self.forceChain,
)
- else:
- builder.add_multiple_subst(
- self.location,
- prefix,
- self.glyph,
- suffix,
- self.replacement,
- self.forceChain,
- )
def asFea(self, indent=""):
res = "sub "
@@ -2068,7 +2074,7 @@ class ConditionsetStatement(Statement):
self.conditions = conditions
def build(self, builder):
- builder.add_conditionset(self.name, self.conditions)
+ builder.add_conditionset(self.location, self.name, self.conditions)
def asFea(self, res="", indent=""):
res += indent + f"conditionset {self.name} " + "{\n"
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index 0a991761..cfaf54d4 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -34,7 +34,7 @@ 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 fontTools.varLib.models import normalizeValue, piecewiseLinearMap
from collections import defaultdict
import itertools
from io import StringIO
@@ -90,7 +90,6 @@ def addOpenTypeFeaturesFromString(
class Builder(object):
-
supportedTables = frozenset(
Tag(tag)
for tag in [
@@ -176,6 +175,10 @@ class Builder(object):
self.stat_ = {}
# for conditionsets
self.conditionsets_ = {}
+ # We will often use exactly the same locations (i.e. the font's masters)
+ # for a large number of variable scalars. Instead of creating a model
+ # for each, let's share the models.
+ self.model_cache = {}
def build(self, tables=None, debug=False):
if self.parseTree is None:
@@ -290,9 +293,8 @@ class Builder(object):
]
# "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
- )
+ 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):
@@ -446,6 +448,7 @@ class Builder(object):
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_:
@@ -768,8 +771,9 @@ class Builder(object):
varidx_map = store.optimize()
gdef.remap_device_varidxes(varidx_map)
- if 'GPOS' in self.font:
- self.font['GPOS'].table.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,
@@ -840,10 +844,15 @@ class Builder(object):
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
+ otLookups = []
+ for l in lookups:
+ try:
+ otLookups.append(l.build())
+ except OpenTypeLibError as e:
+ raise FeatureLibError(str(e), e.location) from e
+ except Exception as e:
+ location = self.lookup_locations[tag][str(l.lookup_index)].location
+ raise FeatureLibError(str(e), location) from e
return otLookups
def makeTable(self, tag):
@@ -945,11 +954,7 @@ class Builder(object):
feature_vars = {}
has_any_variations = False
# Sort out which lookups to build, gather their indices
- for (
- script_,
- language,
- feature_tag,
- ), variations in self.feature_variations_.items():
+ for (_, _, feature_tag), variations in self.feature_variations_.items():
feature_vars[feature_tag] = []
for conditionset, builders in variations.items():
raw_conditionset = self.conditionsets_[conditionset]
@@ -1242,7 +1247,7 @@ class Builder(object):
# 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():
+ for from_glyph, to_glyph in mapping.items():
alts = self.aalt_alternates_.setdefault(from_glyph, set())
alts.add(to_glyph)
return
@@ -1250,7 +1255,7 @@ class Builder(object):
self.add_single_subst_chained_(location, prefix, suffix, mapping)
return
lookup = self.get_lookup_(location, SingleSubstBuilder)
- for (from_glyph, to_glyph) in mapping.items():
+ for from_glyph, to_glyph in mapping.items():
if from_glyph in lookup.mapping:
if to_glyph == lookup.mapping[from_glyph]:
log.info(
@@ -1338,7 +1343,9 @@ class Builder(object):
# 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)
+ raise FeatureLibError(
+ "Empty glyph class in contextual substitution", location
+ )
lookup = self.get_lookup_(location, ChainContextSubstBuilder)
lookup.rules.append(
ChainContextualRule(
@@ -1348,10 +1355,13 @@ class Builder(object):
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)
+ raise FeatureLibError(
+ "Empty glyph class in contextual substitution", location
+ )
# https://github.com/fonttools/fonttools/issues/512
+ # https://github.com/fonttools/fonttools/issues/2150
chain = self.get_lookup_(location, ChainContextSubstBuilder)
- sub = chain.find_chainable_single_subst(set(mapping.keys()))
+ sub = chain.find_chainable_single_subst(mapping)
if sub is None:
sub = self.get_chained_lookup_(location, SingleSubstBuilder)
sub.mapping.update(mapping)
@@ -1376,8 +1386,12 @@ class Builder(object):
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)
+ 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)
@@ -1387,9 +1401,7 @@ class Builder(object):
# 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
- )
+ 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)
@@ -1457,7 +1469,9 @@ class Builder(object):
# 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)
+ raise FeatureLibError(
+ "Empty glyph class in contextual positioning rule", location
+ )
lookup = self.get_lookup_(location, ChainContextPosBuilder)
lookup.rules.append(
ChainContextualRule(
@@ -1467,7 +1481,9 @@ class Builder(object):
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)
+ raise FeatureLibError(
+ "Empty glyph class in contextual positioning rule", location
+ )
# https://github.com/fonttools/fonttools/issues/514
chain = self.get_lookup_(location, ChainContextPosBuilder)
targets = []
@@ -1478,7 +1494,9 @@ class Builder(object):
if value is None:
subs.append(None)
continue
- otValue = self.makeOpenTypeValueRecord(location, value, pairPosContext=False)
+ 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)
@@ -1497,7 +1515,9 @@ class Builder(object):
for markClassDef in markClass.definitions:
for mark in markClassDef.glyphs.glyphSet():
if mark not in lookupBuilder.marks:
- otMarkAnchor = self.makeOpenTypeAnchor(location, markClassDef.anchor)
+ otMarkAnchor = self.makeOpenTypeAnchor(
+ location, markClassDef.anchor
+ )
lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
else:
existingMarkClass = lookupBuilder.marks[mark][0]
@@ -1538,7 +1558,16 @@ class Builder(object):
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
@@ -1555,10 +1584,11 @@ class Builder(object):
def add_vhea_field(self, key, value):
self.vhea_[key] = value
- def add_conditionset(self, key, value):
- if not "fvar" in self.font:
+ 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"
+ "Cannot add feature variations to a font without an 'fvar' table",
+ location,
)
# Normalize
@@ -1575,8 +1605,41 @@ class Builder(object):
for tag, (bottom, top) in value.items()
}
+ # NOTE: This might result in rounding errors (off-by-ones) compared to
+ # rules in Designspace files, since we're working with what's in the
+ # `avar` table rather than the original values.
+ if "avar" in self.font:
+ mapping = self.font["avar"].segments
+ value = {
+ axis: tuple(
+ piecewiseLinearMap(v, mapping[axis]) if axis in mapping else v
+ for v in condition_range
+ )
+ for axis, condition_range in value.items()
+ }
+
self.conditionsets_[key] = value
+ def makeVariablePos(self, location, varscalar):
+ if not self.varstorebuilder:
+ raise FeatureLibError(
+ "Can't define a variable scalar in a non-variable font", location
+ )
+
+ varscalar.axes = self.axes
+ if not varscalar.does_vary:
+ return varscalar.default, None
+
+ 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:
@@ -1588,24 +1651,25 @@ class Builder(object):
if anchor.yDeviceTable is not None:
deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
for dim in ("x", "y"):
- if not isinstance(getattr(anchor, dim), VariableScalar):
+ 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)
- 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)
+ 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 index is not None and index != 0xFFFFFFFF:
+ if device is not None:
if dim == "x":
- deviceX = buildVarDevTable(index)
+ deviceX = device
else:
- deviceY = buildVarDevTable(index)
+ deviceY = device
variable = True
- otlanchor = otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY)
+ otlanchor = otl.buildAnchor(
+ anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY
+ )
if variable:
otlanchor.Format = 3
return otlanchor
@@ -1616,14 +1680,12 @@ class Builder(object):
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:
@@ -1634,15 +1696,12 @@ class Builder(object):
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
+ 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
diff --git a/Lib/fontTools/feaLib/lexer.py b/Lib/fontTools/feaLib/lexer.py
index 140fbd82..e0ae0aef 100644
--- a/Lib/fontTools/feaLib/lexer.py
+++ b/Lib/fontTools/feaLib/lexer.py
@@ -3,6 +3,12 @@ from fontTools.feaLib.location import FeatureLibLocation
import re
import os
+try:
+ import cython
+except ImportError:
+ # if cython not installed, use mock module with no-op decorators and types
+ from fontTools.misc import cython
+
class Lexer(object):
NUMBER = "NUMBER"
@@ -191,7 +197,7 @@ class IncludingLexer(object):
"""A Lexer that follows include statements.
The OpenType feature file specification states that due to
- historical reasons, relative imports should be resolved in this
+ historical reasons, relative imports should be resolved in this
order:
1. If the source font is UFO format, then relative to the UFO's
diff --git a/Lib/fontTools/feaLib/lookupDebugInfo.py b/Lib/fontTools/feaLib/lookupDebugInfo.py
index 876cadff..d4da7de0 100644
--- a/Lib/fontTools/feaLib/lookupDebugInfo.py
+++ b/Lib/fontTools/feaLib/lookupDebugInfo.py
@@ -1,7 +1,8 @@
from typing import NamedTuple
LOOKUP_DEBUG_INFO_KEY = "com.github.fonttools.feaLib"
-LOOKUP_DEBUG_ENV_VAR = "FONTTOOLS_LOOKUP_DEBUGGING"
+LOOKUP_DEBUG_ENV_VAR = "FONTTOOLS_LOOKUP_DEBUGGING"
+
class LookupDebugInfo(NamedTuple):
"""Information about where a lookup came from, to be embedded in a font"""
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index 04ff6030..8ffdf644 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -45,7 +45,6 @@ class Parser(object):
def __init__(
self, featurefile, glyphNames=(), followIncludes=True, includeDir=None, **kwargs
):
-
if "glyphMap" in kwargs:
from fontTools.misc.loggingTools import deprecateArgument
@@ -134,7 +133,8 @@ class Parser(object):
]
raise FeatureLibError(
"The following glyph names are referenced but are missing from the "
- "glyph set:\n" + ("\n".join(error)), None
+ "glyph set:\n" + ("\n".join(error)),
+ None,
)
return self.doc_
@@ -396,7 +396,8 @@ class Parser(object):
self.expect_symbol_("-")
range_end = self.expect_cid_()
self.check_glyph_name_in_glyph_set(
- f"cid{range_start:05d}", f"cid{range_end:05d}",
+ f"cid{range_start:05d}",
+ f"cid{range_end:05d}",
)
glyphs.add_cid_range(
range_start,
@@ -522,27 +523,33 @@ class Parser(object):
)
return (prefix, glyphs, lookups, values, suffix, hasMarks)
- def parse_chain_context_(self):
+ def parse_ignore_glyph_pattern_(self, sub):
location = self.cur_token_location_
prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
vertical=False
)
- chainContext = [(prefix, glyphs, suffix)]
- hasLookups = any(lookups)
+ if any(lookups):
+ raise FeatureLibError(
+ f'No lookups can be specified for "ignore {sub}"', location
+ )
+ if not hasMarks:
+ error = FeatureLibError(
+ f'Ambiguous "ignore {sub}", there should be least one marked glyph',
+ location,
+ )
+ log.warning(str(error))
+ suffix, glyphs = glyphs[1:], glyphs[0:1]
+ chainContext = (prefix, glyphs, suffix)
+ return chainContext
+
+ def parse_ignore_context_(self, sub):
+ location = self.cur_token_location_
+ chainContext = [self.parse_ignore_glyph_pattern_(sub)]
while self.next_token_ == ",":
self.expect_symbol_(",")
- (
- prefix,
- glyphs,
- lookups,
- values,
- suffix,
- hasMarks,
- ) = self.parse_glyph_pattern_(vertical=False)
- chainContext.append((prefix, glyphs, suffix))
- hasLookups = hasLookups or any(lookups)
+ chainContext.append(self.parse_ignore_glyph_pattern_(sub))
self.expect_symbol_(";")
- return chainContext, hasLookups
+ return chainContext
def parse_ignore_(self):
# Parses an ignore sub/pos rule.
@@ -550,18 +557,10 @@ class Parser(object):
location = self.cur_token_location_
self.advance_lexer_()
if self.cur_token_ in ["substitute", "sub"]:
- chainContext, hasLookups = self.parse_chain_context_()
- if hasLookups:
- raise FeatureLibError(
- 'No lookups can be specified for "ignore sub"', location
- )
+ chainContext = self.parse_ignore_context_("sub")
return self.ast.IgnoreSubstStatement(chainContext, location=location)
if self.cur_token_ in ["position", "pos"]:
- chainContext, hasLookups = self.parse_chain_context_()
- if hasLookups:
- raise FeatureLibError(
- 'No lookups can be specified for "ignore pos"', location
- )
+ chainContext = self.parse_ignore_context_("pos")
return self.ast.IgnorePosStatement(chainContext, location=location)
raise FeatureLibError(
'Expected "substitute" or "position"', self.cur_token_location_
@@ -603,9 +602,9 @@ class Parser(object):
assert self.is_cur_keyword_("LigatureCaretByPos")
location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True)
- carets = [self.expect_number_()]
+ carets = [self.expect_number_(variable=True)]
while self.next_token_ != ";":
- carets.append(self.expect_number_())
+ carets.append(self.expect_number_(variable=True))
self.expect_symbol_(";")
return self.ast.LigatureCaretByPosStatement(glyphs, carets, location=location)
@@ -696,7 +695,9 @@ class Parser(object):
location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True)
if not glyphs.glyphSet():
- raise FeatureLibError("Empty glyph class in mark class definition", location)
+ raise FeatureLibError(
+ "Empty glyph class in mark class definition", location
+ )
anchor = self.parse_anchor_()
name = self.expect_class_name_()
self.expect_symbol_(";")
@@ -923,22 +924,27 @@ class Parser(object):
# GSUB lookup type 2: Multiple substitution.
# Format: "substitute f_f_i by f f i;"
- if (
- not reverse
- and len(old) == 1
- and len(old[0].glyphSet()) == 1
- and len(new) > 1
- and max([len(n.glyphSet()) for n in new]) == 1
- and num_lookups == 0
- ):
+ #
+ # GlyphsApp introduces two additional formats:
+ # Format 1: "substitute [f_i f_l] by [f f] [i l];"
+ # Format 2: "substitute [f_i f_l] by f [i l];"
+ # http://handbook.glyphsapp.com/en/layout/multiple-substitution-with-classes/
+ if not reverse and len(old) == 1 and len(new) > 1 and num_lookups == 0:
+ count = len(old[0].glyphSet())
for n in new:
if not list(n.glyphSet()):
raise FeatureLibError("Empty class in replacement", location)
+ if len(n.glyphSet()) != 1 and len(n.glyphSet()) != count:
+ raise FeatureLibError(
+ f'Expected a glyph class with 1 or {count} elements after "by", '
+ f"but found a glyph class with {len(n.glyphSet())} elements",
+ location,
+ )
return self.ast.MultipleSubstStatement(
old_prefix,
- tuple(old[0].glyphSet())[0],
+ old[0],
old_suffix,
- tuple([list(n.glyphSet())[0] for n in new]),
+ new,
forceChain=hasMarks,
location=location,
)
@@ -1747,7 +1753,8 @@ class Parser(object):
def parse_featureNames_(self, tag):
"""Parses a ``featureNames`` statement found in stylistic set features.
- See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_."""
+ See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_.
+ """
assert self.cur_token_ == "featureNames", self.cur_token_
block = self.ast.NestedBlock(
tag, self.cur_token_, location=self.cur_token_location_
diff --git a/Lib/fontTools/feaLib/variableScalar.py b/Lib/fontTools/feaLib/variableScalar.py
index a286568e..c97b4354 100644
--- a/Lib/fontTools/feaLib/variableScalar.py
+++ b/Lib/fontTools/feaLib/variableScalar.py
@@ -1,4 +1,4 @@
-from fontTools.varLib.models import VariationModel, normalizeValue
+from fontTools.varLib.models import VariationModel, normalizeValue, piecewiseLinearMap
def Location(loc):
@@ -74,24 +74,39 @@ class VariableScalar:
# I *guess* we could interpolate one, but I don't know how.
return self.values[key]
- def value_at_location(self, location):
+ def value_at_location(self, location, model_cache=None, avar=None):
loc = location
if loc in self.values.keys():
return self.values[loc]
values = list(self.values.values())
- return self.model.interpolateFromMasters(loc, values)
+ return self.model(model_cache, avar).interpolateFromMasters(loc, values)
- @property
- def model(self):
+ def model(self, model_cache=None, avar=None):
+ if model_cache is not None:
+ key = tuple(self.values.keys())
+ if key in model_cache:
+ return model_cache[key]
locations = [dict(self._normalized_location(k)) for k in self.values.keys()]
- return VariationModel(locations)
-
- def get_deltas_and_supports(self):
+ if avar is not None:
+ mapping = avar.segments
+ locations = [
+ {
+ k: piecewiseLinearMap(v, mapping[k]) if k in mapping else v
+ for k, v in location.items()
+ }
+ for location in locations
+ ]
+ m = VariationModel(locations)
+ if model_cache is not None:
+ model_cache[key] = m
+ return m
+
+ def get_deltas_and_supports(self, model_cache=None, avar=None):
values = list(self.values.values())
- return self.model.getDeltasAndSupports(values)
+ return self.model(model_cache, avar).getDeltasAndSupports(values)
- def add_to_variation_store(self, store_builder):
- deltas, supports = self.get_deltas_and_supports()
+ def add_to_variation_store(self, store_builder, model_cache=None, avar=None):
+ deltas, supports = self.get_deltas_and_supports(model_cache, avar)
store_builder.setSupports(supports)
index = store_builder.storeDeltas(deltas)
return int(self.default), index