Skip to content

Commit

Permalink
Parse variable scalars in feature files.
Browse files Browse the repository at this point in the history
  • Loading branch information
simoncozens committed Oct 28, 2021
1 parent c194a18 commit 6810cf5
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 6 deletions.
48 changes: 42 additions & 6 deletions Lib/fontTools/feaLib/parser.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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_ == "<":
Expand Down Expand Up @@ -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:
Expand Down
78 changes: 78 additions & 0 deletions Lib/fontTools/feaLib/variableScalar.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions Tests/feaLib/parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ def test_anchor_format_e_undefined(self):
" position cursive A <anchor UnknownName> <anchor NULL>;"
"} test;")

def test_anchor_variable_scalar(self):
doc = self.parse(
"feature test {"
" pos cursive A <anchor (wght=200:-100 wght=900:-150 wght=900,wdth=150:-120) -20> <anchor NULL>;"
"} test;")
anchor = doc.statements[0].statements[0].entryAnchor
self.assertEqual(anchor.asFea(), "<anchor (wght=200:-100 wght=900:-150 wght=900,wdth=150:-120) -20>")

def test_anchordef(self):
[foo] = self.parse("anchorDef 123 456 foo;").statements
self.assertEqual(type(foo), ast.AnchorDefinition)
Expand Down Expand Up @@ -1804,6 +1812,11 @@ def test_valuerecord_format_d(self):
self.assertFalse(value)
self.assertEqual(value.asFea(), "<NULL>")

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 <foo> bar;} liga;")
Expand Down

0 comments on commit 6810cf5

Please sign in to comment.