diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index a383e99a3..209219710 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -39,6 +39,7 @@ class Block(Statement): class FeatureFile(Block): def __init__(self): Block.__init__(self, location=None) + self.markClasses = {} # name --> ast.MarkClassDefinition class FeatureBlock(Block): @@ -72,6 +73,13 @@ class GlyphClassDefinition(Statement): self.glyphs = glyphs +class MarkClassDefinition(object): + def __init__(self, location, name): + self.location, self.name = location, name + self.anchors = {} # glyph --> ast.Anchor + self.glyphLocations = {} # glyph --> (filepath, line, column) + + class AlternateSubstitution(Statement): def __init__(self, location, glyph, from_class): Statement.__init__(self, location) diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index a8366b796..989a0490e 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -26,11 +26,12 @@ 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' def build(self): - parsetree = Parser(self.featurefile_path).parse() - parsetree.build(self) + self.parseTree = Parser(self.featurefile_path).parse() + self.parseTree.build(self) for tag in ('GPOS', 'GSUB'): table = self.makeTable(tag) if (table.ScriptList.ScriptCount > 0 or @@ -40,6 +41,11 @@ class Builder(object): fontTable.table = table elif tag in self.font: del self.font[tag] + gdef = self.makeGDEF() + if gdef: + self.font["GDEF"] = gdef + elif "GDEF" in self.font: + del self.font["GDEF"] def get_lookup_(self, location, builder_class): if (self.cur_lookup_ and @@ -64,6 +70,32 @@ class Builder(object): self.features_.setdefault(key, []).append(self.cur_lookup_) return self.cur_lookup_ + def makeGDEF(self): + gdef = otTables.GDEF() + gdef.Version = 1.0 + gdef.GlyphClassDef = otTables.GlyphClassDef() + gdef.GlyphClassDef.classDefs = {} + + glyphMarkClass = {} # glyph --> markClass + for markClass in self.parseTree.markClasses.values(): + for glyph in markClass.anchors.keys(): + if glyph in glyphMarkClass: + other = glyphMarkClass[glyph] + name1, name2 = sorted([markClass.name, other.name]) + raise FeatureLibError( + 'glyph %s cannot be both in markClass @%s and @%s' % + (glyph, name1, name2), markClass.location) + glyphMarkClass[glyph] = markClass + gdef.GlyphClassDef.classDefs[glyph] = 3 + gdef.AttachList = None + gdef.LigCaretList = None + gdef.MarkAttachClassDef = None + if len(gdef.GlyphClassDef.classDefs) == 0: + return None + result = getTableClass("GDEF")() + result.table = gdef + return result + def makeTable(self, tag): table = getattr(otTables, tag, None)() table.Version = 1.0 diff --git a/Lib/fontTools/feaLib/builder_test.py b/Lib/fontTools/feaLib/builder_test.py index de605b0b2..563ddebe1 100644 --- a/Lib/fontTools/feaLib/builder_test.py +++ b/Lib/fontTools/feaLib/builder_test.py @@ -75,7 +75,7 @@ class BuilderTest(unittest.TestCase): def expect_ttx(self, font, expected_ttx): path = self.temp_path(suffix=".ttx") - font.saveXML(path, quiet=True, tables=['GSUB', 'GPOS']) + font.saveXML(path, quiet=True, tables=['GDEF', 'GSUB', 'GPOS']) actual = self.read_ttx(path) expected = self.read_ttx(expected_ttx) if actual != expected: @@ -218,6 +218,19 @@ class BuilderTest(unittest.TestCase): "it must be the first of the languagesystem statements", self.build, "languagesystem latn TRK; languagesystem DFLT dflt;") + def test_markClass(self): + font = makeTTFont() + addOpenTypeFeatures(self.getpath("markClass.fea"), font) + self.expect_ttx(font, self.getpath("markClass.ttx")) + + def test_markClass_redefine(self): + self.assertRaisesRegex( + FeatureLibError, + "glyph C cannot be both in markClass @MARK1 and @MARK2", + self.build, + "markClass [A B C] @MARK1;" + "markClass [C D E] @MARK2;") + def test_script(self): builder = Builder(None, makeTTFont()) builder.start_feature(location=None, name='test') diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 303bf1c72..ff7994758 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -35,15 +35,18 @@ class Parser(object): statements.append(self.parse_languagesystem_()) elif self.is_cur_keyword_("lookup"): statements.append(self.parse_lookup_(vertical=False)) + elif self.is_cur_keyword_("markClass"): + self.parse_markClass_() elif self.is_cur_keyword_("feature"): statements.append(self.parse_feature_block_()) elif self.is_cur_keyword_("valueRecordDef"): statements.append( self.parse_valuerecord_definition_(vertical=False)) else: - raise FeatureLibError("Expected feature, languagesystem, " - "lookup, or glyph class definition", - self.cur_token_location_) + raise FeatureLibError( + "Expected feature, languagesystem, lookup, markClass, " + "or glyph class definition", + self.cur_token_location_) return self.doc_ def parse_anchor_(self): @@ -244,6 +247,21 @@ class Parser(object): self.lookups_.define(name, block) return block + def parse_markClass_(self): + assert self.is_cur_keyword_("markClass") + location = self.cur_token_location_ + glyphs = self.parse_glyphclass_(accept_glyphname=True) + anchor = self.parse_anchor_() + name = self.expect_glyphclass_() + self.expect_symbol_(";") + markClass = self.doc_.markClasses.get(name) + if markClass is None: + markClass = ast.MarkClassDefinition(location, name) + self.doc_.markClasses[name] = markClass + for glyph in glyphs: + markClass.anchors[glyph] = anchor + markClass.glyphLocations[glyph] = location + def is_next_glyphclass_(self): return (self.next_token_ == "[" or self.next_token_type_ in (Lexer.GLYPHCLASS, Lexer.NAME)) @@ -531,6 +549,8 @@ class Parser(object): statements.append(self.parse_language_()) elif self.is_cur_keyword_("lookup"): statements.append(self.parse_lookup_(vertical)) + elif self.is_cur_keyword_("markClass"): + self.parse_markClass_() elif self.is_cur_keyword_({"pos", "position"}): statements.append( self.parse_position_(enumerated=False, vertical=vertical)) @@ -566,6 +586,12 @@ class Parser(object): return self.cur_token_ in k return False + def expect_glyphclass_(self): + self.advance_lexer_() + if self.cur_token_type_ is not Lexer.GLYPHCLASS: + raise FeatureLibError("Expected @NAME", self.cur_token_location_) + return self.cur_token_ + def expect_tag_(self): self.advance_lexer_() if self.cur_token_type_ is not Lexer.NAME: diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index d7b70463e..0133e4378 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -442,6 +442,21 @@ class ParserTest(unittest.TestCase): " enumerate position cursive A ;" "} kern;") + def test_markClass(self): + doc = self.parse("markClass [acute grave] @MARKS;" + "feature test {" + " markClass cedilla @MARKS;" + "} test;") + markClass = doc.markClasses["MARKS"] + self.assertEqual(set(markClass.anchors.keys()), + {"acute", "cedilla", "grave"}) + acuteAnchor = markClass.anchors["acute"] + cedillaAnchor = markClass.anchors["cedilla"] + graveAnchor = markClass.anchors["grave"] + self.assertEqual((acuteAnchor.x, acuteAnchor.y), (350, 3)) + self.assertEqual((cedillaAnchor.x, cedillaAnchor.y), (400, -4)) + self.assertEqual((graveAnchor.x, graveAnchor.y), (350, 3)) + 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] diff --git a/Lib/fontTools/feaLib/testdata/markClass.fea b/Lib/fontTools/feaLib/testdata/markClass.fea new file mode 100644 index 000000000..8de1d40e8 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/markClass.fea @@ -0,0 +1,12 @@ +languagesystem DFLT dflt; + +markClass [acute] @TOP_MARKS; + +feature foo { + markClass [acute grave] @TOP_MARKS; + markClass cedilla @BOTTOM_MARKS; +} foo; + +feature bar { + markClass [dieresis breve] @TOP_MARKS; +} bar; diff --git a/Lib/fontTools/feaLib/testdata/markClass.ttx b/Lib/fontTools/feaLib/testdata/markClass.ttx new file mode 100644 index 000000000..c9ade3dd5 --- /dev/null +++ b/Lib/fontTools/feaLib/testdata/markClass.ttx @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + +