[feaLib] Support compact syntax for chaining to single substitutions

Resolves https://github.com/behdad/fonttools/issues/445 for single
substitutions. The compact forms for chaining to other GSUB types
are not yet supported; these will get fixed in follow-up changes.
This commit is contained in:
Sascha Brawer 2016-01-06 16:15:26 +01:00
parent adc2862bbb
commit a543b2e3b9
9 changed files with 316 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="latn"/>
<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="1"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=2 -->
<Lookup index="0">
<!-- LookupType=1 -->
<LookupFlag value="7"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0">
<Substitution in="d" out="d.alt"/>
</SingleSubst>
</Lookup>
<Lookup index="1">
<!-- LookupType=6 -->
<LookupFlag value="7"/>
<!-- SubTableCount=1 -->
<ChainContextSubst index="0" Format="3">
<!-- BacktrackGlyphCount=1 -->
<BacktrackCoverage index="0">
<Glyph value="a"/>
<Glyph value="e"/>
<Glyph value="n"/>
</BacktrackCoverage>
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="d"/>
</InputCoverage>
<!-- LookAheadGlyphCount=0 -->
<!-- SubstCount=1 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
</SubstLookupRecord>
</ChainContextSubst>
</Lookup>
</LookupList>
</GSUB>
</ttFont>

View File

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

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="latn"/>
<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=2 -->
<Lookup index="0">
<!-- LookupType=1 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0">
<Substitution in="A.sc" out="a"/>
<Substitution in="B.sc" out="b"/>
<Substitution in="C.sc" out="c"/>
<Substitution in="D.sc" out="d"/>
<Substitution in="E.sc" out="e"/>
<Substitution in="F.sc" out="f"/>
<Substitution in="G.sc" out="g"/>
<Substitution in="H.sc" out="h"/>
<Substitution in="I.sc" out="i"/>
<Substitution in="J.sc" out="j"/>
<Substitution in="K.sc" out="k"/>
<Substitution in="L.sc" out="l"/>
<Substitution in="M.sc" out="m"/>
<Substitution in="N.sc" out="n"/>
<Substitution in="O.sc" out="o"/>
<Substitution in="P.sc" out="p"/>
<Substitution in="Q.sc" out="q"/>
<Substitution in="R.sc" out="r"/>
<Substitution in="S.sc" out="s"/>
<Substitution in="T.sc" out="t"/>
<Substitution in="U.sc" out="u"/>
<Substitution in="V.sc" out="v"/>
<Substitution in="W.sc" out="w"/>
<Substitution in="X.sc" out="x"/>
<Substitution in="Y.sc" out="y"/>
<Substitution in="Z.sc" out="z"/>
</SingleSubst>
</Lookup>
<Lookup index="1">
<!-- LookupType=6 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ChainContextSubst index="0" Format="3">
<!-- BacktrackGlyphCount=1 -->
<BacktrackCoverage index="0">
<Glyph value="A"/>
<Glyph value="B"/>
<Glyph value="C"/>
<Glyph value="D"/>
<Glyph value="E"/>
<Glyph value="F"/>
<Glyph value="G"/>
<Glyph value="H"/>
<Glyph value="I"/>
<Glyph value="J"/>
<Glyph value="K"/>
<Glyph value="L"/>
<Glyph value="M"/>
<Glyph value="N"/>
<Glyph value="O"/>
<Glyph value="P"/>
<Glyph value="Q"/>
<Glyph value="R"/>
<Glyph value="S"/>
<Glyph value="T"/>
<Glyph value="U"/>
<Glyph value="V"/>
<Glyph value="W"/>
<Glyph value="X"/>
<Glyph value="Y"/>
<Glyph value="Z"/>
</BacktrackCoverage>
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="A.sc"/>
<Glyph value="B.sc"/>
<Glyph value="C.sc"/>
<Glyph value="D.sc"/>
<Glyph value="E.sc"/>
<Glyph value="F.sc"/>
<Glyph value="G.sc"/>
<Glyph value="H.sc"/>
<Glyph value="I.sc"/>
<Glyph value="J.sc"/>
<Glyph value="K.sc"/>
<Glyph value="L.sc"/>
<Glyph value="M.sc"/>
<Glyph value="N.sc"/>
<Glyph value="O.sc"/>
<Glyph value="P.sc"/>
<Glyph value="Q.sc"/>
<Glyph value="R.sc"/>
<Glyph value="S.sc"/>
<Glyph value="T.sc"/>
<Glyph value="U.sc"/>
<Glyph value="V.sc"/>
<Glyph value="W.sc"/>
<Glyph value="X.sc"/>
<Glyph value="Y.sc"/>
<Glyph value="Z.sc"/>
</InputCoverage>
<!-- LookAheadGlyphCount=0 -->
<!-- SubstCount=1 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
</SubstLookupRecord>
</ChainContextSubst>
</Lookup>
</LookupList>
</GSUB>
</ttFont>