diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index 40f20256e..7ff5c53f4 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -138,6 +138,17 @@ class MultipleSubstitution(Statement):
self.glyph, self.replacement)
+class ReverseChainingSingleSubstitution(Statement):
+ def __init__(self, location, old_prefix, old_suffix, mapping):
+ Statement.__init__(self, location)
+ self.old_prefix, self.old_suffix = old_prefix, old_suffix
+ self.mapping = mapping
+
+ def build(self, builder):
+ builder.add_reverse_chaining_single_substitution(
+ self.location, self.old_prefix, self.old_suffix, self.mapping)
+
+
class SingleSubstitution(Statement):
def __init__(self, location, mapping):
Statement.__init__(self, location)
@@ -171,8 +182,7 @@ class SubstitutionRule(Statement):
def build(self, builder):
builder.add_substitution(
- self.location,
- self.old_prefix, self.old, self.old_suffix,
+ self.location, self.old_prefix, self.old, self.old_suffix,
self.new, self.lookups)
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index ce4e04e77..73e33560d 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -247,18 +247,26 @@ class Builder(object):
self.set_language(location, "dflt",
include_default=True, required=False)
- def add_substitution(self, location, old_prefix, old, old_suffix, new,
- lookups):
- assert len(new) == 0, new
+ def find_lookup_builders_(self, lookups):
+ """Helper for building chain contextual substitutions
+
+ Given a list of lookup names, finds the LookupBuilder for each name.
+ 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))
else:
lookup_builders.append(None)
+ return lookup_builders
+
+ def add_substitution(self, location, old_prefix, old, old_suffix, new,
+ lookups):
+ assert len(new) == 0, new
lookup = self.get_lookup_(location, ChainContextSubstBuilder)
lookup.substitutions.append((old_prefix, old, old_suffix,
- lookup_builders))
+ self.find_lookup_builders_(lookups)))
def add_alternate_substitution(self, location, glyph, from_class):
lookup = self.get_lookup_(location, AlternateSubstBuilder)
@@ -280,6 +288,11 @@ class Builder(object):
location)
lookup.mapping[glyph] = replacements
+ def add_reverse_chaining_single_substitution(self, location, old_prefix,
+ old_suffix, mapping):
+ lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
+ lookup.substitutions.append((old_prefix, old_suffix, mapping))
+
def add_single_substitution(self, location, mapping):
lookup = self.get_lookup_(location, SingleSubstBuilder)
for (from_glyph, to_glyph) in mapping.items():
@@ -304,6 +317,33 @@ class LookupBuilder(object):
self.table == other.table and
self.lookup_flag == other.lookup_flag)
+ @staticmethod
+ def setBacktrackCoverage_(prefix, subtable):
+ subtable.BacktrackGlyphCount = len(prefix)
+ subtable.BacktrackCoverage = []
+ for p in reversed(prefix):
+ coverage = otTables.BacktrackCoverage()
+ coverage.glyphs = sorted(list(p))
+ subtable.BacktrackCoverage.append(coverage)
+
+ @staticmethod
+ def setLookAheadCoverage_(suffix, subtable):
+ subtable.LookAheadGlyphCount = len(suffix)
+ subtable.LookAheadCoverage = []
+ for s in suffix:
+ coverage = otTables.LookAheadCoverage()
+ coverage.glyphs = sorted(list(s))
+ subtable.LookAheadCoverage.append(coverage)
+
+ @staticmethod
+ def setInputCoverage_(glyphs, subtable):
+ subtable.InputGlyphCount = len(glyphs)
+ subtable.InputCoverage = []
+ for g in glyphs:
+ coverage = otTables.InputCoverage()
+ coverage.glyphs = sorted(list(g))
+ subtable.InputCoverage.append(coverage)
+
class AlternateSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
@@ -343,27 +383,9 @@ class ChainContextSubstBuilder(LookupBuilder):
st = otTables.ChainContextSubst()
lookup.SubTable.append(st)
st.Format = 3
-
- st.BacktrackGlyphCount = len(prefix)
- st.BacktrackCoverage = []
- for p in reversed(prefix):
- coverage = otTables.BacktrackCoverage()
- coverage.glyphs = sorted(list(p))
- st.BacktrackCoverage.append(coverage)
-
- st.InputGlyphCount = len(input)
- st.InputCoverage = []
- for i in input:
- coverage = otTables.InputCoverage()
- coverage.glyphs = sorted(list(i))
- st.InputCoverage.append(coverage)
-
- st.LookAheadGlyphCount = len(suffix)
- st.LookAheadCoverage = []
- for s in suffix:
- coverage = otTables.LookAheadCoverage()
- coverage.glyphs = sorted(list(s))
- st.LookAheadCoverage.append(coverage)
+ self.setBacktrackCoverage_(prefix, st)
+ self.setLookAheadCoverage_(suffix, st)
+ self.setInputCoverage_(input, st)
st.SubstCount = len([l for l in lookups if l is not None])
st.SubstLookupRecord = []
@@ -442,6 +464,36 @@ class MultipleSubstBuilder(LookupBuilder):
return lookup
+class ReverseChainSingleSubstBuilder(LookupBuilder):
+ def __init__(self, location, lookup_flag):
+ LookupBuilder.__init__(self, location, 'GSUB', 8, lookup_flag)
+ self.substitutions = [] # (prefix, suffix, mapping)
+
+ def equals(self, other):
+ return (LookupBuilder.equals(self, other) and
+ self.substitutions == other.substitutions)
+
+ def build(self):
+ lookup = otTables.Lookup()
+ lookup.SubTable = []
+ for prefix, suffix, mapping in self.substitutions:
+ st = otTables.ReverseChainSingleSubst()
+ st.Format = 1
+ lookup.SubTable.append(st)
+ self.setBacktrackCoverage_(prefix, st)
+ self.setLookAheadCoverage_(suffix, st)
+ coverage = sorted(list(mapping.keys()))
+ st.Coverage = otTables.Coverage()
+ st.Coverage.glyphs = coverage
+ st.GlyphCount = len(coverage)
+ st.Substitute = [mapping[g] for g in coverage]
+
+ lookup.LookupFlag = self.lookup_flag
+ lookup.LookupType = self.lookup_type
+ lookup.SubTableCount = len(lookup.SubTable)
+ return lookup
+
+
class SingleSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 1, lookup_flag)
diff --git a/Lib/fontTools/feaLib/builder_test.py b/Lib/fontTools/feaLib/builder_test.py
index f4ff12c07..adf85e309 100644
--- a/Lib/fontTools/feaLib/builder_test.py
+++ b/Lib/fontTools/feaLib/builder_test.py
@@ -104,6 +104,11 @@ class BuilderTest(unittest.TestCase):
" sub f_f_i by f f i;"
"} test;")
+ def test_reverseChainingSingleSubst(self):
+ font = TTFont()
+ addOpenTypeFeatures(self.getpath("GSUB_8.fea"), font)
+ self.expect_ttx(font, self.getpath("GSUB_8.ttx"))
+
def test_singleSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
@@ -138,6 +143,12 @@ class BuilderTest(unittest.TestCase):
addOpenTypeFeatures(self.getpath("spec5fi1.fea"), font)
self.expect_ttx(font, self.getpath("spec5fi1.ttx"))
+ def test_spec5h1(self):
+ # OpenType Feature File specification, section 5.h, example 1.
+ font = TTFont()
+ addOpenTypeFeatures(self.getpath("spec5h1.fea"), font)
+ self.expect_ttx(font, self.getpath("spec5h1.ttx"))
+
def test_languagesystem(self):
builder = Builder(None, TTFont())
builder.add_language_system(None, 'latn', 'FRA')
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index 727d2ebde..9c2be7ff6 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -207,8 +207,9 @@ class Parser(object):
return ast.ScriptStatement(location, script)
def parse_substitute_(self):
- assert self.cur_token_ in {"substitute", "sub"}
+ assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"}
location = self.cur_token_location_
+ reverse = self.cur_token_ in {"reversesub", "rsub"}
old_prefix, old, lookups, old_suffix = self.parse_glyph_pattern_()
new = []
@@ -230,6 +231,10 @@ class Parser(object):
# GSUB lookup type 3: Alternate substitution.
# Format: "substitute a from [a.1 a.2 a.3];"
if keyword == "from":
+ if reverse:
+ raise FeatureLibError(
+ 'Reverse chaining substitutions do not support "from"',
+ location)
if len(old) != 1 or len(old[0]) != 1:
raise FeatureLibError(
'Expected a single glyph before "from"',
@@ -246,7 +251,7 @@ 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 (len(old_prefix) == 0 and len(old_suffix) == 0 and
+ 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):
glyphs, replacements = sorted(list(old[0])), sorted(list(new[0]))
if len(replacements) == 1:
@@ -261,7 +266,7 @@ class Parser(object):
# GSUB lookup type 2: Multiple substitution.
# Format: "substitute f_f_i by f f i;"
- if (len(old_prefix) == 0 and len(old_suffix) == 0 and
+ if (not reverse and len(old_prefix) == 0 and len(old_suffix) == 0 and
len(old) == 1 and len(old[0]) == 1 and
len(new) > 1 and max([len(n) for n in new]) == 1 and
num_lookups == 0):
@@ -270,14 +275,43 @@ class Parser(object):
# GSUB lookup type 4: Ligature substitution.
# Format: "substitute f f i by f_f_i;"
- if (len(old_prefix) == 0 and len(old_suffix) == 0 and
+ 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
num_lookups == 0):
return ast.LigatureSubstitution(location, old, list(new[0])[0])
+ # GSUB lookup type 8: Reverse chaining substitution.
+ if reverse:
+ if len(old) != 1:
+ raise FeatureLibError(
+ "In reverse chaining single substitutions, "
+ "only a single glyph or glyph class can be replaced",
+ location)
+ if len(new) != 1:
+ raise FeatureLibError(
+ 'In reverse chaining single substitutions, '
+ 'the replacement (after "by") must be a single glyph '
+ 'or glyph class', location)
+ if num_lookups != 0:
+ raise FeatureLibError(
+ "Reverse chaining substitutions cannot call named lookups",
+ location)
+ glyphs, replacements = sorted(list(old[0])), sorted(list(new[0]))
+ if len(replacements) == 1:
+ replacements = replacements * len(glyphs)
+ if len(glyphs) != len(replacements):
+ raise FeatureLibError(
+ 'Expected a glyph class with %d elements after "by", '
+ 'but found a glyph class with %d elements' %
+ (len(glyphs), len(replacements)), location)
+ return ast.ReverseChainingSingleSubstitution(
+ location, old_prefix, old_suffix,
+ dict(zip(glyphs, replacements)))
+
rule = ast.SubstitutionRule(location, old, new)
rule.old_prefix, rule.old_suffix = old_prefix, old_suffix
rule.lookups = lookups
+ rule.reverse = reverse
return rule
def parse_subtable_(self):
@@ -370,8 +404,8 @@ class Parser(object):
statements.append(self.parse_lookup_(vertical))
elif self.is_cur_keyword_("script"):
statements.append(self.parse_script_())
- elif (self.is_cur_keyword_("substitute") or
- self.is_cur_keyword_("sub")):
+ elif (self.is_cur_keyword_({"sub", "substitute",
+ "rsub", "reversesub"})):
statements.append(self.parse_substitute_())
elif self.is_cur_keyword_("subtable"):
statements.append(self.parse_subtable_())
@@ -393,7 +427,12 @@ class Parser(object):
self.expect_symbol_(";")
def is_cur_keyword_(self, k):
- return (self.cur_token_type_ is Lexer.NAME) and (self.cur_token_ == k)
+ if self.cur_token_type_ is Lexer.NAME:
+ if isinstance(k, type("")): # basestring is gone in Python3
+ return self.cur_token_ == k
+ else:
+ return self.cur_token_ in k
+ return False
def expect_tag_(self):
self.advance_lexer_()
diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py
index b9dfbc0d8..05dc7d87f 100644
--- a/Lib/fontTools/feaLib/parser_test.py
+++ b/Lib/fontTools/feaLib/parser_test.py
@@ -256,6 +256,64 @@ class ParserTest(unittest.TestCase):
FeatureLibError, 'Unknown lookup "Huh"',
self.parse, "feature liga {lookup Huh;} liga;")
+ def test_rsub_format_a(self):
+ doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;")
+ rsub = doc.statements[0].statements[0]
+ self.assertEqual(type(rsub), ast.ReverseChainingSingleSubstitution)
+ self.assertEqual(rsub.old_prefix, [{"a"}, {"b", "B"}])
+ self.assertEqual(rsub.mapping, {"c": "C"})
+ self.assertEqual(rsub.old_suffix, [{"d"}, {"e", "E"}])
+
+ def test_rsub_format_b(self):
+ doc = self.parse(
+ "feature smcp {"
+ " reversesub A B [one.fitted one.oldstyle]' C [d D] by one;"
+ "} smcp;")
+ rsub = doc.statements[0].statements[0]
+ self.assertEqual(type(rsub), ast.ReverseChainingSingleSubstitution)
+ self.assertEqual(rsub.old_prefix, [{"A"}, {"B"}])
+ self.assertEqual(rsub.old_suffix, [{"C"}, {"d", "D"}])
+ self.assertEqual(rsub.mapping, {
+ "one.fitted": "one",
+ "one.oldstyle": "one"
+ })
+
+ def test_rsub_format_c(self):
+ doc = self.parse(
+ "feature test {"
+ " reversesub BACK TRACK [a-d]' LOOK AHEAD by [A.sc-D.sc];"
+ "} test;")
+ rsub = doc.statements[0].statements[0]
+ self.assertEqual(type(rsub), ast.ReverseChainingSingleSubstitution)
+ self.assertEqual(rsub.old_prefix, [{"BACK"}, {"TRACK"}])
+ self.assertEqual(rsub.old_suffix, [{"LOOK"}, {"AHEAD"}])
+ self.assertEqual(rsub.mapping, {
+ "a": "A.sc",
+ "b": "B.sc",
+ "c": "C.sc",
+ "d": "D.sc"
+ })
+
+ def test_rsub_from(self):
+ self.assertRaisesRegex(
+ FeatureLibError,
+ 'Reverse chaining substitutions do not support "from"',
+ self.parse, "feature test {rsub a from [a.1 a.2 a.3];} test;")
+
+ def test_rsub_nonsingle(self):
+ self.assertRaisesRegex(
+ FeatureLibError,
+ "In reverse chaining single substitutions, only a single glyph "
+ "or glyph class can be replaced",
+ self.parse, "feature test {rsub c d by c_d;} test;")
+
+ def test_rsub_multiple_replacement_glyphs(self):
+ self.assertRaisesRegex(
+ FeatureLibError,
+ 'In reverse chaining single substitutions, the replacement '
+ '\(after "by"\) must be a single glyph or glyph class',
+ self.parse, "feature test {rsub f_i by f i;} test;")
+
def test_script(self):
doc = self.parse("feature test {script cyrl;} test;")
s = doc.statements[0].statements[0]
diff --git a/Lib/fontTools/feaLib/testdata/GSUB_8.fea b/Lib/fontTools/feaLib/testdata/GSUB_8.fea
new file mode 100644
index 000000000..a56e09302
--- /dev/null
+++ b/Lib/fontTools/feaLib/testdata/GSUB_8.fea
@@ -0,0 +1,11 @@
+languagesystem DFLT dflt;
+
+feature test {
+ reversesub [a A] [b B] [c C] q' [d D] [e E] [f F] by Q;
+ reversesub [a A] [b B] [c C] [s-z]' [d D] [e E] [f F] by [S-Z];
+
+ # Having no context for a reverse chaining substitution rule
+ # is a little degenerate (we define a chain without linking it
+ # to anything else), but makeotf accepts this.
+ reversesub p by P;
+} test;
diff --git a/Lib/fontTools/feaLib/testdata/GSUB_8.ttx b/Lib/fontTools/feaLib/testdata/GSUB_8.ttx
new file mode 100644
index 000000000..b438a5148
--- /dev/null
+++ b/Lib/fontTools/feaLib/testdata/GSUB_8.ttx
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Lib/fontTools/feaLib/testdata/spec5h1.fea b/Lib/fontTools/feaLib/testdata/spec5h1.fea
new file mode 100644
index 000000000..6a28cbebe
--- /dev/null
+++ b/Lib/fontTools/feaLib/testdata/spec5h1.fea
@@ -0,0 +1,8 @@
+# OpenType Feature File specification, section 5.h, example 1.
+# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
+
+languagesystem DFLT dflt;
+
+feature test {
+ reversesub [a e n] d' by d.alt;
+} test;
diff --git a/Lib/fontTools/feaLib/testdata/spec5h1.ttx b/Lib/fontTools/feaLib/testdata/spec5h1.ttx
new file mode 100644
index 000000000..13febe1a6
--- /dev/null
+++ b/Lib/fontTools/feaLib/testdata/spec5h1.ttx
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+