[feaLib] Implement GSUB LookupType 8: Reverse chaining single substitutions

This commit is contained in:
Sascha Brawer 2015-12-03 13:05:42 +01:00
parent 9f0aa03aec
commit cab0067c7e
9 changed files with 432 additions and 34 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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_()

View File

@ -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]

View File

@ -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;

142
Lib/fontTools/feaLib/testdata/GSUB_8.ttx vendored Normal file
View File

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="test"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<Lookup index="0">
<!-- LookupType=8 -->
<LookupFlag value="0"/>
<!-- SubTableCount=3 -->
<ReverseChainSingleSubst index="0" Format="1">
<Coverage>
<Glyph value="q"/>
</Coverage>
<!-- BacktrackGlyphCount=3 -->
<BacktrackCoverage index="0">
<Glyph value="C"/>
<Glyph value="c"/>
</BacktrackCoverage>
<BacktrackCoverage index="1">
<Glyph value="B"/>
<Glyph value="b"/>
</BacktrackCoverage>
<BacktrackCoverage index="2">
<Glyph value="A"/>
<Glyph value="a"/>
</BacktrackCoverage>
<!-- LookAheadGlyphCount=3 -->
<LookAheadCoverage index="0">
<Glyph value="D"/>
<Glyph value="d"/>
</LookAheadCoverage>
<LookAheadCoverage index="1">
<Glyph value="E"/>
<Glyph value="e"/>
</LookAheadCoverage>
<LookAheadCoverage index="2">
<Glyph value="F"/>
<Glyph value="f"/>
</LookAheadCoverage>
<!-- GlyphCount=1 -->
<Substitute index="0" value="Q"/>
</ReverseChainSingleSubst>
<ReverseChainSingleSubst index="1" Format="1">
<Coverage>
<Glyph value="s"/>
<Glyph value="t"/>
<Glyph value="u"/>
<Glyph value="v"/>
<Glyph value="w"/>
<Glyph value="x"/>
<Glyph value="y"/>
<Glyph value="z"/>
</Coverage>
<!-- BacktrackGlyphCount=3 -->
<BacktrackCoverage index="0">
<Glyph value="C"/>
<Glyph value="c"/>
</BacktrackCoverage>
<BacktrackCoverage index="1">
<Glyph value="B"/>
<Glyph value="b"/>
</BacktrackCoverage>
<BacktrackCoverage index="2">
<Glyph value="A"/>
<Glyph value="a"/>
</BacktrackCoverage>
<!-- LookAheadGlyphCount=3 -->
<LookAheadCoverage index="0">
<Glyph value="D"/>
<Glyph value="d"/>
</LookAheadCoverage>
<LookAheadCoverage index="1">
<Glyph value="E"/>
<Glyph value="e"/>
</LookAheadCoverage>
<LookAheadCoverage index="2">
<Glyph value="F"/>
<Glyph value="f"/>
</LookAheadCoverage>
<!-- GlyphCount=8 -->
<Substitute index="0" value="S"/>
<Substitute index="1" value="T"/>
<Substitute index="2" value="U"/>
<Substitute index="3" value="V"/>
<Substitute index="4" value="W"/>
<Substitute index="5" value="X"/>
<Substitute index="6" value="Y"/>
<Substitute index="7" value="Z"/>
</ReverseChainSingleSubst>
<ReverseChainSingleSubst index="2" Format="1">
<Coverage>
<Glyph value="p"/>
</Coverage>
<!-- BacktrackGlyphCount=0 -->
<!-- LookAheadGlyphCount=0 -->
<!-- GlyphCount=1 -->
<Substitute index="0" value="P"/>
</ReverseChainSingleSubst>
</Lookup>
</LookupList>
</GSUB>
<GPOS>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=0 -->
</ScriptList>
<FeatureList>
<!-- FeatureCount=0 -->
</FeatureList>
<LookupList>
<!-- LookupCount=0 -->
</LookupList>
</GPOS>
</ttFont>

View File

@ -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;

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="test"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<Lookup index="0">
<!-- LookupType=8 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ReverseChainSingleSubst index="0" Format="1">
<Coverage>
<Glyph value="d"/>
</Coverage>
<!-- BacktrackGlyphCount=1 -->
<BacktrackCoverage index="0">
<Glyph value="a"/>
<Glyph value="e"/>
<Glyph value="n"/>
</BacktrackCoverage>
<!-- LookAheadGlyphCount=0 -->
<!-- GlyphCount=1 -->
<Substitute index="0" value="d.alt"/>
</ReverseChainSingleSubst>
</Lookup>
</LookupList>
</GSUB>
<GPOS>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=0 -->
</ScriptList>
<FeatureList>
<!-- FeatureCount=0 -->
</FeatureList>
<LookupList>
<!-- LookupCount=0 -->
</LookupList>
</GPOS>
</ttFont>