Learn to generate an Android Vector Drawable from a variable icon font.
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.
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:
VSCode with rust-analyzer is a good default if you don’t already have a favorite.
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:

# 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
Let’s take this slightly more seriously than font103 and try to avoid unwrap and expect for the most part.
cargo add thiserror)thiserror example shows, have no entries for now, and be called something creative like VectorDrawableError.-> Result<(), VectorDrawableError>
Ok(()) as the final line of mainMake sure you can cargo run before proceeding.
Stuck? See 1-errortype.rs.
Add a dependency on clap (cargo add clap --features derive) and setup an args structure with two arguments:
Location using AxisCollection::location, default_value = ""
#[arg(short, long, default_value = "")]FontRefTry 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 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.
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.
Parse the String location into the input we need to call location. Hints:
[("wght", 250.0), ("wdth", 75.0)] is an array of tuples of &str, f32String has a very nice split method
Iterator over the resultscollect the result into a VecIt 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.
Oh wait, we haven’t actually identified a glyph! Google-style icon fonts have two ways to access a glyph:
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.
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. Implement your own pen:
OutlinePenBezPath (e.g. struct BezPen(BezPath);)move_to call BezPath.move_toYou can then create your pen, draw into it, get the BezPath from it,
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:
Affine::then_translate
You should now have an svg of your icon!
Hint:
Stuck? See 6-svg.rs.
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.