[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) + " "
|
||||
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):
|
||||
|
@ -203,9 +203,12 @@ 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 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.
|
||||
@ -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,18 +1263,23 @@ 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:
|
||||
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):
|
||||
@ -1310,30 +1319,38 @@ 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:
|
||||
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):
|
||||
|
@ -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 == []
|
||||
|
@ -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):
|
||||
|
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.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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user