Writing your own plugins

The power of FEZ comes from its extensibility, and that means the ability to write your own plugins. A guiding principle of writing feature code in the FEZ language is that instead of telling the font specifically what to do (“move these two glyphs fifty units apart”), as much as possible you query the font for information and then automatically work out the right thing to do. This is done through writing “rules which write rules” in Python modules.

This takes a bit of work, but the end result should be that you have sets of rules that you can carry from font to font without needing to rewrite them.

Let’s take a look at a couple of sample plugins to see how they’re constructed.

Grammars

A fontFeatures plugin comes in two parts: the grammar and the code. Grammars explain what kinds of arguments, and how many arguments, the verbs in the plugin expect. Grammars are written in the notation used by Lark so you should start by reading its documentation (or, if you’re like me, by picking it up by looking at existing examples).

Our first plugin is going to be called CopyAnchors, and it will copy all anchors from glyphs in one glyph class to glyphs in another glyph class. What do we want the call to this verb to look like? Probably something like CopyAnchors @gc1 @gc2;. So the arguments to CopyAnchors will be two glyph selectors, separated by whitespace. Here’s how we will start our plugin:

PARSEOPTS = dict(use_helpers=True)

GRAMMAR = """
?start: action
action: glyphselector glyphselector
"""
VERBS = ["CopyAnchors"]

A GRAMMAR contains a complete Lark grammar. PARSEOPTS must be defined; its function is to describe how the grammar is to be parsed. If use_helpers is True, then the grammar defined in __init__.py as HELPERS will be prepended to your grammar. This grammar defines many useful elements, such as glyphselector, which we reuse.

By convention, your grammar should always start with the line ?start: action, and then work backwards defining action. In Lark, tokens (terminals) are in all capital letters, while rules are in lowercase. One of the rules imported by use_helpers is the handy %ignore WS, which inserts an implied optional whitespace between the two glyph selectors.

Now we have the grammar, and also defined the VERBS that this module provides (don’t forget that bit!), we can turn to implementation.

Verb implementations

When a verb is “called” by a FEZ file, first the arguments to the verb are parsed according to the grammar you’ve provided. Then, the abstract syntax tree (AST) that is generated is transformed by our subclass of lark.Transformer, FEZVerb.

FEZVerb gives us access to the font object, the fontFeatures object, and other stuff we might need, through self.parser. It is also possible to only subclass lark.Transformer for low level access to the AST, but you should only very seldom want to do this.

Let’s set up our class, and then we’ll talk about the content of those arguments:

class CopyAnchors(FEZVerb):
    def action(self, args):
        (fromglyphs, toglyphs) = args

Because we’ve subclassed FEZVerb, we receive the benefits of this class; both of our glyphselectors are transformed for us into fez.GlyphSelector objects which can be resolved. Note that we, due to our grammar, know that args will contain exactly two glyphselector’s. A grammar, however, could instead say glyphselector*, receiving any number, and making args return a variable number of them.

This is a common pattern you will see in a lot of FEZ plugins:

fromglyphs = fromglyphs.resolve(self.parser.fontfeatures, self.parser.font)
toglyphs = toglyphs.resolve(self.parser.fontfeatures, self.parser.font)

(It needs the fontfeatures object so that it can resolve named class references; it needs the font object for the list of glyphs in the font.) If we were going to create the toglyphs we would tell resolve that the glyphs don’t need to exist yet:

toglyphs = toglyphs.resolve(parser.fontfeatures, parser.font, mustExist=False)

Now we have a list of from-glyphs, a list of to-glyphs, a fontFeatures object, and the rest is easy: fontFeatures stores a dictionary of anchors for each glyph, so we just copy them across:

for (f, t) in zip(fromglyphs,toglyphs):
  parser.fontfeatures.anchors[t] = parser.fontfeatures.anchors[f]

Normally a FEZ plugin would return a list of Routine objects, but we’re not creating any rules, so we just return an empty list:

return []

And we’re done. Now we’ll look at a more involved plugin - one which inspects the glyphs themselves, and emits some rules in response.

The IMatra Plugin

The IMatra plugin, described above, comes with fontFeatures. It matches the “i” vowel sign in Devanagari to bases of the appropriate width. Let’s see how it works. First, we define the grammar. This is similar to the one above - just three glyph selectors - but we use some constant symbols as a bit of “syntactic sugar”. Note that we only assign the glyph selectors to variables (and put them into the return tuple), and not the syntactic sugar:

PARSEOPTS = dict(use_helpers=True)

GRAMMAR = """
?start: action
//      bases             matra              matras
action: glyphselector ":" glyphselector "->" glyphselector
"""

VERBS = ["IMatra"]

We’re going to need to know how wide all of our matras and bases are, and the fontFeatures way to do this is to use the get_glyph_metrics function from the glyphtools library:

import fontFeatures
from glyphtools import get_glyph_metrics

Now we’re ready to write our action method, which will start with the usual resolving of glyph selectors into glyph name lists:

class IMatra:
    def action(self, args):
        (bases, matra, matras) = args
        bases = bases.resolve(self.parser.fontfeatures, self.parser.font)
        matra = matra.resolve(self.parser.fontfeatures, self.parser.font)
        matras = matras.resolve(self.parser.fontfeatures, self.parser.font)

Let’s think what we need to do now. We have a list of matras, with different “overhangs” (negative RSBs). For each matra, we want a list of bases which this matra best fits, and then we emit a set of substitution rules. First, we’ll create a dictionary to hold our “best fits” bases for each matra, and arrange the list of matras into a list of (glyphname, overhang) tuples:

matras2bases = {}
matrasAndOverhangs = [
    (m, -get_glyph_metrics(parser.font, m)["rsb"]) for m in matras
]

Now we loop over the bases, and find the matra which has the smallest difference between the base width and the matra overhang:

for b in bases:
    w = get_glyph_metrics(parser.font, b)["width"]
    (bestMatra, _) = min(matrasAndOverhangs, key=lambda s: abs(s[1] - w))

And store this in the dictionary:

if not bestMatra in matras2bases:
    matras2bases[bestMatra] = []
matras2bases[bestMatra].append(b)

When the loop is finished and we have processed all the bases, we can now turn the dictionary into a list of substitution rules. We want to emit rules with this kind of form:

Substitute { iMatra-deva } @bases_3 -> iMatra-deva.3;

i.e. the bases are the post-context for the matra substitution. This is how we do it:

rv = []
for bestMatra, basesForThisMatra in matras2bases.items():
    rv.append(
        fontFeatures.Substitution(
            [matra],
            postcontext=[basesForThisMatra],
            replacement=[[bestMatra]]
        )
    )

You may have noticed that bestMatra goes into a list of lists, but matra does not. It’s good to think through why this is, because it will help you understand fontFeatures rules. Resolving a glyph selector always returns a list. So resolving the glyphselector matra returned a list of glyph names, although probably that list had only one element. A substitution or positioning rule defines its input as a list of glyph stream positions, each of which is a list of glyph names that can match at this position. In essence, every element of the glyph stream position list must be expressed as a “glyph class”, even if it is a one-element class. So the input will be:

[
  # Glyph position 1:
  matra # We got a one-element list from `.resolve` e.g. ["iMatra-deva"]
]

For the postcontext, we want to match a glyph class. basesForThisMatra is a list, so this is also fine:

[
  # Glyph position 1:
  basesForThisMatra # e.g. [ "ga-deva", "gha-deva", ... ]
]

We want the replacement parameter to look the same: for each position in the replacement glyph stream, a list of glyphs to be substituted. (This is because Substitution also supports “alternate” substitutions, in which glyph position 1 will substitute multiple glyphs, and also because regularity is good and leads to fewer surprises.) However, bestMatra is not a list, but a single glyph; this is why we have to make it into one:

[
  # Glyph position 1:
  [ "iMatra-deva.3" ]
]

Finally, all that remains to do is wrap up our list of substitution rules into a routine, and return it:

return [fontFeatures.Routine(rules=rv)]

In more complex scenarios, you may find yourself enumerating all the combinations of glyphs within a sequence (the itertools.product function is useful for this); checking whether a given set of glyphs causes a collision when positioned (the fontFeatures.jankyPOS positioner and the collidoscope library may help you); inspecting the paths and doing some sums based on them (fontFeatures.pathUtils and the beziers library are good for this).

To gain more understanding of what this might look like, try working through the code of the fez.IfCollides and fez.BariYe plugins.

Defining multiple verbs

Up until now, we’ve defined extensions which have only one verb. An example of this can be seen in the default Substitute.py extension. GRAMMAR, in this case, applies to the entire extension. Then, you can define:

Substitute_GRAMMAR = """
?start: action
action: normal_action | contextual_action
"""

ReverseSubstitute_GRAMMAR = """
?start: action
action: normal_action
"""

VERBS = ["Substitute", "ReverseSubstitute"]

Notes on curly brackets

In order to prevent grammars from ever being ambiguous, { and } are not allowed in your grammar with any meaning other than grouping other verbs. So, extensions like Feature and Routine may use them, but they must not be used unless it’s to group other verbs.

If you do need to group, note the different way that such grammars are parsed. If you want to define arguments before and after the braces, you must define beforebrace and afterbrace grammars; see Feature.py and Routine.py in the fez directory for examples.

Generally, it’s recommended to avoid curly brackets, as they should normally not be useful for user extensions now that Feature and Routine already exist.