From db49f20d6b2629e1ba25c4afd3fb60817387f3d6 Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Thu, 7 Jan 2016 11:32:54 +0100 Subject: [PATCH] [feaLib] Support compact syntax for chaining to alternate substitutions https://github.com/behdad/fonttools/issues/445 Not sure whether it makes much sense to define a contextual chain that points to a GSUB type 3, but the OpenType feature file syntax does not explicitly forbid it. Adobe's `makeotf` tool rejects this kind of input with "Contextual alternate rule not yet supported"; this implies that the construct is valid (albeit definitely exotic). --- Lib/fontTools/feaLib/ast.py | 14 ++++++-- Lib/fontTools/feaLib/builder.py | 12 +++++-- Lib/fontTools/feaLib/parser.py | 5 ++- Lib/fontTools/feaLib/parser_test.py | 28 +++++++++++---- Lib/fontTools/feaLib/testdata/GSUB_6.fea | 5 +++ Lib/fontTools/feaLib/testdata/GSUB_6.ttx | 43 +++++++++++++++++++++--- 6 files changed, 88 insertions(+), 19 deletions(-) diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index bbffe9fbd..5f828ed38 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -159,12 +159,20 @@ class MarkClassDefinition(Statement): class AlternateSubstStatement(Statement): - def __init__(self, location, glyph, from_class): + def __init__(self, location, prefix, glyph, suffix, replacement): Statement.__init__(self, location) - self.glyph, self.from_class = (glyph, from_class) + self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix) + self.replacement = replacement def build(self, builder): - builder.add_alternate_subst(self.location, self.glyph, self.from_class) + glyph = self.glyph.glyphSet() + assert len(glyph) == 1, glyph + glyph = list(glyph)[0] + prefix = [p.glyphSet() for p in self.prefix] + suffix = [s.glyphSet() for s in self.suffix] + replacement = self.replacement.glyphSet() + builder.add_alternate_subst(self.location, prefix, glyph, suffix, + replacement) class Anchor(Expression): diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index 83049ea3e..7a9d68342 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -403,13 +403,19 @@ class Builder(object): lookup.substitutions.append((prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) - def add_alternate_subst(self, location, glyph, from_class): - lookup = self.get_lookup_(location, AlternateSubstBuilder) + def add_alternate_subst(self, location, + prefix, glyph, suffix, replacement): + if prefix or suffix: + lookup = self.get_chained_lookup_(location, AlternateSubstBuilder) + chain = self.get_lookup_(location, ChainContextSubstBuilder) + chain.substitutions.append((prefix, [glyph], suffix, [lookup])) + else: + lookup = self.get_lookup_(location, AlternateSubstBuilder) if glyph in lookup.alternates: raise FeatureLibError( 'Already defined alternates for glyph "%s"' % glyph, location) - lookup.alternates[glyph] = from_class + lookup.alternates[glyph] = replacement def add_ligature_subst(self, location, prefix, glyphs, suffix, replacement): diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index f3e746624..d658cd42d 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -473,9 +473,8 @@ class Parser(object): raise FeatureLibError( 'Expected a single glyphclass after "from"', location) - return ast.AlternateSubstStatement(location, - list(old[0].glyphSet())[0], - new[0].glyphSet()) + return ast.AlternateSubstStatement( + location, old_prefix, old[0], old_suffix, new[0]) num_lookups = len([l for l in lookups if l is not None]) diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 1a6e95b9e..c5d9e742a 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -819,9 +819,22 @@ class ParserTest(unittest.TestCase): " substitute a from [a.1 a.2 a.3];" "} test;") sub = doc.statements[0].statements[0] - self.assertEqual(type(sub), ast.AlternateSubstStatement) - self.assertEqual(sub.glyph, "a") - self.assertEqual(sub.from_class, {"a.1", "a.2", "a.3"}) + self.assertIsInstance(sub, ast.AlternateSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr([sub.glyph]), "a") + self.assertEqual(glyphstr(sub.suffix), "") + self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]") + + def test_substitute_from_chained(self): # chain to GSUB LookupType 3 + doc = self.parse("feature test {" + " substitute A B a' [Y y] Z from [a.1 a.2 a.3];" + "} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.AlternateSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "A B") + self.assertEqual(glyphstr([sub.glyph]), "a") + self.assertEqual(glyphstr(sub.suffix), "[Y y] Z") + self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]") def test_substitute_from_glyphclass(self): # GSUB LookupType 3 doc = self.parse("feature test {" @@ -829,9 +842,12 @@ class ParserTest(unittest.TestCase): " substitute ampersand from @Ampersands;" "} test;") [glyphclass, sub] = doc.statements[0].statements - self.assertEqual(type(sub), ast.AlternateSubstStatement) - self.assertEqual(sub.glyph, "ampersand") - self.assertEqual(sub.from_class, {"ampersand.1", "ampersand.2"}) + self.assertIsInstance(sub, ast.AlternateSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr([sub.glyph]), "ampersand") + self.assertEqual(glyphstr(sub.suffix), "") + self.assertEqual(glyphstr([sub.replacement]), + "[ampersand.1 ampersand.2]") def test_substitute_ligature(self): # GSUB LookupType 4 doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;") diff --git a/Lib/fontTools/feaLib/testdata/GSUB_6.fea b/Lib/fontTools/feaLib/testdata/GSUB_6.fea index 5ac612965..ca037cb2a 100644 --- a/Lib/fontTools/feaLib/testdata/GSUB_6.fea +++ b/Lib/fontTools/feaLib/testdata/GSUB_6.fea @@ -7,6 +7,10 @@ lookup ChainedMultipleSubst { substitute [A-C a-c] [D d] E c_t' V [W w] [X-Z x-z] by c t; } ChainedMultipleSubst; +lookup ChainedAlternateSubst { + substitute [space comma semicolon] e' from [e e.begin]; +} ChainedAlternateSubst; + lookup ChainedLigatureSubst { substitute A [C c]' [T t]' Z by c_t; } ChainedLigatureSubst; @@ -14,5 +18,6 @@ lookup ChainedLigatureSubst { feature test { lookup ChainedSingleSubst; lookup ChainedMultipleSubst; + lookup ChainedAlternateSubst; lookup ChainedLigatureSubst; } test; diff --git a/Lib/fontTools/feaLib/testdata/GSUB_6.ttx b/Lib/fontTools/feaLib/testdata/GSUB_6.ttx index e796a55cf..79b7c3e4d 100644 --- a/Lib/fontTools/feaLib/testdata/GSUB_6.ttx +++ b/Lib/fontTools/feaLib/testdata/GSUB_6.ttx @@ -22,15 +22,16 @@ - + + - + @@ -153,6 +154,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -167,7 +202,7 @@ - + @@ -192,7 +227,7 @@ - +