def new(klass, fontfile, editor=None): self = FluxProject() self.fontfeatures = FontFeatures() self.fontfile = fontfile self.editor = editor if not self._load_fontfile(): return self.glyphclasses = {} self.glyphactions = {} self.debuggingText = "" self.filename = None if self.fontfile.endswith(".ttf") or self.fontfile.endswith(".otf"): self._load_features_binary() else: self._load_features_source() for groupname, contents in self.font.groups.items(): self.glyphclasses[groupname] = { "type": "manual", "contents": contents } self.fontfeatures.namedClasses.forceput(groupname, tuple(contents)) # Load up the anchors too self._load_anchors() return self
def roundTrip(self, thing, dependencies): f = FontFeatures() f.routines.extend(dependencies) rt = thing.__class__.fromXML(thing.toXML()) f.routines.append(Routine(rules=[rt])) f.resolveAllRoutines() self.assertEqual(rt.asFea(), thing.asFea())
def __init__(self, font): self.grammar = self._make_initial_grammar() self.grammar_generation = 1 self.font = font self.fontfeatures = FontFeatures() self.current_file = None self.plugin_classes = {} self.current_feature = None self.font_modified = False self.variables = {} self._rebuild_parser() # Set up GDEF table from font self.fontfeatures.setGlyphClassesFromFont(self.font) for plugin in self.DEFAULT_PLUGINS: self._load_plugin(plugin)
def test_MergeMultipleSingleSubstitutions_1(self): r1 = Routine(rules=[ Substitution([["a", "y"]], [["b", "z"]]), Substitution([["b", "d", "a"]], [["c", "e", "f"]]), ]) Optimizer(FontFeatures()).optimize_routine(r1, level=1) self.assertEqual(r1.asFea(), " sub [a y b d] by [c z c e];\n")
class TestUnparse(unittest.TestCase): font = TTFont("fonts/Amiri-Regular.ttf") lookups = font["GSUB"].table.LookupList.Lookup ff = FontFeatures() unparser = GSUBUnparser(font["GSUB"], ff, []) def test_single(self): g, _ = self.unparser.unparseLookup(self.lookups[1], 1) # part of locl self.assertEqual(g.rules[0].asFea(), "sub period by period.ara;") self.assertEqual(g.rules[1].asFea(), "sub guillemotleft by guillemotleft.ara;") def test_ligature(self): g, _ = self.unparser.unparseLookup(self.lookups[0], 0) # part of ccmp self.assertEqual(g.rules[0].asFea(), "sub uni0627 uni065F by uni0673;") def test_multiple(self): g, _ = self.unparser.unparseLookup(self.lookups[10], 10) self.assertEqual(g.rules[0].asFea(), "sub uni08B6 by uni0628 smallmeem.above;") def test_ignore(self): g, _ = self.unparser.unparseLookup(self.lookups[47], 47) self.unparser.lookups = [None] * 47 + [g] assert self.unparser.lookups[47] g, _ = self.unparser.unparseLookup(self.lookups[48], 48) self.assertEqual( g.rules[0].asFea(), "ignore sub [uni0622 uni0627 uni0648 uni0671 uni0627.fina uni0671.fina] uni0644.init' uni0644.medi' [uni0647.fina uni06C1.fina];", )
def unparse(font, do_gdef=False, doLookups=True, config={}): """Convert a binary OpenType font into a fontFeatures object Args: font: A ``TTFont`` object. do_gdef: Boolean. Whether the GDEF table should also be read. doLookups: Whether the lookups should be read, or just the script/language information and top-level features. config: A dictionary of glyph class and routine names. """ gsub_gpos = [ font[tableTag] for tableTag in ("GSUB", "GPOS") if tableTag in font ] ff = FontFeatures() languageSystems = unparseLanguageSystems(gsub_gpos) if "GSUB" in font: GSUBUnparser(font["GSUB"], ff, languageSystems, font=font, config=config).unparse(doLookups=doLookups) if "GPOS" in font: GPOSUnparser(font["GPOS"], ff, languageSystems, font=font, config=config).unparse(doLookups=doLookups) return ff
def __init__(self, file=None): if not file: return self.filename = file self.xml = etree.parse(file).getroot() dirname = os.path.dirname(file) self.fontfile = os.path.join(dirname, self.xml.find("source").get("file")) self.fontfeatures = FontFeatures() if not self._load_fontfile(): return self.glyphactions = {} self.xmlToFontFeatures() text = self.xml.find("debuggingText") if text is not None: self.debuggingText = text.text else: self.debuggingText = "" self.glyphclasses = {} # Will sync to fontFeatures when building # XXX will it? glyphclasses = self.xml.find("glyphclasses") if glyphclasses is not None: for c in glyphclasses: thisclass = self.glyphclasses[c.get("name")] = {} if c.get("automatic") == "true": thisclass["type"] = "automatic" thisclass["predicates"] = [ dict(p.items()) for p in c.findall("predicate") ] self.fontfeatures.namedClasses[c.get("name")] = tuple( GlyphClassPredicateTester(self).test_all([ GlyphClassPredicate(x) for x in thisclass["predicates"] ])) else: thisclass["type"] = "manual" thisclass["contents"] = [g.text for g in c] self.fontfeatures.namedClasses[c.get("name")] = tuple( [g.text for g in c]) # The font file is the authoritative source of the anchors, so load them # from the font file on load, in case they have changed. self._load_anchors() self._load_glyphactions()
def convert(self): doc = Parser(self._file_or_path).parse() if self._font is not None: self._glyph_order = self._font.getGlyphOrder() self.ff = FontFeatures() self._collectStatements(doc) return self.ff
def test_multiple_languages_routine(): f = FontFeatures() s1 = Substitution([["a"]], ["b"]) expected = """languagesystem arab URD; languagesystem arab FAR; lookup Routine_1 { lookupflag 0; ; sub a by b; } Routine_1; feature locl { script arab; language URD; lookup Routine_1; } locl; feature locl { script arab; language FAR; lookup Routine_1; } locl; """ f = FontFeatures() r1 = Routine(rules=[s1], languages=[("arab", "URD "), ("arab", "FAR ")]) f.addFeature("locl", [r1]) assert f.asFea(do_gdef=False) == expected
def test_complex_pos(self): v = ValueRecord(xAdvance=120) pos1 = Positioning(["a"], [v]) r1 = Routine(rules=[pos1]) rr1 = RoutineReference(routine=r1) c = Chaining([["a"], ["b"]], lookups=[[rr1], None]) c.feaPreamble(FontFeatures()) self.assertEqual(c.asFea(), "pos a' lookup ChainedRoutine1 b';")
def test_MergeMultipleSingleSubstitutions_2(self): r1 = Routine(rules=[ Substitution([["a"]], [["b"]]), Substitution([["b"]], [["c"]]), Substitution([["d"]], [["e"]]), Substitution([["y"]], [["z"]]), ]) Optimizer(FontFeatures()).optimize_routine(r1, level=1) self.assertEqual(r1.asFea(), " sub [a b d y] by [c c e z];\n")
def test_GlyphClasses(self): r1 = Routine(rules=[ Substitution([["a", "b", "c", "d", "e", "f", "g", "h"]], [["z"]]), ]) ff = FontFeatures() Optimizer(ff).optimize_routine(r1, level=1) self.assertEqual(r1.asFea(), " sub @class1 by z;\n") self.assertEqual(ff.namedClasses["class1"], ("a", "b", "c", "d", "e", "f", "g", "h"))
def test_fromXML(): ff = FontFeatures.fromXML(etree.fromstring(expected)) assert ff.namedClasses["One"] == ["A", "B", "C"] assert ff.namedClasses["Two"] == ["d", "e", "f"] assert "onex" in ff.features assert ff.features["onex"][0].routine == ff.routines[0] assert "twox" in ff.features assert ff.features["twox"][0].routine == ff.routines[1] assert ff.glyphclasses["a"] == "base" assert ff.glyphclasses["d"] == "mark" assert ff.anchors["a"] == {"top": (300, 200)}
def test_routine_partition_not_needed(): f = FontFeatures() s1 = Substitution([["A"]], [["A.grk"]], languages=["grek/*"]) s2 = Substitution([["A"]], [["A.esp"]], languages=["grek/*"]) r = Routine(rules=[s1, s2], flags=0x2) f.routines.append(r) f.partitionRoutine(r, lambda rule: tuple(rule.languages or [])) assert len(f.routines) == 1 r.rules = [] f.partitionRoutine(r, lambda rule: tuple(rule.languages or [])) assert len(f.routines) == 1
def test_featurelist(qtmodeltester): proj = FluxProject() proj.fontfeatures = FontFeatures() r1 = Routine(name="routine1") r2 = Routine(name="routine2") proj.fontfeatures.features["test"] = [r1, r2] proj = FluxProject("Hind.fluxml") model = FeatureListModel(proj) root = model.index(-1, -1) assert (model.describeIndex(root) == "root of tree") feature1 = model.index(0, 0) assert (model.describeIndex(feature1) == "feature at row 0") child1 = model.index(0, 0, feature1) assert (child1.parent() == feature1) assert (model.index(0, 0, feature1) == model.index(0, 0, feature1)) # import code; code.interact(local=locals()) qtmodeltester.check(model, force_py=True) qtmodeltester.check(model)
def test_script_language_split(): f = FontFeatures() s1 = Substitution([["question"]], [["questiongreek"]], languages=[("grek", "*")]) s2 = Substitution([["A"]], [["alpha"]], languages=[("grek", "ELL ")]) s3 = Substitution([["question"]], [["questiondown"]], languages=[("latn", "ESP ")]) s4 = Substitution([["question"]], [["B"]], languages=[("latn", "POL ")]) s5 = Substitution([["X"]], [["Y"]]) r = Routine(rules=[s1, s2, s3, s4, s5]) f.addFeature("locl", [r]) f.buildBinaryFeatures(font) gsub = "\n".join(getXML(font["GSUB"].toXML)) assert gsub == """<Version value="0x00010000"/>
class TestUnparse(unittest.TestCase): font = TTFont("fonts/Amiri-Regular.ttf") lookups = font["GSUB"].table.LookupList.Lookup ff = FontFeatures() unparser = GSUBUnparser(font["GSUB"], ff, []) def test_single(self): g, _ = self.unparser.unparseLookup(self.lookups[1], 1) # part of locl self.assertEqual(g.rules[0].asFea(), "sub period by period.ara;") self.assertEqual(g.rules[1].asFea(), "sub guillemotleft by guillemotleft.ara;") def test_ligature(self): g, _ = self.unparser.unparseLookup(self.lookups[0], 0) # part of ccmp self.assertEqual(g.rules[0].asFea(), "sub uni0627 uni065F by uni0673;") def test_multiple(self): g, _ = self.unparser.unparseLookup(self.lookups[10], 10) self.assertEqual(g.rules[0].asFea(), "sub uni08B6 by uni0628 smallmeem.above;") def test_ignore(self): g, _ = self.unparser.unparseLookup(self.lookups[48], 48) self.assertEqual( g.rules[0].asFea(), "ignore sub [uni0622 uni0627 uni0648 uni0671 uni0627.fina uni0671.fina] uni0644.init' uni0644.medi' [uni0647.fina uni06C1.fina];", ) def test_chaining(self): self.unparser.unparseLookups() g, _ = self.unparser.unparseLookup(self.lookups[33], 33) # part of calt in quran.fea self.unparser.resolve_routine(g) self.assertEqual( g.rules[0].asFea(), "sub uni0644' lookup SingleSubstitution32 uni0621' lookup SingleSubstitution31 uni0627' lookup SingleSubstitution32;", )
def test_routine_partition(): f = FontFeatures() s1 = Substitution([["A"]], [["A.grk"]], languages=["grek/*"]) s2 = Substitution([["A"]], [["A.esp"]], languages=["latn/ESP "]) r = Routine(rules=[s1, s2], flags=0x2) f.routines.append(r) dummy = Routine(rules=[Substitution([["G"]], [["G"]])]) f.routines.append(dummy) c = Chaining( [["A"], ["V"]], lookups=[ [RoutineReference(routine=dummy), RoutineReference(routine=r)], [RoutineReference(routine=r), RoutineReference(routine=dummy)], ]) r2 = Routine(rules=[c]) f.routines.append(r2) f.addFeature("locl", [r]) f.partitionRoutine(r, lambda rule: tuple(rule.languages or [])) assert len(f.routines) == 4 assert f.routines[0].flags == f.routines[1].flags assert len(f.routines[0].rules) == 1 assert len(f.routines[1].rules) == 1 assert f.routines[0].rules[0].replacement[0][0] == "A.grk" assert f.routines[1].rules[0].replacement[0][0] == "A.esp" assert len(c.lookups[0]) == 3 assert len(f.features["locl"]) == 2
def test_language_ordering(): f = FontFeatures() s1 = Substitution([["a"]], ["b"], languages=[("arab", "URD ")]) s2 = Substitution([["a"]], ["c"], languages=[("arab", "FAR ")]) s3 = Substitution([["x"], ["y"]], ["z"], languages=[("arab", "URD ")]) f.addFeature("locl", [Routine(rules=[s1, s2, s3])]) # When going to fea, we put everything in its own feature block to # avoid stupid problems with language systems expected = """languagesystem arab URD; languagesystem arab FAR; lookup Routine_1 { lookupflag 0; ; sub a by b; } Routine_1; lookup Routine_2 { lookupflag 0; ; sub a by c; } Routine_2; lookup Routine_3 { lookupflag 0; ; sub x y by z; } Routine_3; feature locl { script arab; language URD; lookup Routine_1; } locl; feature locl { script arab; language FAR; lookup Routine_2; } locl; feature locl { script arab; language URD; lookup Routine_3; } locl; """ assert f.asFea(do_gdef=False) == expected # But the same is true of multiple lookups in the same feature f = FontFeatures() r1 = Routine(rules=[s1]) r2 = Routine(rules=[s2]) r3 = Routine(rules=[s3]) f.addFeature("locl", [r1, r2, r3]) assert f.asFea(do_gdef=False) == expected
class FeeParser: """Convert a FEE file into a fontFeatures object. The resulting object is stored in the parser's ``fontFeatures`` property. Args: font: A TTFont object or glyphsLib GSFontMaster object. """ basegrammar = """ feefile = wsc statement+ statement = verb:v wsc callRule(v "Args"):args ws ';' wsc -> parser.do(v, args) rest_of_line = <('\\\n' | (~'\n' anything))*> wsc = (comment | ' ' | '\t' | '\n')+ | ws comment = '#' rest_of_line ws? verb = <letter+>:x ?(x in self.valid_verbs) -> x # Ways of specifying glyphs classname = '@' barename:b -> {"classname": b["barename"]} startglyphname = anything:x ?(x in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") midglyphname = anything:x ?(x in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-") # barename = <(letter|digit|"_"|'-')+>:b -> {"barename": b} barename = <startglyphname midglyphname*>:b -> {"barename": b} inlineclass_member = (barename|classname):m ws? -> m inlineclass_members = inlineclass_member+ inlineclass = '[' ws inlineclass_members:m ']' -> {"inlineclass": m} regex = '/' <(~'/' anything)+>:r '/' -> {"regex": r} hexdigit = anything:x ?(x in '0123456789abcdefABCDEF') -> x unicodechar = 'U+' <hexdigit+>:u -> int(u,16) unicoderange = unicodechar:s '=>' unicodechar:e -> {"unicoderange": range(s,e+1)} unicodeglyphname = unicodechar:u -> {"unicodeglyph": u } glyphsuffix = ('.'|'~'):suffixtype <(letter|digit|"_")+>:suffix -> {"suffixtype":suffixtype, "suffix":suffix} glyphselector = ( unicoderange | unicodeglyphname | regex | barename | classname | inlineclass ):g glyphsuffix*:s -> GlyphSelector(g,s, self.input.position) # Number things bareinteger = ('-'|'+')?:sign <digit+>:i -> (-int(i) if sign == "-" else int(i)) namedinteger = '$' barename:b ?(b["barename"] in parser.variables) -> int(parser.variables[b["barename"]]) integer = namedinteger | bareinteger # Value records valuerecord = integer_value_record | fee_value_record | traditional_value_record integer_value_record = integer:xAdvance -> (0, 0, xAdvance, 0) traditional_value_record = '<' integer:xPlacement ws integer:yPlacement ws integer:xAdvance ws integer:yAdvance '>' -> (xPlacement, yPlacement, xAdvance, yAdvance) fee_value_record = '<' ws fee_value_record_member+:m '>' -> { "members": m } fee_value_record_member = ("xAdvance"| "xPlacement" | "yAdvance" | "yPlacement"):d '=' integer:pos ws -> {"dimension": d, "position": pos} """ DEFAULT_PLUGINS = [ "LoadPlugin", "ClassDefinition", "Conditional", "Feature", "Substitute", "Position", "Chain", "Anchors", "Routine", "Include", "Variables", ] def __init__(self, font): self.grammar = self._make_initial_grammar() self.grammar_generation = 1 self.font = font self.fontfeatures = FontFeatures() self.current_file = None self.plugin_classes = {} self.current_feature = None self.font_modified = False self.variables = {} self._rebuild_parser() # Set up GDEF table from font self.fontfeatures.setGlyphClassesFromFont(self.font) for plugin in self.DEFAULT_PLUGINS: self._load_plugin(plugin) def parseFile(self, filename): """Load a FEE features file. Args: filename: Name of the file to read. """ with open(filename, "r") as f: data = f.read() self.current_file = filename return self.parseString(data) def parseString(self, s): """LoadFEE features information from a string. Args: s: Layout rules in FEE format. """ fee = self.parser(s).feefile() if self.font_modified: warnings.warn("Font was modified") return fee def _rebuild_parser(self): self.parser = parsley.wrapGrammar(self.grammar) def _make_initial_grammar(self): g = parsley.makeGrammar( FeeParser.basegrammar, { "match": re.match, "GlyphSelector": GlyphSelector }, unwrap=True, ) g.globals["parser"] = self g.rule_callRule = callRule g.valid_verbs = ["LoadPlugin"] return g def _load_plugin(self, plugin): if "." not in plugin: plugin = "fontFeatures.feeLib." + plugin mod = importlib.import_module(plugin) if not hasattr(mod, "GRAMMAR"): warnings.warn("Module %s is not a FEE plugin" % plugin) return self._register_plugin(mod) def _register_plugin(self, mod): rules = mod.GRAMMAR verbs = getattr(mod, "VERBS", []) self.grammar_generation = self.grammar_generation + 1 classes = inspect.getmembers(mod, inspect.isclass) self.grammar.valid_verbs.extend(verbs) newgrammar = OMeta.makeGrammar( rules, "Grammar%i" % self.grammar_generation).createParserClass( self.grammar, {}) newgrammar.globals = self.grammar.globals for v in verbs: self.grammar.globals[v] = newgrammar for c in classes: self.plugin_classes[c[0]] = c[1] self._rebuild_parser() def do(self, verb, args): return self.plugin_classes[verb].action(self, *args) def filterResults(self, results): return [x for x in collapse(results) if x]
def test_add(): r1 = Routine(name="One") r2 = Routine(name="Two") f1 = FontFeatures() f2 = FontFeatures() f1.namedClasses["One"] = ["A", "B", "C"] f1.glyphclasses["a"] = "base" f1.addFeature("onex", [r1]) f2.namedClasses["Two"] = ["d", "e", "f"] f2.addFeature("twox", [r2]) f2.glyphclasses["d"] = "mark" f2.anchors["a"] = {"top": (300, 200)} combined = f1 + f2 assert combined.namedClasses["One"] == ["A", "B", "C"] assert combined.namedClasses["Two"] == ["d", "e", "f"] assert "onex" in combined.features assert combined.features["onex"][0].routine == r1 assert "twox" in combined.features assert combined.features["twox"][0].routine == r2 assert combined.glyphclasses["a"] == "base" assert combined.glyphclasses["d"] == "mark" el = combined.toXML() assert etree.tostring(el).decode() == expected
def test_routine_named(): r1 = Routine(name="One") f1 = FontFeatures() f1.addFeature("onex", [r1]) assert f1.routineNamed("One") == r1
class FluxProject: @classmethod def new(klass, fontfile, editor=None): self = FluxProject() self.fontfeatures = FontFeatures() self.fontfile = fontfile self.editor = editor if not self._load_fontfile(): return self.glyphclasses = {} self.glyphactions = {} self.debuggingText = "" self.filename = None if self.fontfile.endswith(".ttf") or self.fontfile.endswith(".otf"): self._load_features_binary() else: self._load_features_source() for groupname, contents in self.font.groups.items(): self.glyphclasses[groupname] = { "type": "manual", "contents": contents } self.fontfeatures.namedClasses.forceput(groupname, tuple(contents)) # Load up the anchors too self._load_anchors() return self def __init__(self, file=None): if not file: return self.filename = file self.xml = etree.parse(file).getroot() dirname = os.path.dirname(file) self.fontfile = os.path.join(dirname, self.xml.find("source").get("file")) self.fontfeatures = FontFeatures() if not self._load_fontfile(): return self.glyphactions = {} self.xmlToFontFeatures() text = self.xml.find("debuggingText") if text is not None: self.debuggingText = text.text else: self.debuggingText = "" self.glyphclasses = {} # Will sync to fontFeatures when building # XXX will it? glyphclasses = self.xml.find("glyphclasses") if glyphclasses is not None: for c in glyphclasses: thisclass = self.glyphclasses[c.get("name")] = {} if c.get("automatic") == "true": thisclass["type"] = "automatic" thisclass["predicates"] = [ dict(p.items()) for p in c.findall("predicate") ] self.fontfeatures.namedClasses[c.get("name")] = tuple( GlyphClassPredicateTester(self).test_all([ GlyphClassPredicate(x) for x in thisclass["predicates"] ])) else: thisclass["type"] = "manual" thisclass["contents"] = [g.text for g in c] self.fontfeatures.namedClasses[c.get("name")] = tuple( [g.text for g in c]) # The font file is the authoritative source of the anchors, so load them # from the font file on load, in case they have changed. self._load_anchors() self._load_glyphactions() def _load_fontfile(self): try: if self.fontfile.endswith(".ufo") or self.fontfile.endswith("tf"): # Single master workflow self.font = Babelfont.open(self.fontfile) self.variations = None else: self.variations = VariableFont(self.fontfile) # We need a "scratch copy" because we will be trashing the # glyph data with our interpolations if len(self.variations.masters.keys()) == 1: self.font = list(self.variations.masters.values())[0] self.variations = None else: firstmaster = self.variations.designspace.sources[0].path if firstmaster: self.font = Babelfont.open(firstmaster) else: # Glyphs, fontlab? self.font = Babelfont.open(self.fontfile) except Exception as e: if self.editor: self.editor.showError("Couldn't open %s: %s" % (self.fontfile, e)) else: raise e return False return True def _load_anchors(self): for g in self.font: for a in g.anchors: if not a.name in self.fontfeatures.anchors: self.fontfeatures.anchors[a.name] = {} self.fontfeatures.anchors[a.name][g.name] = (a.x, a.y) def _load_glyphactions(self): glyphactions = self.xml.find("glyphactions") if not glyphactions: return for xmlaction in glyphactions: g = GlyphAction.fromXML(xmlaction) self.glyphactions[g.glyph] = g g.perform(self.font) def _slotArray(self, el): return [[g.text for g in slot.findall("glyph")] for slot in list(el)] def xmlToFontFeatures(self): routines = {} warnings = [] for xmlroutine in self.xml.find("routines"): if "computed" in xmlroutine.attrib: r = ComputedRoutine.fromXML(xmlroutine) r.project = self elif "divider" in xmlroutine.attrib: r = DividerRoutine.fromXML(xmlroutine) else: r = Routine.fromXML(xmlroutine) routines[r.name] = r self.fontfeatures.routines.append(r) for xmlfeature in self.xml.find("features"): # Temporary until we refactor fontfeatures featurename = xmlfeature.get("name") self.fontfeatures.features[featurename] = [] for r in xmlfeature: routinename = r.get("name") if routinename in routines: self.fontfeatures.addFeature(featurename, [routines[routinename]]) else: warnings.append( "Lost routine %s referenced in feature %s" % (routinename, featurename)) return warnings # We don't do anything with them yet def save(self, filename=None): if not filename: filename = self.filename flux = etree.Element("flux") etree.SubElement(flux, "source").set("file", self.fontfile) etree.SubElement(flux, "debuggingText").text = self.debuggingText glyphclasses = etree.SubElement(flux, "glyphclasses") for k, v in self.glyphclasses.items(): self.serializeGlyphClass(glyphclasses, k, v) # Plugins # Features features = etree.SubElement(flux, "features") for k, v in self.fontfeatures.features.items(): f = etree.SubElement(features, "feature") f.set("name", k) for routine in v: etree.SubElement(f, "routine").set("name", routine.name) # Routines routines = etree.SubElement(flux, "routines") for r in self.fontfeatures.routines: routines.append(r.toXML()) # Glyph actions if self.glyphactions: f = etree.SubElement(flux, "glyphactions") for ga in self.glyphactions.values(): f.append(ga.toXML()) et = etree.ElementTree(flux) with open(filename, "wb") as out: et.write(out, pretty_print=True) def serializeGlyphClass(self, element, name, value): c = etree.SubElement(element, "class") c.set("name", name) if value["type"] == "automatic": c.set("automatic", "true") for pred in value["predicates"]: pred_xml = etree.SubElement(c, "predicate") for k, v in pred.items(): pred_xml.set(k, v) else: c.set("automatic", "false") for glyph in value["contents"]: etree.SubElement(c, "glyph").text = glyph return c def saveFEA(self, filename): try: asfea = self.fontfeatures.asFea() with open(filename, "w") as out: out.write(asfea) return None except Exception as e: return str(e) def loadFEA(self, filename): unparsed = FeaUnparser(open(filename, "r")) self.fontfeatures = unparsed.ff def _load_features_binary(self): tt = TTFont(self.fontfile) self.fontfeatures = unparse(tt) print(self.fontfeatures.features) def _load_features_source(self): if self.font.features and self.font.features.text: try: unparsed = FeaUnparser(self.font.features.text) self.fontfeatures = unparsed.ff except Exception as e: print("Could not load feature file: %s" % e) def saveOTF(self, filename): try: self.font.save(filename) ttfont = TTFont(filename) featurefile = UnicodeIO(self.fontfeatures.asFea()) builder = Builder(ttfont, featurefile) catmap = {"base": 1, "ligature": 2, "mark": 3, "component": 4} for g in self.font: if g.category in catmap: builder.setGlyphClass_(None, g.name, catmap[g.category]) builder.build() ttfont.save(filename) except Exception as e: print(e) return str(e)