Skip to content

Commit

Permalink
[feaLib] Support variable ligature caret position
Browse files Browse the repository at this point in the history
Allow variable scaler in ligature caret position and build
CaretValueFormat3 with DeviceTable. Does not support non-variable device
table, but can be added if someone really really wants it.
  • Loading branch information
khaledhosny committed May 26, 2023
1 parent bf77873 commit f7db6e1
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 29 deletions.
19 changes: 19 additions & 0 deletions Lib/fontTools/feaLib/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1553,7 +1553,26 @@ def add_ligatureCaretByIndex_(self, location, glyphs, carets):
if glyph not in self.ligCaretPoints_:
self.ligCaretPoints_[glyph] = carets

def build_caret_(self, location, caret, avar):
if not isinstance(caret, VariableScalar):
return caret

if not self.varstorebuilder:
raise FeatureLibError(

Check warning on line 1561 in Lib/fontTools/feaLib/builder.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/feaLib/builder.py#L1561

Added line #L1561 was not covered by tests
"Can't define a variable scalar in a non-variable font", location
)

caret.axes = self.axes
default, index = caret.add_to_variation_store(
self.varstorebuilder, self.model_cache, avar
)
if index is not None and index != 0xFFFFFFFF:
return (default, buildVarDevTable(index))
return default

Check warning on line 1571 in Lib/fontTools/feaLib/builder.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/feaLib/builder.py#L1571

Added line #L1571 was not covered by tests

def add_ligatureCaretByPos_(self, location, glyphs, carets):
avar = self.font.get("avar")
carets = [self.build_caret_(location, caret, avar) for caret in carets]
for glyph in glyphs:
if glyph not in self.ligCaretCoords_:
self.ligCaretCoords_[glyph] = carets
Expand Down
4 changes: 2 additions & 2 deletions Lib/fontTools/feaLib/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,9 +603,9 @@ def parse_ligatureCaretByPos_(self):
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)

Expand Down
12 changes: 9 additions & 3 deletions Lib/fontTools/otlLib/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2509,9 +2509,14 @@ def buildAttachPoint(points):

def buildCaretValueForCoord(coord):
# 500 --> otTables.CaretValue, format 1
# (500, DeviceTable) --> otTables.CaretValue, format 3
self = ot.CaretValue()
self.Format = 1
self.Coordinate = coord
if isinstance(coord, tuple):
self.Format = 3
self.Coordinate, self.DeviceTable = coord
else:
self.Format = 1
self.Coordinate = coord
return self


Expand Down Expand Up @@ -2573,7 +2578,8 @@ def buildLigGlyph(coords, points):
# ([500], [4]) --> otTables.LigGlyph; None for empty coords/points
carets = []
if coords:
carets.extend([buildCaretValueForCoord(c) for c in sorted(coords)])
coords = sorted(coords, key=lambda c: isinstance(c, tuple) and c[0] or c)
carets.extend([buildCaretValueForCoord(c) for c in coords])
if points:
carets.extend([buildCaretValueForPoint(p) for p in sorted(points)])
if not carets:
Expand Down
73 changes: 49 additions & 24 deletions Tests/feaLib/builder_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,21 @@ def normal_fea(self, lines):
output.append(l)
return output

def make_mock_vf(self):
font = makeTTFont()
font["name"] = newTable("name")
addFvar(font, self.VARFONT_AXES, [])
del font["name"]
return font

@staticmethod
def get_region(var_region_axis):
return (
var_region_axis.StartCoord,
var_region_axis.PeakCoord,
var_region_axis.EndCoord,
)

def test_alternateSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
Expand Down Expand Up @@ -1046,50 +1061,60 @@ def test_variable_scalar_avar(self):
} kern;
"""

def make_mock_vf():
font = makeTTFont()
font["name"] = newTable("name")
addFvar(font, self.VARFONT_AXES, [])
del font["name"]
return font

def get_region(var_region_axis):
return (
var_region_axis.StartCoord,
var_region_axis.PeakCoord,
var_region_axis.EndCoord,
)

# Without `avar` (wght=200, wdth=100 is the default location):
font = make_mock_vf()
font = self.make_mock_vf()
addOpenTypeFeaturesFromString(font, features)

var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList
var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1]
assert get_region(var_region_axis_wght) == (0.0, 0.875, 0.875)
assert get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875)
assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1]
assert get_region(var_region_axis_wght) == (0.0, 0.875, 0.875)
assert get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5)
assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875)
assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5)

# With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to
# the right, but leaving the wdth axis alone:
font = make_mock_vf()
font = self.make_mock_vf()
font["avar"] = newTable("avar")
font["avar"].segments = {"wght": {-1.0: -1.0, 0.0: 0.0, 0.5: 0.625, 1.0: 1.0}}
addOpenTypeFeaturesFromString(font, features)

var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList
var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1]
assert get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625)
assert get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625)
assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1]
assert get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625)
assert get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5)
assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625)
assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5)

def test_ligatureCaretByPos_variable_scalar(self):
"""Test that the `avar` table is consulted when normalizing user-space
values."""

features = """
table GDEF {
LigatureCaretByPos f_i (wght=200:400 wght=900:1000) 380;
} GDEF;
"""

font = self.make_mock_vf()
addOpenTypeFeaturesFromString(font, features)

table = font["GDEF"].table
lig_glyph = table.LigCaretList.LigGlyph[0]
assert lig_glyph.CaretValue[0].Format == 1
assert lig_glyph.CaretValue[0].Coordinate == 380
assert lig_glyph.CaretValue[1].Format == 3
assert lig_glyph.CaretValue[1].Coordinate == 400

var_region_list = table.VarStore.VarRegionList
var_region_axis = var_region_list.Region[0].VarRegionAxis[0]
assert self.get_region(var_region_axis) == (0.0, 0.875, 0.875)


def generate_feature_file_test(name):
Expand Down
11 changes: 11 additions & 0 deletions Tests/feaLib/parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,17 @@ def test_ligatureCaretByPos_singleGlyph(self):
self.assertEqual(glyphstr([s.glyphs]), "f_i")
self.assertEqual(s.carets, [400, 380])

def test_ligatureCaretByPos_variable_scalar(self):
doc = self.parse(
"table GDEF {LigatureCaretByPos f_i (wght=200:400 wght=900:1000) 380;} GDEF;"
)
s = doc.statements[0].statements[0]
self.assertIsInstance(s, ast.LigatureCaretByPosStatement)
self.assertEqual(glyphstr([s.glyphs]), "f_i")
self.assertEqual(len(s.carets), 2)
self.assertEqual(str(s.carets[0]), "(wght=200:400 wght=900:1000)")
self.assertEqual(s.carets[1], 380)

def test_lookup_block(self):
[lookup] = self.parse("lookup Ligatures {} Ligatures;").statements
self.assertEqual(lookup.name, "Ligatures")
Expand Down

0 comments on commit f7db6e1

Please sign in to comment.