Skip to content

Commit

Permalink
[feaLib] Support multiple lookups per glyph position (#1905)
Browse files Browse the repository at this point in the history
This allows for more than one "lookup ..." chaining statements at each glyph position in a chaining contextual substitution or positioning rule: e.g.

    sub a b c' lookup lookup1 lookup lookup2 d;

The corresponding change in the Adobe OpenType Feature File Specification (and implementation in makeotf) happened in adobe-type-tools/afdko#1132.
  • Loading branch information
simoncozens committed May 12, 2020
1 parent a53bb37 commit b299bfb
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 46 deletions.
10 changes: 6 additions & 4 deletions Lib/fontTools/feaLib/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,8 +546,9 @@ def asFea(self, indent=""):
res += " ".join(g.asFea() for g in self.prefix) + " "
for i, g in enumerate(self.glyphs):
res += g.asFea() + "'"
if self.lookups[i] is not None:
res += " lookup " + self.lookups[i].name
if self.lookups[i]:
for lu in self.lookups[i]:
res += " lookup " + lu.name
if i < len(self.glyphs) - 1:
res += " "
if len(self.suffix):
Expand Down Expand Up @@ -578,8 +579,9 @@ def asFea(self, indent=""):
res += " ".join(g.asFea() for g in self.prefix) + " "
for i, g in enumerate(self.glyphs):
res += g.asFea() + "'"
if self.lookups[i] is not None:
res += " lookup " + self.lookups[i].name
if self.lookups[i]:
for lu in self.lookups[i]:
res += " lookup " + lu.name
if i < len(self.glyphs) - 1:
res += " "
if len(self.suffix):
Expand Down
87 changes: 52 additions & 35 deletions Lib/fontTools/feaLib/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,12 @@ def build_feature_aalt_(self):
raise FeatureLibError("Feature %s has not been defined" % name,
location)
for script, lang, feature, lookups in feature:
for lookup in lookups:
for glyph, alts in lookup.getAlternateGlyphs().items():
alternates.setdefault(glyph, set()).update(alts)
for lookuplist in lookups:
if not isinstance(lookuplist, list):
lookuplist = [lookuplist]
for lookup in lookuplist:
for glyph, alts in lookup.getAlternateGlyphs().items():
alternates.setdefault(glyph, set()).update(alts)
single = {glyph: list(repl)[0] for glyph, repl in alternates.items()
if len(repl) == 1}
# TODO: Figure out the glyph alternate ordering used by makeotf.
Expand Down Expand Up @@ -797,9 +800,10 @@ def find_lookup_builders_(self, lookups):
If an input name is None, it gets mapped to a None LookupBuilder.
"""
lookup_builders = []
for lookup in lookups:
if lookup is not None:
lookup_builders.append(self.named_lookups_.get(lookup.name))
for lookuplist in lookups:
if lookuplist is not None:
lookup_builders.append([self.named_lookups_.get(l.name)
for l in lookuplist])
else:
lookup_builders.append(None)
return lookup_builders
Expand Down Expand Up @@ -1259,18 +1263,23 @@ def build(self):
self.setLookAheadCoverage_(suffix, st)
self.setInputCoverage_(glyphs, st)

st.PosCount = len([l for l in lookups if l is not None])
st.PosCount = 0
st.PosLookupRecord = []
for sequenceIndex, l in enumerate(lookups):
if l is not None:
if l.lookup_index is None:
raise FeatureLibError('Missing index of the specified '
'lookup, might be a substitution lookup',
self.location)
rec = otTables.PosLookupRecord()
rec.SequenceIndex = sequenceIndex
rec.LookupListIndex = l.lookup_index
st.PosLookupRecord.append(rec)
for sequenceIndex, lookupList in enumerate(lookups):
if lookupList is not None:
if not isinstance(lookupList, list):
# Can happen with synthesised lookups
lookupList = [ lookupList ]
for l in lookupList:
st.PosCount += 1
if l.lookup_index is None:
raise FeatureLibError('Missing index of the specified '
'lookup, might be a substitution lookup',
self.location)
rec = otTables.PosLookupRecord()
rec.SequenceIndex = sequenceIndex
rec.LookupListIndex = l.lookup_index
st.PosLookupRecord.append(rec)
return self.buildLookup_(subtables)

def find_chainable_single_pos(self, lookups, glyphs, value):
Expand Down Expand Up @@ -1310,30 +1319,38 @@ def build(self):
self.setLookAheadCoverage_(suffix, st)
self.setInputCoverage_(input, st)

st.SubstCount = len([l for l in lookups if l is not None])
st.SubstCount = 0
st.SubstLookupRecord = []
for sequenceIndex, l in enumerate(lookups):
if l is not None:
if l.lookup_index is None:
raise FeatureLibError('Missing index of the specified '
'lookup, might be a positioning lookup',
self.location)
rec = otTables.SubstLookupRecord()
rec.SequenceIndex = sequenceIndex
rec.LookupListIndex = l.lookup_index
st.SubstLookupRecord.append(rec)
for sequenceIndex, lookupList in enumerate(lookups):
if lookupList is not None:
if not isinstance(lookupList, list):
# Can happen with synthesised lookups
lookupList = [ lookupList ]
for l in lookupList:
st.SubstCount += 1
if l.lookup_index is None:
raise FeatureLibError('Missing index of the specified '
'lookup, might be a positioning lookup',
self.location)
rec = otTables.SubstLookupRecord()
rec.SequenceIndex = sequenceIndex
rec.LookupListIndex = l.lookup_index
st.SubstLookupRecord.append(rec)
return self.buildLookup_(subtables)

def getAlternateGlyphs(self):
result = {}
for (_, _, _, lookups) in self.substitutions:
if lookups == self.SUBTABLE_BREAK_:
for (_, _, _, lookuplist) in self.substitutions:
if lookuplist == self.SUBTABLE_BREAK_:
continue
for lookup in lookups:
if lookup is not None:
alts = lookup.getAlternateGlyphs()
for glyph, replacements in alts.items():
result.setdefault(glyph, set()).update(replacements)
for lookups in lookuplist:
if not isinstance(lookups, list):
lookups = [lookups]
for lookup in lookups:
if lookup is not None:
alts = lookup.getAlternateGlyphs()
for glyph, replacements in alts.items():
result.setdefault(glyph, set()).update(replacements)
return result

def find_chainable_single_subst(self, glyphs):
Expand Down
9 changes: 6 additions & 3 deletions Lib/fontTools/feaLib/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,10 @@ def parse_glyph_pattern_(self, vertical):
else:
values.append(None)

lookup = None
if self.next_token_ == "lookup":
lookuplist = None
while self.next_token_ == "lookup":
if lookuplist is None:
lookuplist = []
self.expect_keyword_("lookup")
if not marked:
raise FeatureLibError(
Expand All @@ -417,8 +419,9 @@ def parse_glyph_pattern_(self, vertical):
raise FeatureLibError(
'Unknown lookup "%s"' % lookup_name,
self.cur_token_location_)
lookuplist.append(lookup)
if marked:
lookups.append(lookup)
lookups.append(lookuplist)

if not glyphs and not suffix: # eg., "sub f f i by"
assert lookups == []
Expand Down
3 changes: 2 additions & 1 deletion Tests/feaLib/builder_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ class BuilderTest(unittest.TestCase):
ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical
PairPosSubtable ChainSubstSubtable ChainPosSubtable LigatureSubtable
AlternateSubtable MultipleSubstSubtable SingleSubstSubtable
aalt_chain_contextual_subst AlternateChained
aalt_chain_contextual_subst AlternateChained MultipleLookupsPerGlyph
MultipleLookupsPerGlyph2
""".split()

def __init__(self, methodName):
Expand Down
11 changes: 11 additions & 0 deletions Tests/feaLib/data/MultipleLookupsPerGlyph.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
lookup a_to_bc {
sub a by b c;
} a_to_bc;

lookup b_to_d {
sub b by d;
} b_to_d;

feature test {
sub a' lookup a_to_bc lookup b_to_d b;
} test;
76 changes: 76 additions & 0 deletions Tests/feaLib/data/MultipleLookupsPerGlyph.ttx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>

<GSUB>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="test"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="2"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=3 -->
<Lookup index="0">
<LookupType value="2"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<MultipleSubst index="0">
<Substitution in="a" out="b,c"/>
</MultipleSubst>
</Lookup>
<Lookup index="1">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0">
<Substitution in="b" out="d"/>
</SingleSubst>
</Lookup>
<Lookup index="2">
<LookupType value="6"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ChainContextSubst index="0" Format="3">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="a"/>
</InputCoverage>
<!-- LookAheadGlyphCount=1 -->
<LookAheadCoverage index="0">
<Glyph value="b"/>
</LookAheadCoverage>
<!-- SubstCount=2 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
</SubstLookupRecord>
<SubstLookupRecord index="1">
<SequenceIndex value="0"/>
<LookupListIndex value="1"/>
</SubstLookupRecord>
</ChainContextSubst>
</Lookup>
</LookupList>
</GSUB>

</ttFont>
11 changes: 11 additions & 0 deletions Tests/feaLib/data/MultipleLookupsPerGlyph2.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
lookup a_reduce_sb {
pos a <-80 0 -160 0>;
} a_reduce_sb;

lookup a_raise {
pos a <0 100 0 0>;
} a_raise;

feature test {
pos a' lookup a_reduce_sb lookup a_raise b;
} test;
84 changes: 84 additions & 0 deletions Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>

<GPOS>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="test"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="2"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=3 -->
<Lookup index="0">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SinglePos index="0" Format="1">
<Coverage>
<Glyph value="a"/>
</Coverage>
<ValueFormat value="5"/>
<Value XPlacement="-80" XAdvance="-160"/>
</SinglePos>
</Lookup>
<Lookup index="1">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SinglePos index="0" Format="1">
<Coverage>
<Glyph value="a"/>
</Coverage>
<ValueFormat value="2"/>
<Value YPlacement="100"/>
</SinglePos>
</Lookup>
<Lookup index="2">
<LookupType value="8"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ChainContextPos index="0" Format="3">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="a"/>
</InputCoverage>
<!-- LookAheadGlyphCount=1 -->
<LookAheadCoverage index="0">
<Glyph value="b"/>
</LookAheadCoverage>
<!-- PosCount=2 -->
<PosLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
</PosLookupRecord>
<PosLookupRecord index="1">
<SequenceIndex value="0"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
</ChainContextPos>
</Lookup>
</LookupList>
</GPOS>

</ttFont>
6 changes: 3 additions & 3 deletions Tests/feaLib/parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@ def test_gpos_type_8(self):
self.assertEqual(glyphstr(pos.prefix), "[A a] [B b]")
self.assertEqual(glyphstr(pos.glyphs), "I [N n] P")
self.assertEqual(glyphstr(pos.suffix), "[Y y] [Z z]")
self.assertEqual(pos.lookups, [lookup1, lookup2, None])
self.assertEqual(pos.lookups, [[lookup1], [lookup2], None])

def test_gpos_type_8_lookup_with_values(self):
self.assertRaisesRegex(
Expand Down Expand Up @@ -1508,8 +1508,8 @@ def test_substitute_ligature_chained(self): # chain to GSUB LookupType 4
def test_substitute_lookups(self): # GSUB LookupType 6
doc = Parser(self.getpath("spec5fi1.fea"), GLYPHNAMES).parse()
[_, _, _, langsys, ligs, sub, feature] = doc.statements
self.assertEqual(feature.statements[0].lookups, [ligs, None, sub])
self.assertEqual(feature.statements[1].lookups, [ligs, None, sub])
self.assertEqual(feature.statements[0].lookups, [[ligs], None, [sub]])
self.assertEqual(feature.statements[1].lookups, [[ligs], None, [sub]])

def test_substitute_missing_by(self):
self.assertRaisesRegex(
Expand Down

0 comments on commit b299bfb

Please sign in to comment.