[feaLib] Support multiple lookups per glyph position (#1905)

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.
This commit is contained in:
Simon Cozens 2020-05-12 06:28:25 +01:00 committed by GitHub
parent a53bb37d8e
commit b299bfb389
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 251 additions and 46 deletions

View File

@ -546,8 +546,9 @@ class ChainContextPosStatement(Statement):
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):
@ -578,8 +579,9 @@ class ChainContextSubstStatement(Statement):
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):

View File

@ -203,7 +203,10 @@ class Builder(object):
raise FeatureLibError("Feature %s has not been defined" % name,
location)
for script, lang, feature, lookups in feature:
for lookup in lookups:
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()
@ -797,9 +800,10 @@ class Builder(object):
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
@ -1259,10 +1263,15 @@ class ChainContextPosBuilder(LookupBuilder):
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:
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',
@ -1310,10 +1319,15 @@ class ChainContextSubstBuilder(LookupBuilder):
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:
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',
@ -1326,9 +1340,12 @@ class ChainContextSubstBuilder(LookupBuilder):
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 lookups in lookuplist:
if not isinstance(lookups, list):
lookups = [lookups]
for lookup in lookups:
if lookup is not None:
alts = lookup.getAlternateGlyphs()

View File

@ -404,8 +404,10 @@ class Parser(object):
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(
@ -417,8 +419,9 @@ class Parser(object):
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 == []

View File

@ -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):

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -1065,7 +1065,7 @@ class ParserTest(unittest.TestCase):
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(
@ -1508,8 +1508,8 @@ class ParserTest(unittest.TestCase):
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(