[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:
parent
a53bb37d8e
commit
b299bfb389
@ -546,8 +546,9 @@ class ChainContextPosStatement(Statement):
|
|||||||
res += " ".join(g.asFea() for g in self.prefix) + " "
|
res += " ".join(g.asFea() for g in self.prefix) + " "
|
||||||
for i, g in enumerate(self.glyphs):
|
for i, g in enumerate(self.glyphs):
|
||||||
res += g.asFea() + "'"
|
res += g.asFea() + "'"
|
||||||
if self.lookups[i] is not None:
|
if self.lookups[i]:
|
||||||
res += " lookup " + self.lookups[i].name
|
for lu in self.lookups[i]:
|
||||||
|
res += " lookup " + lu.name
|
||||||
if i < len(self.glyphs) - 1:
|
if i < len(self.glyphs) - 1:
|
||||||
res += " "
|
res += " "
|
||||||
if len(self.suffix):
|
if len(self.suffix):
|
||||||
@ -578,8 +579,9 @@ class ChainContextSubstStatement(Statement):
|
|||||||
res += " ".join(g.asFea() for g in self.prefix) + " "
|
res += " ".join(g.asFea() for g in self.prefix) + " "
|
||||||
for i, g in enumerate(self.glyphs):
|
for i, g in enumerate(self.glyphs):
|
||||||
res += g.asFea() + "'"
|
res += g.asFea() + "'"
|
||||||
if self.lookups[i] is not None:
|
if self.lookups[i]:
|
||||||
res += " lookup " + self.lookups[i].name
|
for lu in self.lookups[i]:
|
||||||
|
res += " lookup " + lu.name
|
||||||
if i < len(self.glyphs) - 1:
|
if i < len(self.glyphs) - 1:
|
||||||
res += " "
|
res += " "
|
||||||
if len(self.suffix):
|
if len(self.suffix):
|
||||||
|
@ -203,7 +203,10 @@ class Builder(object):
|
|||||||
raise FeatureLibError("Feature %s has not been defined" % name,
|
raise FeatureLibError("Feature %s has not been defined" % name,
|
||||||
location)
|
location)
|
||||||
for script, lang, feature, lookups in feature:
|
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():
|
for glyph, alts in lookup.getAlternateGlyphs().items():
|
||||||
alternates.setdefault(glyph, set()).update(alts)
|
alternates.setdefault(glyph, set()).update(alts)
|
||||||
single = {glyph: list(repl)[0] for glyph, repl in alternates.items()
|
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.
|
If an input name is None, it gets mapped to a None LookupBuilder.
|
||||||
"""
|
"""
|
||||||
lookup_builders = []
|
lookup_builders = []
|
||||||
for lookup in lookups:
|
for lookuplist in lookups:
|
||||||
if lookup is not None:
|
if lookuplist is not None:
|
||||||
lookup_builders.append(self.named_lookups_.get(lookup.name))
|
lookup_builders.append([self.named_lookups_.get(l.name)
|
||||||
|
for l in lookuplist])
|
||||||
else:
|
else:
|
||||||
lookup_builders.append(None)
|
lookup_builders.append(None)
|
||||||
return lookup_builders
|
return lookup_builders
|
||||||
@ -1259,10 +1263,15 @@ class ChainContextPosBuilder(LookupBuilder):
|
|||||||
self.setLookAheadCoverage_(suffix, st)
|
self.setLookAheadCoverage_(suffix, st)
|
||||||
self.setInputCoverage_(glyphs, st)
|
self.setInputCoverage_(glyphs, st)
|
||||||
|
|
||||||
st.PosCount = len([l for l in lookups if l is not None])
|
st.PosCount = 0
|
||||||
st.PosLookupRecord = []
|
st.PosLookupRecord = []
|
||||||
for sequenceIndex, l in enumerate(lookups):
|
for sequenceIndex, lookupList in enumerate(lookups):
|
||||||
if l is not None:
|
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:
|
if l.lookup_index is None:
|
||||||
raise FeatureLibError('Missing index of the specified '
|
raise FeatureLibError('Missing index of the specified '
|
||||||
'lookup, might be a substitution lookup',
|
'lookup, might be a substitution lookup',
|
||||||
@ -1310,10 +1319,15 @@ class ChainContextSubstBuilder(LookupBuilder):
|
|||||||
self.setLookAheadCoverage_(suffix, st)
|
self.setLookAheadCoverage_(suffix, st)
|
||||||
self.setInputCoverage_(input, st)
|
self.setInputCoverage_(input, st)
|
||||||
|
|
||||||
st.SubstCount = len([l for l in lookups if l is not None])
|
st.SubstCount = 0
|
||||||
st.SubstLookupRecord = []
|
st.SubstLookupRecord = []
|
||||||
for sequenceIndex, l in enumerate(lookups):
|
for sequenceIndex, lookupList in enumerate(lookups):
|
||||||
if l is not None:
|
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:
|
if l.lookup_index is None:
|
||||||
raise FeatureLibError('Missing index of the specified '
|
raise FeatureLibError('Missing index of the specified '
|
||||||
'lookup, might be a positioning lookup',
|
'lookup, might be a positioning lookup',
|
||||||
@ -1326,9 +1340,12 @@ class ChainContextSubstBuilder(LookupBuilder):
|
|||||||
|
|
||||||
def getAlternateGlyphs(self):
|
def getAlternateGlyphs(self):
|
||||||
result = {}
|
result = {}
|
||||||
for (_, _, _, lookups) in self.substitutions:
|
for (_, _, _, lookuplist) in self.substitutions:
|
||||||
if lookups == self.SUBTABLE_BREAK_:
|
if lookuplist == self.SUBTABLE_BREAK_:
|
||||||
continue
|
continue
|
||||||
|
for lookups in lookuplist:
|
||||||
|
if not isinstance(lookups, list):
|
||||||
|
lookups = [lookups]
|
||||||
for lookup in lookups:
|
for lookup in lookups:
|
||||||
if lookup is not None:
|
if lookup is not None:
|
||||||
alts = lookup.getAlternateGlyphs()
|
alts = lookup.getAlternateGlyphs()
|
||||||
|
@ -404,8 +404,10 @@ class Parser(object):
|
|||||||
else:
|
else:
|
||||||
values.append(None)
|
values.append(None)
|
||||||
|
|
||||||
lookup = None
|
lookuplist = None
|
||||||
if self.next_token_ == "lookup":
|
while self.next_token_ == "lookup":
|
||||||
|
if lookuplist is None:
|
||||||
|
lookuplist = []
|
||||||
self.expect_keyword_("lookup")
|
self.expect_keyword_("lookup")
|
||||||
if not marked:
|
if not marked:
|
||||||
raise FeatureLibError(
|
raise FeatureLibError(
|
||||||
@ -417,8 +419,9 @@ class Parser(object):
|
|||||||
raise FeatureLibError(
|
raise FeatureLibError(
|
||||||
'Unknown lookup "%s"' % lookup_name,
|
'Unknown lookup "%s"' % lookup_name,
|
||||||
self.cur_token_location_)
|
self.cur_token_location_)
|
||||||
|
lookuplist.append(lookup)
|
||||||
if marked:
|
if marked:
|
||||||
lookups.append(lookup)
|
lookups.append(lookuplist)
|
||||||
|
|
||||||
if not glyphs and not suffix: # eg., "sub f f i by"
|
if not glyphs and not suffix: # eg., "sub f f i by"
|
||||||
assert lookups == []
|
assert lookups == []
|
||||||
|
@ -71,7 +71,8 @@ class BuilderTest(unittest.TestCase):
|
|||||||
ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical
|
ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical
|
||||||
PairPosSubtable ChainSubstSubtable ChainPosSubtable LigatureSubtable
|
PairPosSubtable ChainSubstSubtable ChainPosSubtable LigatureSubtable
|
||||||
AlternateSubtable MultipleSubstSubtable SingleSubstSubtable
|
AlternateSubtable MultipleSubstSubtable SingleSubstSubtable
|
||||||
aalt_chain_contextual_subst AlternateChained
|
aalt_chain_contextual_subst AlternateChained MultipleLookupsPerGlyph
|
||||||
|
MultipleLookupsPerGlyph2
|
||||||
""".split()
|
""".split()
|
||||||
|
|
||||||
def __init__(self, methodName):
|
def __init__(self, methodName):
|
||||||
|
11
Tests/feaLib/data/MultipleLookupsPerGlyph.fea
Normal file
11
Tests/feaLib/data/MultipleLookupsPerGlyph.fea
Normal 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;
|
76
Tests/feaLib/data/MultipleLookupsPerGlyph.ttx
Normal file
76
Tests/feaLib/data/MultipleLookupsPerGlyph.ttx
Normal 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>
|
11
Tests/feaLib/data/MultipleLookupsPerGlyph2.fea
Normal file
11
Tests/feaLib/data/MultipleLookupsPerGlyph2.fea
Normal 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;
|
84
Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx
Normal file
84
Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx
Normal 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>
|
@ -1065,7 +1065,7 @@ class ParserTest(unittest.TestCase):
|
|||||||
self.assertEqual(glyphstr(pos.prefix), "[A a] [B b]")
|
self.assertEqual(glyphstr(pos.prefix), "[A a] [B b]")
|
||||||
self.assertEqual(glyphstr(pos.glyphs), "I [N n] P")
|
self.assertEqual(glyphstr(pos.glyphs), "I [N n] P")
|
||||||
self.assertEqual(glyphstr(pos.suffix), "[Y y] [Z z]")
|
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):
|
def test_gpos_type_8_lookup_with_values(self):
|
||||||
self.assertRaisesRegex(
|
self.assertRaisesRegex(
|
||||||
@ -1508,8 +1508,8 @@ class ParserTest(unittest.TestCase):
|
|||||||
def test_substitute_lookups(self): # GSUB LookupType 6
|
def test_substitute_lookups(self): # GSUB LookupType 6
|
||||||
doc = Parser(self.getpath("spec5fi1.fea"), GLYPHNAMES).parse()
|
doc = Parser(self.getpath("spec5fi1.fea"), GLYPHNAMES).parse()
|
||||||
[_, _, _, langsys, ligs, sub, feature] = doc.statements
|
[_, _, _, langsys, ligs, sub, feature] = doc.statements
|
||||||
self.assertEqual(feature.statements[0].lookups, [ligs, None, sub])
|
self.assertEqual(feature.statements[0].lookups, [[ligs], None, [sub]])
|
||||||
self.assertEqual(feature.statements[1].lookups, [ligs, None, sub])
|
self.assertEqual(feature.statements[1].lookups, [[ligs], None, [sub]])
|
||||||
|
|
||||||
def test_substitute_missing_by(self):
|
def test_substitute_missing_by(self):
|
||||||
self.assertRaisesRegex(
|
self.assertRaisesRegex(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user