[feaLib] Support compact syntax for chaining to ligature substitutions

https://github.com/behdad/fonttools/issues/445
This commit is contained in:
Sascha Brawer 2016-01-07 10:31:13 +01:00
parent b19c6b3dec
commit 931bbc50c1
9 changed files with 178 additions and 22 deletions

View File

@ -250,19 +250,17 @@ class IgnoreSubstitutionRule(Statement):
class LigatureSubstStatement(Statement): class LigatureSubstStatement(Statement):
def __init__(self, location, glyphs, replacement): def __init__(self, location, prefix, glyphs, suffix, replacement):
Statement.__init__(self, location) 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): def build(self, builder):
# OpenType feature file syntax, section 5.d, "Ligature substitution": prefix = [p.glyphSet() for p in self.prefix]
# "Since the OpenType specification does not allow ligature glyphs = [g.glyphSet() for g in self.glyphs]
# substitutions to be specified on target sequences that contain suffix = [s.glyphSet() for s in self.suffix]
# glyph classes, the implementation software will enumerate builder.add_ligature_subst(
# all specific glyph sequences if glyph classes are detected" self.location, prefix, glyphs, suffix, self.replacement)
g = [g.glyphSet() for g in self.glyphs]
for glyphs in sorted(itertools.product(*g)):
builder.add_ligature_subst(self.location, glyphs, self.replacement)
class LookupFlagStatement(Statement): class LookupFlagStatement(Statement):

View File

@ -4,6 +4,7 @@ from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.parser import Parser from fontTools.feaLib.parser import Parser
from fontTools.ttLib import getTableClass from fontTools.ttLib import getTableClass
from fontTools.ttLib.tables import otBase, otTables from fontTools.ttLib.tables import otBase, otTables
import itertools
import warnings import warnings
@ -410,9 +411,22 @@ class Builder(object):
location) location)
lookup.alternates[glyph] = from_class lookup.alternates[glyph] = from_class
def add_ligature_subst(self, location, glyphs, replacement): def add_ligature_subst(self, location,
lookup = self.get_lookup_(location, LigatureSubstBuilder) prefix, glyphs, suffix, replacement):
lookup.ligatures[glyphs] = 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, def add_multiple_subst(self, location,
prefix, glyph, suffix, replacements): prefix, glyph, suffix, replacements):

View File

@ -16,7 +16,7 @@ import unittest
def makeTTFont(): def makeTTFont():
glyphs = ( 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 one two three four five six seven eight nine "
"zero.oldstyle one.oldstyle two.oldstyle three.oldstyle " "zero.oldstyle one.oldstyle two.oldstyle three.oldstyle "
"four.oldstyle five.oldstyle six.oldstyle seven.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 " "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 " "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 " "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 " "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 " "ydieresis yacute "
"grave acute dieresis macron circumflex cedilla umlaut ogonek caron " "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)) self.expect_ttx(font, self.getpath("GSUB_%s.ttx" % name))
def test_spec(self): 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() font = makeTTFont()
addOpenTypeFeatures(self.getpath("spec%s.fea" % name), font) addOpenTypeFeatures(self.getpath("spec%s.fea" % name), font)
self.expect_ttx(font, self.getpath("spec%s.ttx" % name)) self.expect_ttx(font, self.getpath("spec%s.ttx" % name))

View File

@ -510,12 +510,13 @@ class Parser(object):
# GSUB lookup type 4: Ligature substitution. # GSUB lookup type 4: Ligature substitution.
# Format: "substitute f f i by f_f_i;" # 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(old) > 1 and len(new) == 1 and
len(new[0].glyphSet()) == 1 and len(new[0].glyphSet()) == 1 and
num_lookups == 0): num_lookups == 0):
return ast.LigatureSubstStatement( 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. # GSUB lookup type 8: Reverse chaining substitution.
if reverse: if reverse:

View File

@ -807,7 +807,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(sub.glyph, "f_f_i") self.assertEqual(sub.glyph, "f_f_i")
self.assertEqual(sub.replacement, ("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;") doc = self.parse("lookup L {sub [A-C] f_f_i' [X-Z] by f f i;} L;")
sub = doc.statements[0].statements[0] sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertIsInstance(sub, ast.MultipleSubstStatement)
@ -836,9 +836,20 @@ class ParserTest(unittest.TestCase):
def test_substitute_ligature(self): # GSUB LookupType 4 def test_substitute_ligature(self): # GSUB LookupType 4
doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;") doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;")
sub = doc.statements[0].statements[0] 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(glyphstr(sub.glyphs), "f f i")
self.assertEqual(sub.replacement, "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 def test_substitute_lookups(self): # GSUB LookupType 6
doc = Parser(self.getpath("spec5fi1.fea")).parse() doc = Parser(self.getpath("spec5fi1.fea")).parse()

View File

@ -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; substitute [A-C a-c] [D d] E c_t' V [W w] [X-Z x-z] by c t;
} ChainedMultipleSubst; } ChainedMultipleSubst;
lookup ChainedLigatureSubst {
substitute A [C c]' [T t]' Z by c_t;
} ChainedLigatureSubst;
feature test { feature test {
lookup ChainedSingleSubst; lookup ChainedSingleSubst;
lookup ChainedMultipleSubst; lookup ChainedMultipleSubst;
lookup ChainedLigatureSubst;
} test; } test;

View File

@ -22,14 +22,15 @@
<FeatureRecord index="0"> <FeatureRecord index="0">
<FeatureTag value="test"/> <FeatureTag value="test"/>
<Feature> <Feature>
<!-- LookupCount=2 --> <!-- LookupCount=3 -->
<LookupListIndex index="0" value="1"/> <LookupListIndex index="0" value="1"/>
<LookupListIndex index="1" value="4"/> <LookupListIndex index="1" value="4"/>
<LookupListIndex index="2" value="6"/>
</Feature> </Feature>
</FeatureRecord> </FeatureRecord>
</FeatureList> </FeatureList>
<LookupList> <LookupList>
<!-- LookupCount=5 --> <!-- LookupCount=7 -->
<Lookup index="0"> <Lookup index="0">
<!-- LookupType=1 --> <!-- LookupType=1 -->
<LookupFlag value="0"/> <LookupFlag value="0"/>
@ -151,6 +152,50 @@
</SubstLookupRecord> </SubstLookupRecord>
</ChainContextSubst> </ChainContextSubst>
</Lookup> </Lookup>
<Lookup index="5">
<!-- LookupType=4 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<LigatureSubst index="0" Format="1">
<LigatureSet glyph="C">
<Ligature components="T" glyph="c_t"/>
<Ligature components="t" glyph="c_t"/>
</LigatureSet>
<LigatureSet glyph="c">
<Ligature components="T" glyph="c_t"/>
<Ligature components="t" glyph="c_t"/>
</LigatureSet>
</LigatureSubst>
</Lookup>
<Lookup index="6">
<!-- LookupType=6 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ChainContextSubst index="0" Format="3">
<!-- BacktrackGlyphCount=1 -->
<BacktrackCoverage index="0">
<Glyph value="A"/>
</BacktrackCoverage>
<!-- InputGlyphCount=2 -->
<InputCoverage index="0">
<Glyph value="C"/>
<Glyph value="c"/>
</InputCoverage>
<InputCoverage index="1">
<Glyph value="T"/>
<Glyph value="t"/>
</InputCoverage>
<!-- LookAheadGlyphCount=1 -->
<LookAheadCoverage index="0">
<Glyph value="Z"/>
</LookAheadCoverage>
<!-- SubstCount=1 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="5"/>
</SubstLookupRecord>
</ChainContextSubst>
</Lookup>
</LookupList> </LookupList>
</GSUB> </GSUB>

View File

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

View File

@ -0,0 +1,73 @@
<?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=4 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<LigatureSubst index="0" Format="1">
<LigatureSet glyph="e">
<Ligature components="t" glyph="ampersand"/>
</LigatureSet>
<LigatureSet glyph="e.begin">
<Ligature components="t" glyph="ampersand"/>
</LigatureSet>
</LigatureSubst>
</Lookup>
<Lookup index="1">
<!-- LookupType=6 -->
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ChainContextSubst index="0" Format="3">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=2 -->
<InputCoverage index="0">
<Glyph value="e"/>
<Glyph value="e.begin"/>
</InputCoverage>
<InputCoverage index="1">
<Glyph value="t"/>
</InputCoverage>
<!-- LookAheadGlyphCount=1 -->
<LookAheadCoverage index="0">
<Glyph value="c"/>
</LookAheadCoverage>
<!-- SubstCount=1 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
</SubstLookupRecord>
</ChainContextSubst>
</Lookup>
</LookupList>
</GSUB>
</ttFont>