Rod

Introduction to accessing fonts in Rust with Skrifa

Learn to generate an Android Vector Drawable from a variable icon font.

Logistics

It is assumed that all code is cloned into the same directory and that cargo new is run in that directory.

At times only partial commandline output is given to reduce noise.

Install Rust

See https://www.rust-lang.org/tools/install

If you haven’t written Rust at all so far take the time to complete at least one of:

  1. Microsoft Rust first steps
    • 0.5 to 1.0 days
  2. Google Comprehensive Rust
    • 4+ days, can be consumed in small chunks

Get an editor

VSCode with rust-analyzer is a good default if you don’t already have a favorite.

Get some icon fonts

Clone https://github.com/google/material-design-icons

It’s big, give it a minute :) In `` you’ll find the variable fonts that contain all variants of all icons shown at https://fonts.google.com/icons. When Style is set to “Material Symbols” The “Customize” options there are actually manipulating the axes of the variable font:

Variable axes fill, weight, grade, optical size

Generate a Vector Drawable

Create a CLI tool

# Create a new binary project
$ cargo new font2vd
     Created binary (application) `font2vd` package
2 directories, 2 files
$ cd font2vd
$ cargo run
Hello, world!

Subsequent instructions assume you are in the root of font2vd

Error handling

Let’s take this slightly more seriously than font103 and try to avoid unwrap and expect for the most part.

Make sure you can cargo run before proceeding.

Stuck? See 1-errortype.rs.

Command line args

Add a dependency on clap (cargo add clap --features derive) and setup an args structure with two arguments:

  1. A string that is a position in designspace
    • We’ll be using this to construct a Location using AxisCollection::location
    • A comma-separated list of axis tag : value pairs could work on the command line
    • Since variable fonts have a well defined default location you could mark it as allowed to default by adding , default_value = ""
      • E.g. #[arg(short, long, default_value = "")]
  2. A path to an icon font
    • We’ll be using this to construct a FontRef

Try cargo run -- --help. It should print help about your commandline arguments.

What’s with the -- --help? - the stuff before the -- is arguments to Cargo, after is arguments to your program. See the cargo run docs.

Stuck? See 2-args.rs.

Load the font with error handling

Load the font specified in your args structure into memory, perhaps using std::fs::read. If that fails you’ll get a std::io::Error. Alas, our main returns VectorDrawableError. Add a variant to VectorDrawableEror that can hold an io error. It should look like the Disconnect variant shown in the thiserror example. Use Result::map_err to convert the error type and the error propagation operator ? to handle the error.

Try running your program and pointing it at a path you can’t read. It should print something akin to:

$ cargo run -- --pos wght:0 --file not-real
Args:
Args {
    pos: "wght:0",
    file: "not-real",
}
Error: ReadFont(Os { code: 2, kind: NotFound, message: "No such file or directory" })

Stuck? See 3-fileerror.rs.

Create a FontRef

Add a dependency on skrifa (cargo add skrifa).

Create a FontRef, passing in your data as a u8 slice. FontRef::new returns a Result with a new error type. You’ll have to add a variant to VectorDrawableError and use map_err again.

Just to confirm it’s working you could print the variable font axes by calling .axes() on your FontRef (it helpfully implements MetadataProvider). This will give you the axes and ranges declared in the fvar table in user units.

Set the location in variation space

Parse the String location into the input we need to call location. Hints:

It might be worth iterating on. Writing a for loop over the split and push results into a mutable vector might feel natural depending what language(s) you are most familiar with.

Print the debug representation once you have parsed the location. It should look something like this:

$ cargo run -- -p wght:100,FILL:0.75 -f ../material-design-icons/variablefont/MaterialSymbolsOutlined\[FILL\,GRAD\,opsz\,wght\].ttf
Args {
    pos: "wght:100,FILL:0.75",
    file: "../material-design-icons/variablefont/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
}
Location { coords: [0.75, 0.0, 0.0, -1.0] }

Note that Location is in normalized units.

Stuck? See 4-split.rs.

Find the Glyph ID of our icon

Oh wait, we haven’t actually identified a glyph! Google-style icon fonts have two ways to access a glyph:

  1. By ligature, e.g. “alarm” will resolve to the glyph for the alarm icon
  2. By codepoint, each unique icon name is assigned a single private-use area codepoint

Resolving ligatures is slightly fiddly so lets go with codepoint for now. Add a commandline argument to specify the codepoint. Since things like https://fonts.corp.google.com/icons tend to give the codepoint in hex you might want to support inputs like 0xe855 (the codepoint for alarm).

Once you’ve got your codepoint resolve it to a glyph identifier using the charactermap, just like in font103.

Stuck? See 5-gid.rs.

Draw an svg

Before we make a Vector Drawable let’s draw an SVG so we can look at it in a browser and confirm the expected result.

In a stunning stroke of luck Skrifa has an example of drawing an svg path. You can implement your own pen or use BezPathPen (cargo add write-fonts) to generate a BezPath and call BezPath::to_svg to get the path.

To display an svg we’ll need to wrap some boilerplate around our path. Notably, we’ll have to specify the rectangular region of svg space we want to look at via the viewBox attribute. Conveniently Google style icon fonts draw into a square space starting at 0,0 and extending to (upem, upem). You can get upem from the head table by calling .head() on your FontRef.

Write a string to stdout similar to:

<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
   <path d="M ... Z"/>
</svg>

Try out your svg, a browser can render it. If all went well you should see your icon … but upside-down:

Blast! Turns out fonts are y-up and svg is y-down. Luckily kurbo (cargo add kurbo) has an Affine implementation and there is a BezPath::apply_affine method. Affine::FLIP_Y will correct our clock but … then the content with stretch from (0, -upem) to (upem, 0). To fix that do one of:

  1. Write the viewBox for where the content is now
  2. Move the content back up using Affine::then_translate
    • Or building the appropriate Affine some other way

You should now have an svg of your icon!

Hint:

Stuck? See 6-svg.rs.

Draw a vector drawable

It’s just a slightly different xml wrapper. See Vector images in the Android documentation.

Write one and try it out in Android Studio. Put it in app/src/main/res/drawable with the extension .xml and use it in an Android application.