aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/feaLib/builder.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/feaLib/builder.py')
-rw-r--r--Lib/fontTools/feaLib/builder.py171
1 files changed, 115 insertions, 56 deletions
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