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 27, 2023
1 parent fd854d7 commit 8f4f817
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 29 deletions.
9 changes: 9 additions & 0 deletions Lib/fontTools/feaLib/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1553,7 +1553,16 @@ def add_ligatureCaretByIndex_(self, location, glyphs, carets):
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

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/feaLib/builder.py#L1562

Added line #L1562 was not covered by tests

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
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 8f4f817

Please sign in to comment.