diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index a69580013..62c9992c8 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -198,8 +198,8 @@ class ChainContextPosStatement(Statement): class ChainContextSubstStatement(Statement): def __init__(self, location, prefix, glyphs, suffix, lookups): Statement.__init__(self, location) - self.glyphs, self.lookups = glyphs, lookups - self.prefix, self.suffix = prefix, suffix + self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix + self.lookups = lookups def build(self, builder): prefix = [p.glyphSet() for p in self.prefix] @@ -373,12 +373,14 @@ class ReverseChainSingleSubstStatement(Statement): class SingleSubstStatement(Statement): - def __init__(self, location, mapping): + def __init__(self, location, mapping, prefix, suffix): Statement.__init__(self, location) - self.mapping = mapping + self.mapping, self.prefix, self.suffix = mapping, prefix, suffix def build(self, builder): - builder.add_single_subst(self.location, self.mapping) + prefix = [p.glyphSet() for p in self.prefix] + suffix = [s.glyphSet() for s in self.suffix] + builder.add_single_subst(self.location, prefix, suffix, self.mapping) class ScriptStatement(Statement): diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index 2569c18e4..5c524fe33 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -51,6 +51,13 @@ class Builder(object): elif "GDEF" in self.font: del self.font["GDEF"] + def get_chained_lookup_(self, location, builder_class): + result = builder_class(self.font, location) + result.lookupflag = self.lookupflag_ + result.markFilterSet = self.lookupflag_markFilterSet_ + self.lookups_.append(result) + return result + def get_lookup_(self, location, builder_class): if (self.cur_lookup_ and type(self.cur_lookup_) == builder_class and @@ -410,7 +417,14 @@ class Builder(object): lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder) lookup.substitutions.append((old_prefix, old_suffix, mapping)) - def add_single_subst(self, location, mapping): + def add_single_subst(self, location, prefix, suffix, mapping): + if prefix or suffix: + sub = self.get_chained_lookup_(location, SingleSubstBuilder) + sub.mapping.update(mapping) + lookup = self.get_lookup_(location, ChainContextSubstBuilder) + lookup.substitutions.append( + (prefix, [mapping.keys()], suffix, [sub])) + return lookup = self.get_lookup_(location, SingleSubstBuilder) for (from_glyph, to_glyph) in mapping.items(): if from_glyph in lookup.mapping: diff --git a/Lib/fontTools/feaLib/builder_test.py b/Lib/fontTools/feaLib/builder_test.py index 25e931a90..9ef45b40c 100644 --- a/Lib/fontTools/feaLib/builder_test.py +++ b/Lib/fontTools/feaLib/builder_test.py @@ -174,7 +174,7 @@ class BuilderTest(unittest.TestCase): self.expect_ttx(font, self.getpath("GPOS_%s.ttx" % name)) def test_spec(self): - for name in "4h1 5d1 5d2 5fi1 5h1 6d2 6e 6f 6h_ii".split(): + for name in "4h1 5d1 5d2 5fi1 5fi2 5h1 6d2 6e 6f 6h_ii".split(): font = makeTTFont() addOpenTypeFeatures(self.getpath("spec%s.fea" % name), font) self.expect_ttx(font, self.getpath("spec%s.ttx" % name)) diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 94a7c4ed0..178c50e69 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -446,10 +446,10 @@ class Parser(object): keyword = self.expect_keyword_("by") while self.next_token_ != ";": gc = self.parse_glyphclass_(accept_glyphname=True) - new.append(gc.glyphSet()) + new.append(gc) elif self.next_token_ == "from": keyword = self.expect_keyword_("from") - new = [self.parse_glyphclass_(accept_glyphname=False).glyphSet()] + new = [self.parse_glyphclass_(accept_glyphname=False)] else: keyword = None self.expect_symbol_(";") @@ -475,7 +475,7 @@ class Parser(object): location) return ast.AlternateSubstStatement(location, list(old[0].glyphSet())[0], - new[0]) + new[0].glyphSet()) num_lookups = len([l for l in lookups if l is not None]) @@ -483,10 +483,10 @@ class Parser(object): # Format A: "substitute a by a.sc;" # Format B: "substitute [one.fitted one.oldstyle] by one;" # Format C: "substitute [a-d] by [A.sc-D.sc];" - if (not reverse and len(old_prefix) == 0 and len(old_suffix) == 0 and - len(old) == 1 and len(new) == 1 and num_lookups == 0): + if (not reverse and len(old) == 1 and len(new) == 1 and + num_lookups == 0): glyphs = sorted(list(old[0].glyphSet())) - replacements = sorted(list(new[0])) + replacements = sorted(list(new[0].glyphSet())) if len(replacements) == 1: replacements = replacements * len(glyphs) if len(glyphs) != len(replacements): @@ -495,24 +495,28 @@ class Parser(object): 'but found a glyph class with %d elements' % (len(glyphs), len(replacements)), location) return ast.SingleSubstStatement(location, - dict(zip(glyphs, replacements))) + dict(zip(glyphs, replacements)), + old_prefix, old_suffix) # GSUB lookup type 2: Multiple substitution. # Format: "substitute f_f_i by f f i;" if (not reverse and len(old_prefix) == 0 and len(old_suffix) == 0 and len(old) == 1 and len(old[0].glyphSet()) == 1 and - len(new) > 1 and max([len(n) for n in new]) == 1 and + len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1 and num_lookups == 0): - return ast.MultipleSubstStatement(location, - tuple(old[0].glyphSet())[0], - tuple([list(n)[0] for n in new])) + return ast.MultipleSubstStatement( + location, + tuple(old[0].glyphSet())[0], + tuple([list(n.glyphSet())[0] for n in new])) # GSUB lookup type 4: Ligature substitution. # Format: "substitute f f i by f_f_i;" if (not reverse and len(old_prefix) == 0 and len(old_suffix) == 0 and - len(old) > 1 and len(new) == 1 and len(new[0]) == 1 and + len(old) > 1 and len(new) == 1 and + len(new[0].glyphSet()) == 1 and num_lookups == 0): - return ast.LigatureSubstStatement(location, old, list(new[0])[0]) + return ast.LigatureSubstStatement( + location, old, list(new[0].glyphSet())[0]) # GSUB lookup type 8: Reverse chaining substitution. if reverse: @@ -531,7 +535,7 @@ class Parser(object): "Reverse chaining substitutions cannot call named lookups", location) glyphs = sorted(list(old[0].glyphSet())) - replacements = sorted(list(new[0])) + replacements = sorted(list(new[0].glyphSet())) if len(replacements) == 1: replacements = replacements * len(glyphs) if len(glyphs) != len(replacements): diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 96c989859..94cf0cd17 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -717,39 +717,83 @@ class ParserTest(unittest.TestCase): '"dflt" is not a valid script tag; use "DFLT" instead', self.parse, "feature test {script dflt;} test;") - def test_substitute_single_format_a(self): # GSUB LookupType 1 + def test_sub_single_format_a(self): # GSUB LookupType 1 doc = self.parse("feature smcp {substitute a by a.sc;} smcp;") sub = doc.statements[0].statements[0] self.assertEqual(type(sub), ast.SingleSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "") self.assertEqual(sub.mapping, {"a": "a.sc"}) + self.assertEqual(glyphstr(sub.suffix), "") - def test_substitute_single_format_b(self): # GSUB LookupType 1 + def test_sub_single_format_a_chained(self): # chain to GSUB LookupType 1 + doc = self.parse("feature test {sub [A a] d' [C] by d.alt;} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(sub.mapping, {"d": "d.alt"}) + self.assertEqual(glyphstr(sub.prefix), "[A a]") + self.assertEqual(glyphstr(sub.suffix), "C") + + def test_sub_single_format_b(self): # GSUB LookupType 1 doc = self.parse( "feature smcp {" " substitute [one.fitted one.oldstyle] by one;" "} smcp;") sub = doc.statements[0].statements[0] - self.assertEqual(type(sub), ast.SingleSubstStatement) + self.assertIsInstance(sub, ast.SingleSubstStatement) self.assertEqual(sub.mapping, { "one.fitted": "one", "one.oldstyle": "one" }) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr(sub.suffix), "") - def test_substitute_single_format_c(self): # GSUB LookupType 1 + def test_sub_single_format_b_chained(self): # chain to GSUB LookupType 1 + doc = self.parse( + "feature smcp {" + " substitute PRE FIX [one.fitted one.oldstyle]' SUF FIX by one;" + "} smcp;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(sub.mapping, { + "one.fitted": "one", + "one.oldstyle": "one" + }) + self.assertEqual(glyphstr(sub.prefix), "PRE FIX") + self.assertEqual(glyphstr(sub.suffix), "SUF FIX") + + def test_sub_single_format_c(self): # GSUB LookupType 1 doc = self.parse( "feature smcp {" " substitute [a-d] by [A.sc-D.sc];" "} smcp;") sub = doc.statements[0].statements[0] - self.assertEqual(type(sub), ast.SingleSubstStatement) + self.assertIsInstance(sub, ast.SingleSubstStatement) self.assertEqual(sub.mapping, { "a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc" }) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr(sub.suffix), "") - def test_substitute_single_format_c_different_num_elements(self): + def test_sub_single_format_c_chained(self): # chain to GSUB LookupType 1 + doc = self.parse( + "feature smcp {" + " substitute [a-d]' X Y [Z z] by [A.sc-D.sc];" + "} smcp;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(sub.mapping, { + "a": "A.sc", + "b": "B.sc", + "c": "C.sc", + "d": "D.sc" + }) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr(sub.suffix), "X Y [Z z]") + + def test_sub_single_format_c_different_num_elements(self): self.assertRaisesRegex( FeatureLibError, 'Expected a glyph class with 4 elements after "by", ' @@ -789,7 +833,7 @@ class ParserTest(unittest.TestCase): self.assertEqual(glyphstr(sub.glyphs), "f f i") self.assertEqual(sub.replacement, "f_f_i") - def test_substitute_lookups(self): + def test_substitute_lookups(self): # GSUB LookupType 6 doc = Parser(self.getpath("spec5fi1.fea")).parse() [langsys, ligs, sub, feature] = doc.statements self.assertEqual(feature.statements[0].lookups, [ligs, None, sub]) diff --git a/Lib/fontTools/feaLib/testdata/spec5fi2.fea b/Lib/fontTools/feaLib/testdata/spec5fi2.fea new file mode 100644 index 000000000..235a1d8e1 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/spec5fi2.fea @@ -0,0 +1,10 @@ +# OpenType Feature File specification, section 5.f.i, example 2 +# "Specifying a Chain Sub rule and marking sub-runs" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem latn dflt; + +feature test { + lookupflag 7; + substitute [a e n] d' by d.alt; +} test; diff --git a/Lib/fontTools/feaLib/testdata/spec5fi2.ttx b/Lib/fontTools/feaLib/testdata/spec5fi2.ttx new file mode 100644 index 000000000..0bce0907a --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/spec5fi2.ttx @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Lib/fontTools/feaLib/testdata/spec5fi3.fea b/Lib/fontTools/feaLib/testdata/spec5fi3.fea new file mode 100644 index 000000000..f6215b165 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/spec5fi3.fea @@ -0,0 +1,9 @@ +# OpenType Feature File specification, section 5.f.i, example 2 +# "Specifying a Chain Sub rule and marking sub-runs" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem latn dflt; + +feature test { + substitute [A-Z] [A.sc-Z.sc]' by [a-z]; +} test; diff --git a/Lib/fontTools/feaLib/testdata/spec5fi3.ttx b/Lib/fontTools/feaLib/testdata/spec5fi3.ttx new file mode 100644 index 000000000..3bc260acd --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/spec5fi3.ttx @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +