Wires & Books


Lizzie Brooks: a font for telling certain kinds of truth

Recently at a guerilla publishing meetup, one of the attendees mentioned she had wanted to make an alethiometer font but had run into technical troubles. I replied that Iʼd had success making an icon font for one of my projects, and offered to explain how to do it. So, hereʼs a tutorial.

Prerequisites

  1. You will need to be familiar with installing and using command-line tools on your operating system of choice. You will also need a source code editor; there are many good options available. If you donʼt already have a favorite, Visual Studio Code is available for many operating systems and relatively simple to install.
  2. You will need to have Node.js and Python 3 installed, and to be familiar with installing libraries for these languages with npm and pip.

Iʼm aware that these prerequisites may be demanding for some readers. Unfortunately, I donʼt have the time right now to build an easy-to-use app for this. Youʼll have to use the same low level tooling I used.

How a Font Works

You are probably aware by now that a computer fundamentally operates on numbers, and in order to make it operate with letters, humans have devised various ways to assign numbers to the different letters. So, in Unicode, the number (or "code point") 82 (in decimal, 52 in hexadecimal) is assigned to the Latin capital letter R.

It gets more complicated quickly as you get away from the characters used in English; the code point 225 (or E1 in hexadecimal) is assigned to á, but you can also form the letter á by combining the code point 97 (the Latin letter a) and the codepoint 769 (which is assigned to something called "combining acute accent"). So there are sometimes multiple code points used to make up a character, and sometimes multiple ways to represent the same character. But all of that is just about how we assign numbers to the idea of a character. If you want to get an character drawn in a way that you can see, you need a font.

A font is basically a set of rules for how to draw certain characters. On my computer, the font Helvetica has rules for drawing 2,252 different characters, mostly from the Latin, Greek, and Cyrillic writing systems. (This sounds like a lot, but the latest version of Unicode defines 144,762 different characters; Helvetica only covers 1.5% of that space.) Because the font has rules for drawing characters, not just embedded pictures of characters, the font can adjust to different text sizes, stretch itself horizontally or vertically, and so on. These rules can get incredibly complex, because human writing systems are incredibly complex. Luckily, for an icon font, we don't need most of that complexity.

A note on licenses

A font (as a set of rules for drawing various characters) is a creative work, and like other kinds of creative works, can be protected by copyright. (In the US, the actual shapes of letters are not copyrightable, but the instructions for drawing them are.) As the creator of a font, you are allowed to choose any terms you like for sharing it. Many fonts are shared under the terms of the SIL Open Font License; many more are shared only in exchange for money.

However, since we are making an icon font, it's important to remember that icons are also copyrightable. If you are not the copyright holder of the icons, you might not be legally allowed to combine them into an icon font; even if you are, there may be restrictions on what terms you can use to share the resulting font. This will be important later.

The Tutorial Proper

We are creating an icon font to represent alethiometer symbols. The alethiometer has thirty-six different symbols, arranged in a circle. The order of the symbols seems to vary from depiction to depiction; I have chosen the order in this poster, which I am told is associated with the original novel (rather than the subsequent film or TV show).

Step One: Create Glyphs

We need glyphs for these thirty-six symbols. A font glyph is a set of instructions for moving a virtual pen along either a straight line or a mathematically-defined curve. That is, it's basically a vector graphic.

However, not all vector graphics are suitable for font glyphs. This is especially true when using software to automatically vectorize an image.

Here is a crude drawing of a ring shape. The ring is in black, on a white background. This should work as a font glyph, right? After all, fonts already have plenty of circles in them.

It turns out that there's two ways to draw that ring shape. One way is to draw a black circle, and then draw a smaller white circle on top of the black circle, as a second layer. This approach can work fine for most vector graphics use cases, and it's an approach that vectorization software often uses, but fonts cannot work this way.

In order for this ring shape to work in a font, it has to be drawn as a single layer: a circle with a hole in the middle. Likewise, to make my alethiometer font, I needed the symbols to all be drawn as a single layer. And, since this is an icon font, I needed the symbol drawings to be licensed under terms that let me use them. I couldn't find any existing drawings of the alethiometer symbols that would be usable.

Step One-Half: Learn to Draw

No.

Step One-Half: Cheat

Game-icons.net provides a few thousand single-layer icons under the CC-BY-3.0 license. This license lets me modify and reuse the icons, provided I give attribution; sounds simple enough.

As you might expect from the name, the icons on this site are intended primarily for fantasy gaming. As a result, I've had to make a few substitutions:

  • The alpha-and-omega symbol is depicted as just an omega.
  • The madonna symbol has been replaced with the Mona Lisa, which is kind of similar, if you think about it.
  • The compasses have been replaced with a sextant, which is sort of a similar device.
  • And the lute has been replaced with a guitar.

Because of these substitutions, I've named the font “Lizzie Brooks”. This is a clever reference to the source material.

The icons cannot be used directly as downloaded from the website; this has to do with some extra cruft the website puts in. Instead, I extracted the SVG path definition for each icon and plugged it into the following template.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;" fill="#000000">
    <path d="..."></path>
</svg>

Step Two: Generate a Bad Font

We're using a program called fantasticon to convert these SVG files into a TTF (TrueType) font. This program uses Node.js, which is why I listed it in the prerequisites. It's controlled using a configuration file that is Javascript source code.

module.exports = {
    name: "lizziebrooks",
    inputDir: './icons', // (required)
    outputDir: './generated', // (required)
    fontTypes: ['ttf'],
    assetTypes: ['json',],
    fontsUrl: '/static/fonts',
    normalize: true,
    descent: 50,
    getIconId: ({
        basename, // `string` - Example: 'foo';
        relativeDirPath, // `string` - Example: 'sub/dir/foo.svg'
        absoluteFilePath, // `string` - Example: '/var/icons/sub/dir/foo.svg'
        relativeFilePath, // `string` - Example: 'foo.svg'
        index // `number` - Example: `0`
    }) => basename.split('-')[1],
};

Save this config file as fantasticonrc.js, make icons and generated subdirectories in the same directory as the file, and place all your icons in that icons subdirectory. They should be named things like 01-hourglass.svg; that is, a number, a hyphen, and a symbol name. The number will be used for the icon order in the font.

Run npx fantasticon in the directory with the config file. This should produce an output in generated called lizziebrooks.ttf.

Unfortunately, this generated font is not actually compliant with the TrueType specification, and many applications will have problems with it. We need to improve it.

Step Three: Generate a Better Font

This step uses a Python library called fontTools. In order to make our font more compatible, we need to do the following things:

  1. Remove cruft: fantasticon generated a font with the symbols we need, but it has things in there that we don't need, because fantasticon is intended for web developers.
  2. Add the mandatory glyphs: the TrueType specification requires that the first four glyphs in a font meet certain criteria. We'll add them at the beginning of the generated font, so that the font will end up having 40 glyphs in total: 4 mandatory but invisible glyphs, and 36 symbols.
  3. Set the "left-side bearing" for our symbols: fantasticon was designed for squareish symbols, but our symbols have varying widths. We want the symbols that get drawn to be properly aligned, so we have to tweak some parameters of the font.
  4. Set appropriate name and description strings into the font. (For example, since all the icons are CC-BY-3.0 licensed, the font is also CC-BY-3.0 licensed. We'll embed that license name into the font itself.)

I wrote a Python program that uses fontTools to accomplish these four things:

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2021 Rose Davidson <rose@metaclassical.com>
#
# SPDX-License-Identifier: CC-BY-3.0

import enum
import io

from fontTools import ttLib
from fontTools.ttLib.tables._g_l_y_f import Glyph
from fontTools.subset import (
    Subsetter,
    Options as SubsetterOptions,
    load_font as subset_load_font,
    save_font as subset_save_font,
)


def check(truth, errmsg):
    if not truth:
        raise Exception(errmsg)


class NameId(enum.IntEnum):
    COPYRIGHT = 0
    FAMILY = 1
    SUBFAMILY = 2
    UNIQUE_ID = 3
    FULL_NAME = 4
    VERSION = 5
    POSTSCRIPT_NAME = 6
    MANUFACTURER = 8
    DESIGNER = 9
    DESCRIPTION = 10
    DESIGNER_URL = 12
    LICENSE = 13
    LICENSE_URL = 14


class PlatformId(enum.IntEnum):
    UNICODE = 0
    MACINTOSH = 1
    WINDOWS = 3


WINDOWS_ENGLISH_IDS = PlatformId.WINDOWS, 1, 0x409
MAC_ROMAN_IDS = PlatformId.MACINTOSH, 0, 0

# https://docs.microsoft.com/en-us/typography/opentype/spec/name
# https://docs.microsoft.com/en-us/typography/opentype/spec/namesmp
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html
# https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-chapter08
FONT_NAMES = [
    ("Copyright 2021 Rose Davidson", NameId.COPYRIGHT),
    ("Lizzie Brooks", NameId.FAMILY),
    ("Regular", NameId.SUBFAMILY),
    ("Lizzie Brooks Regular v1.0", NameId.UNIQUE_ID),
    ("Lizzie Brooks Regular", NameId.FULL_NAME),
    ("Version 1.0", NameId.VERSION),
    ("LizzieBrooks-Regular", NameId.POSTSCRIPT_NAME),
    ("Straylight Press", NameId.MANUFACTURER),
    ("Rose Davidson", NameId.DESIGNER),
    ("https://wiresandbooks.com", NameId.DESIGNER_URL),
    ("CC-BY-3.0", NameId.LICENSE),
    ("https://creativecommons.org/licenses/by/3.0/", NameId.LICENSE_URL),
]


def subset(fontfile):
    options = SubsetterOptions(glyph_names=True)
    # https://docs.microsoft.com/en-us/typography/opentype/spec/gsub
    options.drop_tables.append("GSUB")
    # https://docs.microsoft.com/en-us/typography/opentype/spec/features_ko#tag-liga
    options.layout_features.remove("liga")

    subsetter = Subsetter(options=options)
    unicodes = list(range(0xF101, 0xF124 + 1))
    font = subset_load_font(fontfile, options)

    subsetter.populate(unicodes=unicodes)
    subsetter.subset(font)

    out = io.BytesIO()
    subset_save_font(font, out, options)
    return out.getvalue()


def load_font(fontbytes):
    return ttLib.TTFont(
        io.BytesIO(fontbytes), recalcBBoxes=False, recalcTimestamp=False, lazy=True
    )


def fully_eager_load_font(fontbytes):
    font = ttLib.TTFont(io.BytesIO(fontbytes), lazy=False)
    for tag in font.keys():
        table = font[tag]
        # https://github.com/fonttools/fonttools/issues/2060
        # cmap has subtables that must be loaded
        if tag == "cmap":
            for subtable in table.tables:
                subtable.cmap = subtable.cmap
        font[tag] = table
    return font


def save_font(font):
    out = io.BytesIO()
    font.save(out)
    return out.getvalue()


def set_glyph_order(font, glyphorder):
    font.setGlyphOrder(glyphorder)
    # need to touch this if we lazy-loaded
    font["post"] = font["post"]


def fixup(fontbytes):
    # 1. rename glyph00000 (no actual name in file) to ".notdef"
    font = load_font(fontbytes)
    glyphorder = font.getGlyphOrder()
    check(glyphorder[0] == "glyph00000", "Expected glyph00000 to be first")
    # have to reload, because of how the internal tables work
    font = load_font(fontbytes)
    glyphorder[0] = ".notdef"
    set_glyph_order(font, glyphorder)
    font["post"].extraNames.remove("")
    fontbytes = save_font(font)
    # 2a. add mandatory glyphs
    # https://docs.microsoft.com/en-us/typography/opentype/otspec150/recom#first-four-glyphs-in-fonts
    font = fully_eager_load_font(fontbytes)
    font["glyf"][".null"] = Glyph()
    font["glyf"]["nonmarkingreturn"] = Glyph()
    font["glyf"]["space"] = Glyph()
    # 2b. add horizontal metrics for mandatory glyphs
    font["hmtx"][".null"] = (0, 0)
    font["hmtx"]["nonmarkingreturn"] = (font["head"].unitsPerEm, 0)
    font["hmtx"]["space"] = (font["head"].unitsPerEm, 0)
    # 2c. fix codepoints for mandatory glyphs
    for subtable in font["cmap"].tables:
        subtable.cmap[0] = ".null"
        subtable.cmap[0x0D] = "nonmarkingreturn"  # carriage return
        subtable.cmap[0x20] = "space"  # space
        subtable.cmap[0xA0] = "space"  # non-breaking space
    # 2d. Move mandatory glyphs to appropriate places in glyphorder
    glyphorder = font.getGlyphOrder()[:]
    mandatory = [".notdef", ".null", "nonmarkingreturn", "space"]
    for glyphname in mandatory:
        glyphorder.remove(glyphname)
    discretionary = glyphorder[:]
    glyphorder = mandatory + discretionary
    set_glyph_order(font, glyphorder)
    font["glyf"].setGlyphOrder(glyphorder)
    fontbytes = save_font(font)
    # 3. Set proper left-side bearings for the discretionary glyphs
    font = fully_eager_load_font(fontbytes)
    for glyphname in discretionary:
        current_mtx = font["hmtx"][glyphname]
        font["hmtx"][glyphname] = (current_mtx[0], font["glyf"][glyphname].xMin)
    fontbytes = save_font(font)
    # 4. Set appropriate string names
    font = load_font(fontbytes)
    names = font["name"]
    for plat_id, enc_id, lang_id in (WINDOWS_ENGLISH_IDS, MAC_ROMAN_IDS):
        names.removeNames(platformID=plat_id, platEncID=enc_id, langID=lang_id)
        for text, name_id in FONT_NAMES:
            names.setName(text, name_id, plat_id, enc_id, lang_id)

    return save_font(font)


def main():
    fontfile = "./generated/lizziebrooks.ttf"
    subsetted = subset(fontfile)
    fixed = fixup(subsetted)
    with open("./lizziebrooks.ttf", "wb") as out:
        out.write(fixed)


if __name__ == "__main__":
    main()

Save this file as postprocess.py in the same directory as before, run python postprocess.py, and you should see a lizziebrooks.ttf output in your main directory. This is our final font.

Step Four: Use the Font.

The process for using this font will vary depending on your typesetting software. Some programs, such as Affinity Publisher, let you pick from a list of glyphs in a given font.

Other programs may require you to enter the specific Unicode codepoint. The icons are assigned to codepoints in the Unicode Private Use Area, which ensures our characters won't conflict with ordinary fonts. The icons are located at codepoints F101 through F124 (in decimal, that's 61697 through 61732).

Here's how we might use it in LaTeX. You'll need a LaTeX engine that supports modern fonts; I've used XeTeX but LuaTeX will also work.

\documentclass{article}
\usepackage{tikz}
\usepackage{fontspec}

\newfontface\lizzie{lizziebrooks.ttf}

% First alethiometer symbol is at codepoint 61697
\newcommand{\alethiometerHourglass}{\symbol{61697}}
\newcommand{\alethiometerSun}{\symbol{61698}}
\newcommand{\alethiometerAlphaomega}{\symbol{61699}}
\newcommand{\alethiometerMarionette}{\symbol{61700}}
\newcommand{\alethiometerSerpent}{\symbol{61701}}
\newcommand{\alethiometerCauldron}{\symbol{61702}}
\newcommand{\alethiometerAnchor}{\symbol{61703}}
\newcommand{\alethiometerAngel}{\symbol{61704}}
\newcommand{\alethiometerHelmet}{\symbol{61705}}
\newcommand{\alethiometerBeehive}{\symbol{61706}}
\newcommand{\alethiometerMoon}{\symbol{61707}}
\newcommand{\alethiometerMadonna}{\symbol{61708}}
\newcommand{\alethiometerApple}{\symbol{61709}}
\newcommand{\alethiometerBird}{\symbol{61710}}
\newcommand{\alethiometerBread}{\symbol{61711}}
\newcommand{\alethiometerAnt}{\symbol{61712}}
\newcommand{\alethiometerBull}{\symbol{61713}}
\newcommand{\alethiometerCandle}{\symbol{61714}}
\newcommand{\alethiometerCornucopia}{\symbol{61715}}
\newcommand{\alethiometerChameleon}{\symbol{61716}}
\newcommand{\alethiometerThunderbolt}{\symbol{61717}}
\newcommand{\alethiometerDolphin}{\symbol{61718}}
\newcommand{\alethiometerWalledgarden}{\symbol{61719}}
\newcommand{\alethiometerGlobe}{\symbol{61720}}
\newcommand{\alethiometerSword}{\symbol{61721}}
\newcommand{\alethiometerGriffin}{\symbol{61722}}
\newcommand{\alethiometerHorse}{\symbol{61723}}
\newcommand{\alethiometerCamel}{\symbol{61724}}
\newcommand{\alethiometerElephant}{\symbol{61725}}
\newcommand{\alethiometerCrocodile}{\symbol{61726}}
\newcommand{\alethiometerBaby}{\symbol{61727}}
\newcommand{\alethiometerCompass}{\symbol{61728}}
\newcommand{\alethiometerLute}{\symbol{61729}}
\newcommand{\alethiometerTree}{\symbol{61730}}
\newcommand{\alethiometerWildman}{\symbol{61731}}
\newcommand{\alethiometerOwl}{\symbol{61732}}

\begin{document}
% See chapter 9
Inquiry regarding Mr. de Ruyter: {\lizzie{}\alethiometerSerpent{}\alethiometerCauldron{}\alethiometerBeehive{}}

\vspace{4em}

\begin{tikzpicture}[line cap=rect,line width=3pt]
    \draw (0,0) circle [radius=4cm];
    % First alethiometer symbol is at codepoint 61697 and should be at angle 90; angles _decrease_ as we go clockwise, for some reason
    \foreach \angle / \codepoint in {
            90/61697, 80/61698, 70/61699, 60/61700, 50/61701, 40/61702, 30/61703, 20/61704, 10/61705, 0/61706, -10/61707, -20/61708, -30/61709,
            -40/61710, -50/61711, -60/61712, -70/61713, -80/61714, -90/61715, -100/61716, -110/61717, -120/61718, -130/61719, -140/61720,
            -150/61721, -160/61722, -170/61723, -180/61724, -190/61725, -200/61726, -210/61727, -220/61728, -230/61729, -240/61730, -250/61731, -260/61732
        }
        {
            \node[font=\large,rotate=(\angle - 90)] at (\angle:3.6cm) {\lizzie{}\symbol{\codepoint}};
        }
\end{tikzpicture}

\end{document}


Rendering this file produces a PDF with two things in it: a bit of text describing one of Lyra's experiments with the alethiometer, and a depiction of the alethiometer itself. (The alethiometer needles are not currently rendered; consider it an exercise for the reader, if you like.)

Potential Improvements

  • The fontTools library is actually capable of reading SVG files directly, and in theory we could skip the fantasticon processing step. However, I haven't quite figured out how to use fontTools to convert SVGs into appropriate TrueType glyphs, so for now, the two stage process is necessary.
  • We could add Latin characters from another font with a compatible license and then set up ligatures so that typing 'apple' would first, letter-by-letter, display the Latin characters, and then as soon as the symbol name was complete, convert into the symbol. This might be simpler to use in some programs, compared to glyph pickers or Unicode codepoints.

The Final Font

Download it here