from warnings import warn warn("featureLib.py is deprecated.", DeprecationWarning) """ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX W A R N I N G This is work in progress, a fast moving target. XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX """ """FeatureLib An attempt to get OpenType features written like Adobe's FDK Feature descriptions to export into UFO and back. FontLab has an interface for writing the features. FeatureLib offers some tools to store the feature text, or to try interpretating it. There seem to be no clever ways to make an interpreter for feature speak based on abstract descriptions of the language: No Backus Naur Form description is available. No python interpreter No C source code available to the public So rather than go for the complete image, this implementation is incomplete and probably difficult to extend. But it can interpret the following features and write them back into the right format and order: feature xxxx { } xxxx; # lines with comment @classname = [name1 name2 etc]; sub x x x by y y; sub x from [a b c]; pos xx yy 100; When interpret = False is passed as parameter it won't attempt to interpret the feature text and just store it. Uninterpreted feature text is exported as an url encoded string to ensure roundtripping when the data is stored in a plist: %09feature%20smcp%20%7B This makes the feature text safe for storage in plist form without breaking anything. Also, if interpretation fails for any reason, the feature text is stored so data should be lost. To do: - make it possible to read a .fea file and spit into seperate items - test with more and different features from fontlab. """ DEBUG = True DEFAULTNAME = "xxxx" __all__ = ["Feature", "FeatureSet", "many_to_many", "one_from_many", "simple_pair", "extractFLFeatures", "putFeaturesLib", "getFeaturesLib"] # substition types # are there official opentype substitution titles for this? many_to_many = 0 one_from_many = 1 # kern types simple_pair = 0 # lib key for features featureLibKey = "org.robofab.features" class Feature: """Feature contains one single feature, of any flavor. Read from feature script Write to feature script Read from simple dict Write to simple dict Parse some of the lines Accept edits and additions """ def __init__(self, name=None, text=None, data=None, interpret=True): if name is not None: self.name = name else: self.name = DEFAULTNAME self._sub = [] self._pos = [] self._comment = [] self._feature = [] self._classes = [] self._tab = " "*4 self._text = None if text is not None: self.readFeatureText(text, interpret) elif data is not None: self.fromDict(data) def __repr__(self): return ""%(self.name) def addSub(self, itemsIn, itemsOut, subType=many_to_many): """Add a substitution statement""" self._sub.append((subType, itemsIn, itemsOut)) def addPos(self, itemOne, itemTwo, offset): """Add a positioning statement""" self._pos.append((itemOne, itemTwo, offset)) def hasSubs(self): """Return True if this feature has substitutions defined.""" return len(self._sub) > 0 def hasPos(self): """Return True if this feature has positioning defined.""" return len(self._pos) > 0 def readFeatureText(self, featureText, interpret=True): """Read the feature text and try to make sense of it. Note: Should you want to preserve the actual featuretext rather than the intrepreted data, set interpret = False In case the feature text isn't properly interpreted (possible) or because the feature text is hand edited and you just want it to round trip to UFO. """ if interpret: if featureText is not None: self.parse(featureText) else: self._text = featureText def parse(self, featureText): """bluntly split the lines of feature code as they come from fontlab This doesn't by any means parse all of the possible combinations in a .fea file. It parses the pos and sub lines defines within a feature. Something higher up should parse the seperate features from the .fea. It doesn't check for validity of the lines. """ lines = featureText.split("\n") count = 0 featureOpened = False interpretOK = True for l in lines: # # run through all lines # p = l.strip() if len(p)==0:continue if p[-1] == ";": p = p[:-1] p = p.split(" ") count += 1 # # plain substitutions # example: # sub @class496 by @class497; # sub aring from [amacron aringacute adieresis aacute]; # sub s s by s_s; # if p[0] == "sub": if "by" in p: # sub xx by xx; self.addSub(p[1:p.index("by")], p[p.index("by")+1:], many_to_many) elif "from" in p: # sub x from [zzz]; theList = " ".join(p[p.index("from")+1:])[1:-1].split(" ") self.addSub(p[1:p.index("from")], theList, one_from_many) # # plain kerning # example: # pos Yacute A -215; # elif p[0] == "pos": items = p[1:-1] value = int(p[-1]) self._pos.append((simple_pair, items, value)) # # comments # elif p[0] == "#": # comment? self._comment.append(" ".join(p[1:])) # # features beginning or feature within feature # elif p[0] == "feature": # comment? if not featureOpened: # ah, it's a fully wrapped description if len(p[1]) == 4 and p[2] == "{": self.name = p[1] else: print 'uh oh xxxxx', p featureOpened = True else: # it's an unwrapped (from fontlab) description self._feature.append(p[1:]) # # feature ending # elif p[0] == "}" and p[1] == self.name: featureOpened = False # # special cases (humph) # # # special case: class definitions # elif "=" in p: # check for class defenitions # example: # @MMK_L_A = [A Aacute]; # @S = [S Sacute Scedille] equalOperatorIndex = p.index("=") classNames = p[:equalOperatorIndex] # get the seperate names from the list: classMembers = " ".join(p[equalOperatorIndex+1:])[1:-1].split(" ") self._classes.append((classNames, classMembers)) # # we can't make sense of it, store the feature text instead then.. # else: print "Feature interpreter error:", p interpretOK = False if not interpretOK: if DEBUG: "Couldn't interpret all feature lines, storing the text as well." self._text = featureText def writeFeatureText(self, wrapped=True): """return the feature as an OpenType feature string wrapped = True: wrapped with featurename { feature items; } wrapped = False: similar to that produced by FontLab """ text = [] if self._text: # if literal feature text is stored: use that # XXXX how to handle is there are new, manually added feature items? # XXXX should the caller clear the text first? from urllib import unquote return unquote(self._text) if wrapped: text.append("feature %s {"%self.name) if self._comment: text.append(" # %s"%(" ".join(self._comment))) if self._feature: for f in self._feature: text.append(" feature %s;"%(" ".join(f))) if self._classes: # # first dump any in-feature class definitions # for classNames, classMembers in self._classes: text.append(self._tab+"%s = [%s];"%(" ".join(classNames), " ".join(classMembers))) if self._pos: # # run through the list twice to get the class kerns first # for posType, names, value in self._pos: text.append(self._tab+"pos %s %d;"%(" ".join(names), value)) if self._sub: for (subType, stuffIn, stuffOut) in self._sub: if subType == many_to_many: text.append(self._tab+"sub %s by %s;"%((" ".join(stuffIn), " ".join(stuffOut)))) elif subType == one_from_many: text.append(self._tab+"sub %s from [%s];"%((" ".join(stuffIn), " ".join(stuffOut)))) if wrapped: text.append("} %s;"%self.name) final = "\n".join(text)+"\n" return final def asDict(self): """Return the data of this feature as a plist ready dictionary""" data = {} data['name'] = self.name if self._comment: data['comment'] = self._comment if self._sub: data["sub"] = self._sub if self._pos: data["pos"] = self._pos if self._feature: data["feature"] = self._feature if self._text: from urllib import quote data['text'] = quote(self._text) return data def fromDict(self, aDict): """Read the data from a dict.""" self.name = aDict.get("name", DEFAULTNAME) self._sub = aDict.get("sub", []) self._pos = aDict.get("pos", []) self._feature = aDict.get("feature", []) self._comment = aDict.get("comment", []) text = aDict.get('text', None) if text is not None: from urllib import unquote self._text = unquote(text) class FeatureSet(dict): """A dict to combine all features, and write them to various places""" def __init__(self, interpret=True): self.interpret = interpret def readFL(self, aFont): """Read the feature stuff from a RFont in FL context. This can be structured better I think, but let's get something working first. """ for name in aFont.getOTFeatures(): if DEBUG: print 'reading %s from %s'%(name, aFont.info.fullName) self[name] = Feature(name, aFont.getOTFeature(name), interpret = self.interpret) self.changed = True def writeFL(self, aFont, featureName=None): """Write one or all features back""" if featureName == None: names = self.keys() else: names = [featureName] for n in names: text = self[n].writeFeatureText(wrapped=False) if DEBUG: print "writing feature %s"%n print '- '*30 print `text` print `self[n]._text` print '- '*30 aFont.setOTFeature(n, text) def writeLib(self, aFont): aFont.lib[featureLibKey] = self.asDict() def readLib(self, aFont): """Read the feature stuff from the font lib. Rather than add all this to yet another file in the UFO, just store it in the lib. UFO users will be able to read the data anyway. """ stuff = aFont.lib.get(featureLibKey, None) if stuff is None: if DEBUG: print "No features found in this lib.." return self.clear() self.update(stuff) def append(self, aFeature): """Append a feature object to this set""" self[aFeature.name] = aFeature if DEBUG: print "..added %s to FeatureSet"%aFeature.name def newFeature(self, name): """Add a new feature and return it""" self[name] = Feature(name) return self[name] def update(self, aDict): """Accept a dictionary with all features written out as dicts. Ready for data read from plist """ for name, feature in aDict.items(): self[name] = Feature(data=feature, interpret=self.interpret) def asDict(self): """Return a dict with all features also written out as dicts. Not the same as self. Data is ready for writing to plist """ data = {} for name, feature in self.items(): data[name] = feature.asDict() return data # convenience functions def extractFLFeatures(aFont, interpret=True): """FontLab specific: copy features from the font to the font.lib""" fs = FeatureSet(interpret = interpret) fs.readFL(aFont) fs.writeLib(aFont) def putFeaturesLib(aFont, featureSet): """Put the features in the appropriate place in the font.lib""" featureSet.writeLib(aFont) def getFeaturesLib(aFont, interpret=True): """Get the featureset from a lib.""" fs = FeatureSet(interpret = interpret) fs.readLib(aFont) return fs if __name__ == "__main__": # examples print "-"*10, "sub many by many" # a regular ligature feature dligtext = """feature dlig { # Latin @MMK_L_A = [A Aacute]; sub I J by IJ; sub i j by ij; sub s s by s_s; } dlig; """ feat1 = Feature(text=dligtext) print feat1.asDict() print print feat1.writeFeatureText() print "-"*10, "sub one from many" # aalt one from many substitution aalttext = """feature aalt { sub aring from [amacron acircumflex adblgrave a agrave abreve acaron atilde aogonek aringacute adieresis aacute]; sub utilde from [umacron uring uacute udieresisacute ucircumflex uhorn udblgrave udieresis uhungarumlaut udieresisgrave ugrave ubreve uogonek ucaron u]; sub Hcircumflex from [H.sc Hcedilla.sc Hcircumflex.sc Hdotaccent H Hcedilla Hdieresis Hdieresis.sc Hdotaccent.sc]; sub pdotaccent from [p pacute]; } aalt; """ feat2 = Feature(text=aalttext) print feat2.asDict() print print feat2.writeFeatureText() print "-"*10, "kerning" # kern and positioning kerntext = """ feature kern { # Latin pos Yacute A -215; pos Yacute B -30; pos Yacute C -100; pos Yacute D -50; pos Yacute E -35; pos Yacute F -35; pos Yacute G -80; pos Yacute H -25; # -- kerning classes @MMK_L_A = [A Aacute]; @MMK_R_C = [C Ccedilla]; } kern; """ feat3 = Feature(text=kerntext) print feat3.asDict() print print feat3.writeFeatureText() print "-"*10, "something with groups in it" # references to groups are treated like any other grouptext = """ feature smcp { # Latin sub @class496 by @class497; } smcp; """ # Feature doesn't interpret the text in this example # when interpret = False is given as parameter, # the feature code is stored and reproduced exactly. # But then you have to specify the name of the feature # otherwise it will default and overwrite other features # with the same default name. feat4 = Feature(name="smcp", text=grouptext, interpret = False) print feat4.asDict() print print feat4.writeFeatureText() print "-"*10, "store the feature set in the lib" # now create a feature set to dump all features in the lib set = FeatureSet() set.append(feat1) set.append(feat2) set.append(feat3) set.append(feat4) from robofab.world import NewFont testFont = NewFont() set.writeLib(testFont) print testFont.lib[featureLibKey] print "-"*10, "read the feature set from the lib again" notherSet = FeatureSet() notherSet.readLib(testFont) for name, feat in notherSet.items(): print name, feat