[feaLib] Support compact syntax for chaining to ligature substitutions
https://github.com/behdad/fonttools/issues/445
This commit is contained in:
parent
b19c6b3dec
commit
931bbc50c1
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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))
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
5
Lib/fontTools/feaLib/testdata/GSUB_6.fea
vendored
5
Lib/fontTools/feaLib/testdata/GSUB_6.fea
vendored
@ -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;
|
||||||
|
49
Lib/fontTools/feaLib/testdata/GSUB_6.ttx
vendored
49
Lib/fontTools/feaLib/testdata/GSUB_6.ttx
vendored
@ -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>
|
||||||
|
|
||||||
|
9
Lib/fontTools/feaLib/testdata/spec5fi4.fea
vendored
Normal file
9
Lib/fontTools/feaLib/testdata/spec5fi4.fea
vendored
Normal 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;
|
73
Lib/fontTools/feaLib/testdata/spec5fi4.ttx
vendored
Normal file
73
Lib/fontTools/feaLib/testdata/spec5fi4.ttx
vendored
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user