From 2de6fc7744ce000f344ff4ecbb528b6d0c0dbd25 Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Mon, 11 Jan 2016 16:00:52 +0100 Subject: [PATCH] [feaLib] Imlement special support for `feature aalt` --- Lib/fontTools/feaLib/ast.py | 10 ++ Lib/fontTools/feaLib/builder.py | 81 +++++++++- Lib/fontTools/feaLib/builder_test.py | 16 +- Lib/fontTools/feaLib/parser.py | 9 ++ Lib/fontTools/feaLib/parser_test.py | 6 + Lib/fontTools/feaLib/testdata/spec8a.fea | 21 +++ Lib/fontTools/feaLib/testdata/spec8a.ttx | 194 +++++++++++++++++++++++ 7 files changed, 333 insertions(+), 4 deletions(-) create mode 100644 Lib/fontTools/feaLib/testdata/spec8a.fea create mode 100644 Lib/fontTools/feaLib/testdata/spec8a.ttx diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 511055dd2..956a5932b 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -263,6 +263,16 @@ class CursivePosStatement(Statement): self.location, self.glyphclass, self.entryAnchor, self.exitAnchor) +class FeatureReferenceStatement(Statement): + """Example: feature salt;""" + def __init__(self, location, featureName): + Statement.__init__(self, location) + self.location, self.featureName = (location, featureName) + + def build(self, builder): + builder.add_feature_reference(self.location, self.featureName) + + class LanguageStatement(Statement): def __init__(self, location, language, include_default, required): Statement.__init__(self, location) diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index a70238a4e..f0714e904 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -28,11 +28,15 @@ class Builder(object): self.cur_feature_name_ = None self.lookups_ = [] self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] + self.parseTree = None + self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' + # for feature 'aalt' + self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' + self.aalt_location_ = None + # for table 'GDEF' self.attachPoints_ = {} # "a" --> {3, 7} self.ligatureCaretByIndex_ = {} # "f_f_i" --> {3, 7} self.ligatureCaretByPos_ = {} # "f_f_i" --> {300, 600} - self.parseTree = None - self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' self.glyphClassDefs_ = {} # "fi" --> (2, (file, line, column)) self.markAttach_ = {} # "acute" --> (4, (file, line, column)) self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4 @@ -41,6 +45,7 @@ class Builder(object): def build(self): self.parseTree = Parser(self.featurefile_path).parse() self.parseTree.build(self) + self.build_feature_aalt_() for tag in ('GPOS', 'GSUB'): table = self.makeTable(tag) if (table.ScriptList.ScriptCount > 0 or @@ -93,6 +98,50 @@ class Builder(object): self.cur_feature_name_) return self.cur_lookup_ + def build_feature_aalt_(self): + if not self.aalt_features_: + return + alternates = {} # glyph --> {glyph.alt1, glyph.alt2, ...} + for location, name in self.aalt_features_ + [(None, "aalt")]: + feature = [(script, lang, feature, lookups) + for (script, lang, feature), lookups + in self.features_.items() + if feature == name] + if not feature: + raise FeatureLibError("Feature %s has not been defined" % name, + location) + for script, lang, feature, lookups in feature: + for lookup in lookups: + for glyph, alts in lookup.getAlternateGlyphs().items(): + alternates.setdefault(glyph, set()).update(alts) + single = {glyph: list(repl)[0] for glyph, repl in alternates.items() + if len(repl) == 1} + multi = {glyph: sorted(repl, key=self.font.getGlyphID) + for glyph, repl in alternates.items() + if len(repl) > 1} + if not single and not multi: + return + aalt_lookups = [] + for (script, lang, feature), lookups in self.features_.items(): + if feature == "aalt": + aalt_lookups.extend(lookups) + self.features_ = {(script, lang, feature): lookups + for (script, lang, feature), lookups + in self.features_.items() + if feature != "aalt"} + old_lookups = self.lookups_ + self.lookups_ = [] + self.start_feature(self.aalt_location_, "aalt") + if single: + self.add_single_subst( + self.aalt_location_, prefix=None, suffix=None, mapping=single) + for glyph, repl in multi.items(): + self.add_multiple_subst( + self.aalt_location_, prefix=None, glyph=glyph, suffix=None, + replacements=repl) + self.end_feature() + self.lookups_.extend(old_lookups) + def buildGDEF(self): gdef = otTables.GDEF() gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_() @@ -331,6 +380,8 @@ class Builder(object): self.language_systems = self.get_default_language_systems_() self.cur_lookup_ = None self.cur_feature_name_ = name + if name == "aalt": + self.aalt_location_ = location def end_feature(self): assert self.cur_feature_name_ is not None @@ -482,6 +533,13 @@ class Builder(object): location) lookup.alternates[glyph] = replacement + def add_feature_reference(self, location, featureName): + if self.cur_feature_name_ != "aalt": + raise FeatureLibError( + 'Feature references are only allowed inside "feature aalt"', + location) + self.aalt_features_.append((location, featureName)) + def add_ligature_subst(self, location, prefix, glyphs, suffix, replacement): if prefix or suffix: @@ -732,6 +790,10 @@ class LookupBuilder(object): """Infers glyph glasses for the GDEF table, such as {"cedilla":3}.""" return {} + def getAlternateGlyphs(self): + """Helper for building 'aalt' features.""" + return {} + def buildCoverage_(self, glyphs, tableClass=otTables.Coverage): coverage = tableClass() coverage.glyphs = sorted(glyphs, key=self.font.getGlyphID) @@ -822,6 +884,9 @@ class AlternateSubstBuilder(LookupBuilder): subtable.alternates = self.alternates return self.buildLookup_([subtable]) + def getAlternateGlyphs(self): + return self.alternates + class ChainContextPosBuilder(LookupBuilder): def __init__(self, font, location): @@ -882,6 +947,15 @@ class ChainContextSubstBuilder(LookupBuilder): st.SubstLookupRecord.append(rec) return self.buildLookup_(subtables) + def getAlternateGlyphs(self): + result = {} + for (_prefix, _input, _suffix, lookups) in self.substitutions: + for lookup in lookups: + alts = lookup.getAlternateGlyphs() + for glyph, replacements in alts.items(): + result.setdefault(glyph, set()).update(replacements) + return result + class LigatureSubstBuilder(LookupBuilder): def __init__(self, font, location): @@ -1189,6 +1263,9 @@ class SingleSubstBuilder(LookupBuilder): subtable.mapping = self.mapping return self.buildLookup_([subtable]) + def getAlternateGlyphs(self): + return {glyph: set([repl]) for glyph, repl in self.mapping.items()} + class ClassPairPosSubtableBuilder(object): def __init__(self, builder, valueFormat1, valueFormat2): diff --git a/Lib/fontTools/feaLib/builder_test.py b/Lib/fontTools/feaLib/builder_test.py index 03de9498f..bfa6f2238 100644 --- a/Lib/fontTools/feaLib/builder_test.py +++ b/Lib/fontTools/feaLib/builder_test.py @@ -26,7 +26,8 @@ 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 " "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 " - "d.alt e.begin n.end s.end " + "a.alt1 a.alt2 a.alt3 b.alt c.mid d.alt d.mid e.begin e.mid " + "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 " "ydieresis yacute " "grave acute dieresis macron circumflex cedilla umlaut ogonek caron " @@ -167,11 +168,22 @@ class BuilderTest(unittest.TestCase): def test_spec(self): for name in ("4h1 5d1 5d2 5fi1 5fi2 5fi3 5fi4 5h1 " - "6d2 6e 6f 6h_ii 9b").split(): + "6d2 6e 6f 6h_ii 8a 9b").split(): font = makeTTFont() addOpenTypeFeatures(self.getpath("spec%s.fea" % name), font) self.expect_ttx(font, self.getpath("spec%s.ttx" % name)) + def test_feature_outside_aalt(self): + self.assertRaisesRegex( + FeatureLibError, + 'Feature references are only allowed inside "feature aalt"', + self.build, "feature test { feature test; } test;") + + def test_feature_undefinedReference(self): + self.assertRaisesRegex( + FeatureLibError, 'Feature none has not been defined', + self.build, "feature aalt { feature none; } aalt;") + def test_GlyphClassDef_conflictingClasses(self): self.assertRaisesRegex( FeatureLibError, "Glyph X was assigned to a different class", diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index cc704fcf8..b4dde3283 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -765,6 +765,13 @@ class Parser(object): self.parse_block_(block, vertical) return block + def parse_feature_reference_(self): + assert self.cur_token_ == "feature", self.cur_token_ + location = self.cur_token_location_ + featureName = self.expect_tag_() + self.expect_symbol_(";") + return ast.FeatureReferenceStatement(location, featureName) + def parse_block_(self, block, vertical): self.expect_symbol_("{") for symtab in self.symbol_tables_: @@ -779,6 +786,8 @@ class Parser(object): statements.append(self.parse_anchordef_()) elif self.is_cur_keyword_({"enum", "enumerate"}): statements.append(self.parse_enumerate_(vertical=vertical)) + elif self.is_cur_keyword_("feature"): + statements.append(self.parse_feature_reference_()) elif self.is_cur_keyword_("ignore"): statements.append(self.parse_ignore_()) elif self.is_cur_keyword_("language"): diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index 7cce5a7be..41b2476cb 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -132,6 +132,12 @@ class ParserTest(unittest.TestCase): self.assertEqual(liga.name, "liga") self.assertTrue(liga.use_extension) + def test_feature_reference(self): + doc = self.parse("feature aalt { feature salt; } aalt;") + ref = doc.statements[0].statements[0] + self.assertIsInstance(ref, ast.FeatureReferenceStatement) + self.assertEqual(ref.featureName, "salt") + def test_glyphclass(self): [gc] = self.parse("@dash = [endash emdash figuredash];").statements self.assertEqual(gc.name, "dash") diff --git a/Lib/fontTools/feaLib/testdata/spec8a.fea b/Lib/fontTools/feaLib/testdata/spec8a.fea new file mode 100644 index 000000000..42b05e504 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/spec8a.fea @@ -0,0 +1,21 @@ +languagesystem DFLT dflt; +languagesystem latn dflt; +languagesystem latn TRK; +languagesystem cyrl dflt; + +feature aalt { + feature salt; + feature smcp; + substitute d by d.alt; +} aalt; + +feature smcp { + sub [a-c] by [A.sc-C.sc]; + sub f i by f_i; # not considered for aalt +} smcp; + +feature salt { + sub a from [a.alt1 a.alt2 a.alt3]; + sub e [c d e]' f by [c.mid d.mid e.mid]; + sub b by b.alt; +} salt; diff --git a/Lib/fontTools/feaLib/testdata/spec8a.ttx b/Lib/fontTools/feaLib/testdata/spec8a.ttx new file mode 100644 index 000000000..c417599e6 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/spec8a.ttx @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +