diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 830a085774..bb51e91895 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -1,5 +1,6 @@ from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer +from fontTools.feaLib.variableScalar import VariableScalar from fontTools.misc.encodingTools import getEncoding from fontTools.misc.textTools import bytechr, tobytes, tostr import fontTools.feaLib.ast as ast @@ -152,7 +153,7 @@ def parse_anchor_(self): location=location, ) - x, y = self.expect_number_(), self.expect_number_() + x, y = self.expect_number_(variable=True), self.expect_number_(variable=True) contourpoint = None if self.next_token_ == "contourpoint": # Format B @@ -1616,10 +1617,10 @@ def parse_valuerecord_(self, vertical): xAdvance, yAdvance = (value.xAdvance, value.yAdvance) else: xPlacement, yPlacement, xAdvance, yAdvance = ( - self.expect_number_(), - self.expect_number_(), - self.expect_number_(), - self.expect_number_(), + self.expect_number_(variable=True), + self.expect_number_(variable=True), + self.expect_number_(variable=True), + self.expect_number_(variable=True), ) if self.next_token_ == "<": @@ -2080,12 +2081,47 @@ def expect_name_(self): return self.cur_token_ raise FeatureLibError("Expected a name", self.cur_token_location_) - def expect_number_(self): + def expect_number_(self, variable=False): self.advance_lexer_() if self.cur_token_type_ is Lexer.NUMBER: return self.cur_token_ + if variable and self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "(": + return self.expect_variable_scalar_() raise FeatureLibError("Expected a number", self.cur_token_location_) + def expect_variable_scalar_(self): + self.advance_lexer_() # "(" + scalar = VariableScalar() + while True: + if self.cur_token_type_ == Lexer.SYMBOL and self.cur_token_ == ")": + break + location, value = self.expect_master_() + scalar.add_value(location, value) + return scalar + + def expect_master_(self): + location = {} + while True: + if self.cur_token_type_ is not Lexer.NAME: + raise FeatureLibError("Expected an axis name", self.cur_token_location_) + axis = self.cur_token_ + self.advance_lexer_() + if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "="): + raise FeatureLibError("Expected an equals sign", self.cur_token_location_) + value = self.expect_number_() + location[axis] = value + if self.next_token_type_ is Lexer.NAME and self.next_token_[0] == ":": + # Lexer has just read the value as a glyph name. We'll correct it later + break + self.advance_lexer_() + if not(self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ","): + raise FeatureLibError("Expected an comma or an equals sign", self.cur_token_location_) + self.advance_lexer_() + self.advance_lexer_() + value = int(self.cur_token_[1:]) + self.advance_lexer_() + return location, value + def expect_any_number_(self): self.advance_lexer_() if self.cur_token_type_ in Lexer.NUMBERS: diff --git a/Lib/fontTools/feaLib/variableScalar.py b/Lib/fontTools/feaLib/variableScalar.py new file mode 100644 index 0000000000..217e41f655 --- /dev/null +++ b/Lib/fontTools/feaLib/variableScalar.py @@ -0,0 +1,78 @@ +from fontTools.varLib.models import VariationModel, normalizeValue + + +class Location(dict): + def __hash__(self): + return hash(frozenset(self)) + + +class VariableScalar: + """A scalar with different values at different points in the designspace.""" + + def __init__(self, location_value={}): + self.values = {} + self.axes = {} + for location, value in location_value.items(): + self.add_value(location, value) + + def __repr__(self): + items = [] + for location,value in self.values.items(): + loc = ",".join(["%s=%i" % (ax,loc) for ax,loc in location.items()]) + items.append("%s:%i" % (loc, value)) + return "("+(" ".join(items))+")" + + @property + def axes_dict(self): + if not self.axes: + raise ValueError(".axes must be defined on variable scalar before interpolating") + return {ax.tag: ax for ax in self.axes} + + def _normalized_location(self, location): + normalized_location = {} + for axtag in location.keys(): + if axtag not in self.axes_dict: + raise ValueError("Unknown axis %s in %s" % axtag, location) + axis = self.axes_dict[axtag] + normalized_location[axtag] = normalizeValue( + location[axtag], (axis.minimum, axis.default, axis.maximum) + ) + + for ax in self.axes: + if ax.tag not in normalized_location: + normalized_location[ax.tag] = 0 + + return Location(normalized_location) + + def add_value(self, location, value): + self.values[Location(location)] = value + + @property + def default(self): + key = {ax.tag: ax.default for ax in self.axes} + if key not in self.values: + raise ValueError("Default value could not be found") + # I *guess* we could interpolate one, but I don't know how. + return self.values[key] + + def value_at_location(self, location): + loc = location + if loc in self.values.keys(): + return self.values[loc] + values = list(self.values.values()) + return self.model.interpolateFromMasters(loc, values) + + @property + def model(self): + locations = [self._normalized_location(k) for k in self.values.keys()] + return VariationModel(locations) + + def get_deltas_and_supports(self): + values = list(self.values.values()) + return self.model.getDeltasAndSupports(values) + + def add_to_variation_store(self, store_builder): + deltas, supports = self.get_deltas_and_supports() + store_builder.setSupports(supports) + index = store_builder.storeDeltas(deltas) + return int(self.default), index diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py index 11808190a3..3d11b878db 100644 --- a/Tests/feaLib/parser_test.py +++ b/Tests/feaLib/parser_test.py @@ -172,6 +172,14 @@ def test_anchor_format_e_undefined(self): " position cursive A ;" "} test;") + def test_anchor_variable_scalar(self): + doc = self.parse( + "feature test {" + " pos cursive A ;" + "} test;") + anchor = doc.statements[0].statements[0].entryAnchor + self.assertEqual(anchor.asFea(), "") + def test_anchordef(self): [foo] = self.parse("anchorDef 123 456 foo;").statements self.assertEqual(type(foo), ast.AnchorDefinition) @@ -1804,6 +1812,11 @@ def test_valuerecord_format_d(self): self.assertFalse(value) self.assertEqual(value.asFea(), "") + def test_valuerecord_variable_scalar(self): + doc = self.parse("feature test {valueRecordDef <0 (wght=200:-100 wght=900:-150 wght=900,wdth=150:-120) 0 0> foo;} test;") + value = doc.statements[0].statements[0].value + self.assertEqual(value.asFea(), "<0 (wght=200:-100 wght=900:-150 wght=900,wdth=150:-120) 0 0>") + def test_valuerecord_named(self): doc = self.parse("valueRecordDef <1 2 3 4> foo;" "feature liga {valueRecordDef bar;} liga;")