What it is?

Defcon representations are a powerful way to handle cached visual (and non-visual) representations of font data. They are easy to use and very efficient. They require a bit of setup, but it’s totally worth it.

RoboFont frequently uses representations under its hood: every time you select some points in the glyph editor, when you activate kerning in space center, when you draw a glyph with merz. In this article we’ll see how to create our own representations.

How can I use it?

Defcon representation follow the Factory Method Pattern. RoboFont will hand the representation data to us through a factory function called every time we need that specific representation. Pratically we need to follow these steps:

  1. write a function with a predermined interface, the so-called factory function
  2. register the factory function on a specific defcon type (glyph, kerning, font…)
  3. every time we request our representation of an object, three outcomes are possible:
    1. it’s the first request, so the factory function will be executed, the output cached and then handed to us
    2. the object has not changed since the last time we requested the representation, so we will receive the cached data and save expensive computation
    3. the object has changed, so the cache has been invalidated and the representation will be computed again through the factory function

A practical example

It’s a lot to unpack and figure out, so let’s go through the previous steps once again with a practical example. Our goal is to create a glyph representation that computes the list of the bounding boxes of contours and components. The representation factory function will look like this:

def boundingBoxesFactory(glyph):
    boundingBoxes = []
    for eachContour in glyph:
        boundingBoxes.append(("Contour", eachContour.bounds))
    for eachComponent in glyph.components:
        boundingBoxes.append(("Component", eachComponent.bounds))
    return boundingBoxes

Fun fact, internally the .bounds property uses a representation factory!

We then need to register it:

from defcon import Glyph, registerRepresentationFactory
registerRepresentationFactory(Glyph, "boundingBoxesRepresentation", boundingBoxesFactory)

Now we can use it with a glyph from an opened font in RoboFont. To test the caching performance, we can use the time module from the Python standard library. Just call the .getRepresentation() method over the glyph and print the output:

from time import time

font = CurrentFont()

start = time()
boundingBoxes = font["Aacute"].getRepresentation("boundingBoxesRepresentation")
end = time()

print(f"elapsed: {end - start}")
print(boundingBoxes)
# >>> elapsed: 0.0012180805206298828
# >>> [('Component', (20, 0, 559, 680)), ('Component', (158, 720, 421, 889))]

If we call again the representation without touching the glyph data, the output will be the same but the execution way faster!

# >>> elapsed: 0.00012874603271484375
# >>> [('Component', (20, 0, 559, 680)), ('Component', (158, 720, 421, 889))]

It might seem absurd to look into time differences of 1/100 of a second, but it’s an order of magnitude with just two elements! Think about the difference that might occur with glyphs formed of many elements or complex outlines.

Here follows the complete example:

from time import time

from defcon import Glyph, registerRepresentationFactory
from mojo.roboFont import CurrentFont


def boundingBoxesFactory(glyph):
    boundingBoxes = []
    for eachContour in glyph:
        boundingBoxes.append(("Contour", eachContour.bounds))
    for eachComponent in glyph.components:
        boundingBoxes.append(("Component", eachComponent.bounds))
    return boundingBoxes


if __name__ == "__main__":
    registerRepresentationFactory(Glyph, "boundingBoxesRepresentation", boundingBoxesFactory)

    font = CurrentFont()

    start = time()
    boundingBoxes = font["Aacute"].getRepresentation("boundingBoxesRepresentation")
    end = time()
    print(end - start)
    print(boundingBoxes)

    start = time()
    boundingBoxes = font["Aacute"].getRepresentation("boundingBoxesRepresentation")
    end = time()
    print(end - start)
    print(boundingBoxes)

Destructive Notifications

Defcon representations are quite flexible, as they allow to specify which events should invalidate the representation cache. As we mentioned in the example, when an object is changed, the representation factory is invalidated. By default, any notification posted by the object with no exceptions. In some situations, this might lead to unnecessary computation. Let’s say we have a factory computing some value based on glyph width. If the Unicode value of such glyph is changed, we have no reason to invalidate the representation cache. To do so, we need to list Glyph.WidthChanged as a destructive notification:

registerRepresentationFactory(
    Glyph, "WidthRepresentation",
    widthRepresentationFactory,
    destructiveNotifications=["Glyph.WidthChanged"]
)

The destructiveNotifications argument can only process events of the same object where you are registering the representation. If you are defining a Kerning representation, only Kerning.changed, Kerning.PairSet, Kerning.PairDeleted (and so on…) can be used.

Last edited on 01/09/2021