From 15a678c34e3606e65c436c77c74b04539e2e14bb Mon Sep 17 00:00:00 2001 From: James Godfrey-Kittle Date: Wed, 4 Mar 2015 15:23:47 -0800 Subject: Use feaTools in features.py. Use an existing library for parsing OpenType features, which should be more robust (though this library does have some syntax parsing issues, addressed in our code). --- scripts/lib/fontbuild/Build.py | 8 +- scripts/lib/fontbuild/features.py | 258 ++++++++++++++++++++++---------------- 2 files changed, 154 insertions(+), 112 deletions(-) (limited to 'scripts/lib') diff --git a/scripts/lib/fontbuild/Build.py b/scripts/lib/fontbuild/Build.py index a3c7d54..c524e7a 100644 --- a/scripts/lib/fontbuild/Build.py +++ b/scripts/lib/fontbuild/Build.py @@ -6,7 +6,7 @@ from fontbuild.convertCurves import glyphCurvesToQuadratic from fontbuild.mitreGlyph import mitreGlyph from fontbuild.generateGlyph import generateGlyph from fontTools.misc.transform import Transform -from fontbuild.features import generateFeatureFile, readFeatureFile, readGlyphClasses, writeFeatureFile +from fontbuild.features import readFeatureFile, writeFeatureFile from fontbuild.markFeature import GenerateFeature_mark from fontbuild.mkmkFeature import GenerateFeature_mkmk from fontbuild.decomposeGlyph import decomposeGlyph @@ -142,8 +142,7 @@ class FontProject: log(">> Generating glyphs") generateGlyphs(f, self.diacriticList, self.adobeGlyphList) log(">> Copying features") - readGlyphClasses(f, self.ot_classes) - readFeatureFile(f, self.basefont.features.text) + readFeatureFile(f, self.ot_classes + self.basefont.features.text) log(">> Decomposing") for gname in self.decompose: if f.has_key(gname): @@ -156,10 +155,9 @@ class FontProject: if kern: log(">> Generating kern classes") - readGlyphClasses(f, self.ot_kerningclasses, update=False) + readFeatureFile(f, self.ot_kerningclasses) log(">> Generating font files") - generateFeatureFile(f) ufoName = self.generateOutputPath(f, "ufo") f.save(ufoName) diff --git a/scripts/lib/fontbuild/features.py b/scripts/lib/fontbuild/features.py index fb1f1c9..3b52f76 100755 --- a/scripts/lib/fontbuild/features.py +++ b/scripts/lib/fontbuild/features.py @@ -1,120 +1,164 @@ -"""Functions for parsing and validating RoboFab RFont feature files.""" - - import re - -# feature file syntax rules from: -# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html -_glyphNameChars = r"[A-Za-z_][\w.]" -_glyphName = r"%s{,30}" % _glyphNameChars -_className = r"@%s{,29}" % _glyphNameChars -_classValToken = ( - r"(?:%s|%s(?:\s*-\s*%s)?)" % (_className, _glyphName, _glyphName)) -_classVal = r"\[\s*(%s(?:\s+%s)*)\s*\]" % (_classValToken, _classValToken) -_classDef = re.compile(r"(%s)\s*=\s*%s\s*;" % (_className, _classVal)) -_featureDef = re.compile( - r"(feature\s+(?P[A-Za-z]{4})\s+\{.*?\}\s+(?P=tag)\s*;)", - re.DOTALL) -_subRuleToken = r"(?:%s|%s)'?" % (_glyphName, _className) -_subRuleTokenList = ( - r"\[?\s*(%s(?:\s+%s)*)\s*\]?" % (_subRuleToken, _subRuleToken)) -_subRule = re.compile( - r"(\s*)sub(?:stitute)?\s+%s\s+by\s+%s\s*;" % - (_subRuleTokenList, _subRuleTokenList)) -_systemDef = re.compile(r"languagesystem\s+([A-Za-z]+)\s+([A-Za-z]+)\s*;") -_comment = re.compile(r"\s*#.*") +from feaTools import parser +from feaTools.writers.fdkSyntaxWriter import FDKSyntaxFeatureWriter + + +# fix some regular expressions used by feaTools +# we may want to push these fixes upstream +# allow dashes in glyph class content, for glyph ranges +parser.classDefinitionRE = re.compile( + "([\s;\{\}]|^)" # whitepace, ; {, } or start of line + "@" # @ + "([\w\d_.]+)" # name + "\s*=\s*" # = + "\[" # [ + "([\w\d\s\-_.@]+)" # content + "\]" # ] + "\s*;" # ; + , re.M + ) +parser.classContentRE = re.compile( + "([\w\d\-_.@]+)" + ) + +# allow apostrophes in feature/lookup content +parser.sequenceInlineClassRE = re.compile( + "\[" # [ + "([\w\d\s_.@']+)" # content + "\]" # ] + ) + +# allow apostrophes in the target and replacement of a substitution +parser.subType1And4RE = re.compile( + "([\s;\{\}]|^)" # whitepace, ; {, } or start of line + "substitute|sub\s+" # sub + "([\w\d\s_.@\[\]']+)" # target + "\s+by\s+" # by + "([\w\d\s_.@\[\]']+)" # replacement + "\s*;" # ; + ) + +# don't be greedy when matching feature/lookup content (may be duplicates) +parser.featureContentRE[3] = parser.featureContentRE[3].replace('*', '*?') +parser.lookupContentRE[3] = parser.lookupContentRE[3].replace('*', '*?') + + +class FilterFeatureWriter(FDKSyntaxFeatureWriter): + """Feature writer to detect invalid references and duplicate definitions.""" + + def __init__(self, refs=set(), name=None, isFeature=False): + """Initializes the set of known references, empty by default.""" + self.refs = refs + self.featureNames = set() + self.lookupNames = set() + self.languageSystems = set() + super(FilterFeatureWriter, self).__init__( + name=name, isFeature=isFeature) + + # error to print when undefined reference is found in glyph class + self.classErr = ('Undefined reference "%s" removed from glyph class ' + 'definition %s.') + + # error to print when undefined reference is found in sub or pos rule + subErr = ['Substitution rule with undefined reference "%s" removed'] + if self._name: + subErr.append(" from ") + subErr.append("feature" if self._isFeature else "lookup") + subErr.append(' "%s"' % self._name) + subErr.append(".") + self.subErr = "".join(subErr) + self.posErr = self.subErr.replace("Substitution", "Positioning") + + def _subwriter(self, name, isFeature): + """Use this class for nested expressions e.g. in feature definitions.""" + return FilterFeatureWriter(self.refs, name, isFeature) + + def _checkRefs(self, refs, errorMsg): + """Check a list of references found in a sub or pos rule.""" + for ref in refs: + # trailing apostrophes should be ignored + if ref[-1] == "'": + ref = ref[:-1] + if ref not in self.refs: + print errorMsg % ref + # insert an empty instruction so that we can't end up with an + # empty block, which is illegal syntax + super(FilterFeatureWriter, self).rawText(";") + return False + return True + + def classDefinition(self, name, contents): + """Check that contents are valid, then add name to known references.""" + if name in self.refs: + return + newContents = [] + for ref in contents: + if ref not in self.refs and ref != "-": + print self.classErr % (ref, name) + else: + newContents.append(ref) + self.refs.add(name) + super(FilterFeatureWriter, self).classDefinition(name, newContents) + + def gsubType1(self, target, replacement): + """Check a sub rule with one-to-one replacement.""" + if type(target) == str: + target, replacement = [target], [replacement] + if self._checkRefs(target + replacement, self.subErr): + super(FilterFeatureWriter, self).gsubType1(target, replacement) + + def gsubType4(self, target, replacement): + """Check a sub rule with many-to-one replacement.""" + if self._checkRefs(target + [replacement], self.subErr): + super(FilterFeatureWriter, self).gsubType4(target, replacement) + + def gposType1(self, target, value): + """Check a positioning rule.""" + if self._checkRefs([target], self.posErr): + super(FilterFeatureWriter, self).gposType1(target, value) + + # these rules may contain references, but they aren't present in Roboto + def gsubType3(self, target, replacement): + raise NotImplementedError + def gsubType6(self, precedingContext, target, trailingContext, replacement): + raise NotImplementedError + def gposType2(self, target, value): + raise NotImplementedError + + def feature(self, name): + """Adds a feature definition only once.""" + if name not in self.featureNames: + self.featureNames.add(name) + return super(FilterFeatureWriter, self).feature(name) + # we must return a new writer even if we don't add it to this one + return FDKSyntaxFeatureWriter(name, True) + + def lookup(self, name): + """Adds a lookup block only once.""" + if name not in self.lookupNames: + self.lookupNames.add(name) + return super(FilterFeatureWriter, self).lookup(name) + # we must return a new writer even if we don't add it to this one + return FDKSyntaxFeatureWriter(name, False) + + def languageSystem(self, langTag, scriptTag): + """Adds a language system instruction only once.""" + system = (langTag, scriptTag) + if system not in self.languageSystems: + self.languageSystems.add(system) + super(FilterFeatureWriter, self).languageSystem(langTag, scriptTag) def readFeatureFile(font, text): """Incorporate valid definitions from feature text into font.""" - - readGlyphClasses(font, text) - text = "\n".join([l for l in text.splitlines() if not _comment.match(l)]) - - # filter out substitution rules with invalid references - errorMsg = "feature definition %s (substitution rule removed)" - if not hasattr(font.features, "tags"): - font.features.tags = [] - font.features.values = {} - for value, tag in _featureDef.findall(text): - lines = value.splitlines() - for i in range(len(lines)): - match = _subRule.match(lines[i]) - if not match: - continue - indentation, subbed, sub = match.groups() - refs = subbed.split() + sub.split() - invalid = None - for ref in refs: - if ref[-1] == "'": - ref = ref[:-1] - if not invalid and not _isValidRef(errorMsg % tag, ref, font): - invalid = ref - if invalid: - lines[i] = ("%s; # substitution rule removed for invalid " - "reference %s" % (indentation, invalid)) - font.features.tags.append(tag) - font.features.values[tag] = "\n".join(lines) - - -def readGlyphClasses(font, text, update=True): - """Incorporate valid glyph classes from feature text into font.""" - - text = "\n".join([l for l in text.splitlines() if not _comment.match(l)]) - - # filter out invalid references from glyph class definitions - errorMsg = "glyph class definition %s (reference removed)" - if not hasattr(font, "classNames"): - font.classNames = [] - font.classVals = {} - for name, value in _classDef.findall(text): - if name in font.classNames: - if not update: - continue - font.classNames.remove(name) - refs = value.split() - refs = [r for r in refs if _isValidRef(errorMsg % name, r, font)] - font.classNames.append(name) - font.classVals[name] = " ".join(refs) - - if not hasattr(font, "languageSystems"): - font.languageSystems = [] - for system in _systemDef.findall(text): - if system not in font.languageSystems: - font.languageSystems.append(system) - - -def _isValidRef(referencer, ref, font): - """Check if a reference is valid for a font.""" - - if ref.startswith("@"): - if not font.classVals.has_key(ref): - print "Undefined class %s referenced in %s." % (ref, referencer) - return False - else: - for r in ref.split("-"): - if r and not font.has_key(r): - print "Undefined glyph %s referenced in %s." % (r, referencer) - return False - return True - - -def generateFeatureFile(font): - """Populate a font's feature file text from its classes and features.""" - - classes = "\n".join( - ["%s = [%s];" % (n, font.classVals[n]) for n in font.classNames]) - systems = "\n".join( - ["languagesystem %s %s;" % (s[0], s[1]) for s in font.languageSystems]) - fea = "\n\n".join([font.features.values[t] for t in font.features.tags]) - font.features.text = "\n\n".join([classes, systems, fea]) + writer = FilterFeatureWriter(set(font.keys())) + parser.parseFeatures(writer, text + font.features.text) + font.features.text = writer.write() def writeFeatureFile(font, path): """Write the font's features to an external file.""" - - generateFeatureFile(font) fout = open(path, "w") fout.write(font.features.text) fout.close() -- cgit v1.2.3