From 931bbc50c169bddc10e8c0332649778699f53959 Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Thu, 7 Jan 2016 10:31:13 +0100 Subject: [PATCH] [feaLib] Support compact syntax for chaining to ligature substitutions https://github.com/behdad/fonttools/issues/445 --- Lib/fontTools/feaLib/ast.py | 18 +++--- Lib/fontTools/feaLib/builder.py | 20 +++++- Lib/fontTools/feaLib/builder_test.py | 6 +- Lib/fontTools/feaLib/parser.py | 5 +- Lib/fontTools/feaLib/parser_test.py | 15 ++++- Lib/fontTools/feaLib/testdata/GSUB_6.fea | 5 ++ Lib/fontTools/feaLib/testdata/GSUB_6.ttx | 49 ++++++++++++++- Lib/fontTools/feaLib/testdata/spec5fi4.fea | 9 +++ Lib/fontTools/feaLib/testdata/spec5fi4.ttx | 73 ++++++++++++++++++++++ 9 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 Lib/fontTools/feaLib/testdata/spec5fi4.fea create mode 100644 Lib/fontTools/feaLib/testdata/spec5fi4.ttx diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index fdd4293c2..bbffe9fbd 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -250,19 +250,17 @@ class IgnoreSubstitutionRule(Statement): class LigatureSubstStatement(Statement): - def __init__(self, location, glyphs, replacement): + def __init__(self, location, prefix, glyphs, suffix, replacement): Statement.__init__(self, location) - self.glyphs, self.replacement = (glyphs, replacement) + self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) + self.replacement = replacement def build(self, builder): - # OpenType feature file syntax, section 5.d, "Ligature substitution": - # "Since the OpenType specification does not allow ligature - # substitutions to be specified on target sequences that contain - # glyph classes, the implementation software will enumerate - # all specific glyph sequences if glyph classes are detected" - g = [g.glyphSet() for g in self.glyphs] - for glyphs in sorted(itertools.product(*g)): - builder.add_ligature_subst(self.location, glyphs, self.replacement) + prefix = [p.glyphSet() for p in self.prefix] + glyphs = [g.glyphSet() for g in self.glyphs] + suffix = [s.glyphSet() for s in self.suffix] + builder.add_ligature_subst( + self.location, prefix, glyphs, suffix, self.replacement) class LookupFlagStatement(Statement): diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index 6ed5cf40a..83049ea3e 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -4,6 +4,7 @@ from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.parser import Parser from fontTools.ttLib import getTableClass from fontTools.ttLib.tables import otBase, otTables +import itertools import warnings @@ -410,9 +411,22 @@ class Builder(object): location) lookup.alternates[glyph] = from_class - def add_ligature_subst(self, location, glyphs, replacement): - lookup = self.get_lookup_(location, LigatureSubstBuilder) - lookup.ligatures[glyphs] = replacement + def add_ligature_subst(self, location, + prefix, glyphs, suffix, replacement): + if prefix or suffix: + lookup = self.get_chained_lookup_(location, LigatureSubstBuilder) + chain = self.get_lookup_(location, ChainContextSubstBuilder) + chain.substitutions.append((prefix, glyphs, suffix, [lookup])) + else: + lookup = self.get_lookup_(location, LigatureSubstBuilder) + + # OpenType feature file syntax, section 5.d, "Ligature substitution": + # "Since the OpenType specification does not allow ligature + # substitutions to be specified on target sequences that contain + # glyph classes, the implementation software will enumerate + # all specific glyph sequences if glyph classes are detected" + for g in sorted(itertools.product(*glyphs)): + lookup.ligatures[g] = replacement def add_multiple_subst(self, location, prefix, glyph, suffix, replacements): diff --git a/Lib/fontTools/feaLib/builder_test.py b/Lib/fontTools/feaLib/builder_test.py index a55fecba4..6b7a9a622 100644 --- a/Lib/fontTools/feaLib/builder_test.py +++ b/Lib/fontTools/feaLib/builder_test.py @@ -16,7 +16,7 @@ import unittest def makeTTFont(): glyphs = ( - ".notdef space slash fraction semicolon period comma " + ".notdef space slash fraction semicolon period comma ampersand " "zero one two three four five six seven eight nine " "zero.oldstyle one.oldstyle two.oldstyle three.oldstyle " "four.oldstyle five.oldstyle six.oldstyle seven.oldstyle " @@ -26,7 +26,7 @@ def makeTTFont(): "A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc " "N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc " "A.alt1 A.alt2 A.alt3 B.alt1 B.alt2 B.alt3 C.alt1 C.alt2 C.alt3 " - "d.alt n.end s.end " + "d.alt e.begin n.end s.end " "f_l c_h c_k c_s c_t f_f f_f_i f_f_l f_i o_f_f_i s_t " "ydieresis yacute " "grave acute dieresis macron circumflex cedilla umlaut ogonek caron " @@ -165,7 +165,7 @@ class BuilderTest(unittest.TestCase): self.expect_ttx(font, self.getpath("GSUB_%s.ttx" % name)) def test_spec(self): - for name in "4h1 5d1 5d2 5fi1 5fi2 5h1 6d2 6e 6f 6h_ii".split(): + for name in "4h1 5d1 5d2 5fi1 5fi2 5fi4 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 262640e7c..f3e746624 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -510,12 +510,13 @@ class Parser(object): # 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 + if (not reverse 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].glyphSet())[0]) + location, old_prefix, old, old_suffix, + list(new[0].glyphSet())[0]) # GSUB lookup type 8: Reverse chaining substitution. if reverse: diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 8b967a4be..1a6e95b9e 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -807,7 +807,7 @@ class ParserTest(unittest.TestCase): self.assertEqual(sub.glyph, "f_f_i") self.assertEqual(sub.replacement, ("f", "f", "i")) - def test_substitute_multiple_chained(self): # GSUB LookupType 2 + def test_substitute_multiple_chained(self): # chain to GSUB LookupType 2 doc = self.parse("lookup L {sub [A-C] f_f_i' [X-Z] by f f i;} L;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.MultipleSubstStatement) @@ -836,9 +836,20 @@ class ParserTest(unittest.TestCase): def test_substitute_ligature(self): # GSUB LookupType 4 doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;") sub = doc.statements[0].statements[0] - self.assertEqual(type(sub), ast.LigatureSubstStatement) + self.assertIsInstance(sub, ast.LigatureSubstStatement) self.assertEqual(glyphstr(sub.glyphs), "f f i") self.assertEqual(sub.replacement, "f_f_i") + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr(sub.suffix), "") + + def test_substitute_ligature_chained(self): # chain to GSUB LookupType 4 + doc = self.parse("feature F {substitute A B f' i' Z by f_i;} F;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.LigatureSubstStatement) + self.assertEqual(glyphstr(sub.glyphs), "f i") + self.assertEqual(sub.replacement, "f_i") + self.assertEqual(glyphstr(sub.prefix), "A B") + self.assertEqual(glyphstr(sub.suffix), "Z") def test_substitute_lookups(self): # GSUB LookupType 6 doc = Parser(self.getpath("spec5fi1.fea")).parse() diff --git a/Lib/fontTools/feaLib/testdata/GSUB_6.fea b/Lib/fontTools/feaLib/testdata/GSUB_6.fea index e5ece9d3e..5ac612965 100644 --- a/Lib/fontTools/feaLib/testdata/GSUB_6.fea +++ b/Lib/fontTools/feaLib/testdata/GSUB_6.fea @@ -7,7 +7,12 @@ lookup ChainedMultipleSubst { substitute [A-C a-c] [D d] E c_t' V [W w] [X-Z x-z] by c t; } ChainedMultipleSubst; +lookup ChainedLigatureSubst { + substitute A [C c]' [T t]' Z by c_t; +} ChainedLigatureSubst; + feature test { lookup ChainedSingleSubst; lookup ChainedMultipleSubst; + lookup ChainedLigatureSubst; } test; diff --git a/Lib/fontTools/feaLib/testdata/GSUB_6.ttx b/Lib/fontTools/feaLib/testdata/GSUB_6.ttx index d1846efdd..e796a55cf 100644 --- a/Lib/fontTools/feaLib/testdata/GSUB_6.ttx +++ b/Lib/fontTools/feaLib/testdata/GSUB_6.ttx @@ -22,14 +22,15 @@ - + + - + @@ -151,6 +152,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Lib/fontTools/feaLib/testdata/spec5fi4.fea b/Lib/fontTools/feaLib/testdata/spec5fi4.fea new file mode 100644 index 000000000..faabd94f8 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/spec5fi4.fea @@ -0,0 +1,9 @@ +# OpenType Feature File specification, section 5.f.i, example 4 +# "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 [e e.begin]' t' c by ampersand; +} test; diff --git a/Lib/fontTools/feaLib/testdata/spec5fi4.ttx b/Lib/fontTools/feaLib/testdata/spec5fi4.ttx new file mode 100644 index 000000000..411a180d5 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/spec5fi4.ttx @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +