From b299bfb389883d79b4becf32e65ca774e2d2c623 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 12 May 2020 06:28:25 +0100 Subject: [PATCH] [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. --- Lib/fontTools/feaLib/ast.py | 10 ++- Lib/fontTools/feaLib/builder.py | 87 +++++++++++-------- Lib/fontTools/feaLib/parser.py | 9 +- Tests/feaLib/builder_test.py | 3 +- Tests/feaLib/data/MultipleLookupsPerGlyph.fea | 11 +++ Tests/feaLib/data/MultipleLookupsPerGlyph.ttx | 76 ++++++++++++++++ .../feaLib/data/MultipleLookupsPerGlyph2.fea | 11 +++ .../feaLib/data/MultipleLookupsPerGlyph2.ttx | 84 ++++++++++++++++++ Tests/feaLib/parser_test.py | 6 +- 9 files changed, 251 insertions(+), 46 deletions(-) create mode 100644 Tests/feaLib/data/MultipleLookupsPerGlyph.fea create mode 100644 Tests/feaLib/data/MultipleLookupsPerGlyph.ttx create mode 100644 Tests/feaLib/data/MultipleLookupsPerGlyph2.fea create mode 100644 Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index e416c0c62..c630a514d 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -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): diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index 52b23f1b2..d8c4476ce 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -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): diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index a3eaf6261..6f42fbe4d 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -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 == [] diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 5ce4cc266..f2f1c05d6 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -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): diff --git a/Tests/feaLib/data/MultipleLookupsPerGlyph.fea b/Tests/feaLib/data/MultipleLookupsPerGlyph.fea new file mode 100644 index 000000000..e0c22226b --- /dev/null +++ b/Tests/feaLib/data/MultipleLookupsPerGlyph.fea @@ -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; \ No newline at end of file diff --git a/Tests/feaLib/data/MultipleLookupsPerGlyph.ttx b/Tests/feaLib/data/MultipleLookupsPerGlyph.ttx new file mode 100644 index 000000000..927694cbc --- /dev/null +++ b/Tests/feaLib/data/MultipleLookupsPerGlyph.ttx @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/MultipleLookupsPerGlyph2.fea b/Tests/feaLib/data/MultipleLookupsPerGlyph2.fea new file mode 100644 index 000000000..5a9d19b24 --- /dev/null +++ b/Tests/feaLib/data/MultipleLookupsPerGlyph2.fea @@ -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; \ No newline at end of file diff --git a/Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx b/Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx new file mode 100644 index 000000000..008d95b65 --- /dev/null +++ b/Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py index d05a82448..87b8c96a4 100644 --- a/Tests/feaLib/parser_test.py +++ b/Tests/feaLib/parser_test.py @@ -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(