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 = "")]
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 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
, f32
String
has a very nice split
method
Iterator
over the resultscollect
the result into a Vec
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.
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. 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:
Affine::then_translate
You should now have an svg of your icon!
Hint:
kurbo
are being used?” run cargo tree
Cargo.toml
declares one version of kurbo and write-fonts depends on anotherStuck? 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.