コード例 #1
0
    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
コード例 #2
0
 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())
コード例 #3
0
 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)
コード例 #4
0
 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")
コード例 #5
0
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];",
        )
コード例 #6
0
ファイル: __init__.py プロジェクト: m4rc1e/fontFeatures
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
コード例 #7
0
    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()
コード例 #8
0
ファイル: __init__.py プロジェクト: simoncozens/fontFeatures
    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
コード例 #9
0
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
コード例 #10
0
    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';")
コード例 #11
0
 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")
コード例 #12
0
 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"))
コード例 #13
0
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)}
コード例 #14
0
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
コード例 #15
0
ファイル: test_model.py プロジェクト: simoncozens/flux
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)
コード例 #16
0
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"/>
コード例 #17
0
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;",
        )
コード例 #18
0
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
コード例 #19
0
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
コード例 #20
0
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]
コード例 #21
0
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
コード例 #22
0
def test_routine_named():
    r1 = Routine(name="One")
    f1 = FontFeatures()
    f1.addFeature("onex", [r1])
    assert f1.routineNamed("One") == r1
コード例 #23
0
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)