diff options
Diffstat (limited to 'Lib/fontTools/feaLib/builder.py')
-rw-r--r-- | Lib/fontTools/feaLib/builder.py | 171 |
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 |