Skip to content

Commit

Permalink
First full draft of font104
Browse files Browse the repository at this point in the history
  • Loading branch information
rsheeter committed Mar 14, 2024
1 parent 86ff7ef commit d60e2cc
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 23 deletions.
4 changes: 2 additions & 2 deletions font104/3-fileerror.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use thiserror::Error;

#[derive(Error, Debug)]
pub enum VectorDrawableError {
#[error("Unable to read font")]
ReadFont(#[from] io::Error)
#[error("Unable to read font {0}")]
ReadFont(io::Error)
}

/// A program to generate vector drawables from glyphs in a font
Expand Down
42 changes: 22 additions & 20 deletions font104/4-split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use thiserror::Error;

#[derive(Error, Debug)]
pub enum VectorDrawableError {
#[error("Unable to read font")]
ReadFont(#[from] io::Error),
#[error("Unable to create a font ref")]
FontRef(#[from] ReadError),
#[error("Unable to read font {0}")]
ReadFont(io::Error),
#[error("Unable to create a font ref {0}")]
FontRef(ReadError),
#[error("Unintelligible position")]
UnableToParsePosition,
}
Expand All @@ -27,6 +27,21 @@ struct Args {
file: String,
}

fn parse_location(raw: &str) -> Result<Vec<(&str, f32)>, VectorDrawableError> {
raw.split(",")
.map(|s| {
let parts = s.split(":").collect::<Vec<_>>();
if parts.len() != 2 {
return Err(VectorDrawableError::UnableToParsePosition);
}
let tag = parts[0];
let value = parts[1].parse::<f32>()
.map_err(|_| VectorDrawableError::UnableToParsePosition)?;
Ok((tag, value))
})
.collect::<Result<Vec<_>, _>>()
}

fn main() -> Result<(), VectorDrawableError> {
let args = Args::parse();
println!("{args:#?}");
Expand All @@ -35,24 +50,11 @@ fn main() -> Result<(), VectorDrawableError> {
.map_err(VectorDrawableError::ReadFont)?;
let font = FontRef::new(&raw_font)
.map_err(VectorDrawableError::FontRef)?;

let location = args.pos.split(",")
.map(|s| {
let parts = s.split(":").collect::<Vec<_>>();
if parts.len() != 2 {
return Err(VectorDrawableError::UnableToParsePosition);
}
let tag = parts[0];
let value = parts[1].parse::<f32>()
.map_err(|_| VectorDrawableError::UnableToParsePosition)?;
Ok((tag, value))
})
.collect::<Result<Vec<_>, _>>()?;


// rebinding (reusing variable names) is much more common in Rust than most other languages
let location = parse_location(&args.pos)?;
let location = font.axes().location(location);

println!("{location:?}");
println!("{location:?}");

Ok(())
}
80 changes: 80 additions & 0 deletions font104/5-gid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use std::{fs, io};

use clap::Parser;
use skrifa::{raw::ReadError, FontRef, MetadataProvider};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum VectorDrawableError {
#[error("Unable to read font {0}")]
ReadFont(io::Error),
#[error("Unable to create a font ref {0}")]
FontRef(ReadError),
#[error("Unintelligible position")]
UnableToParsePosition,
#[error("Unable to parse codepoint")]
UnableToParseCodepoint,
#[error("No mapping for codepoint")]
UnableToMapCodepointToGlyphId,
}

/// A program to generate vector drawables from glyphs in a font
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// Position in designspace. Commas-separated tag:value pairs, e.g. FILL:0,wght:157
#[arg(short, long, default_value = "")]
pos: String,

/// The font file to process
#[arg(short, long)]
file: String,

/// The codepoint for the icon, e.g. 0x855.
#[arg(short, long)]
icon: String,
}

fn parse_location(raw: &str) -> Result<Vec<(&str, f32)>, VectorDrawableError> {
raw.split(",")
.map(|s| {
let parts = s.split(":").collect::<Vec<_>>();
if parts.len() != 2 {
return Err(VectorDrawableError::UnableToParsePosition);
}
let tag = parts[0];
let value = parts[1].parse::<f32>()
.map_err(|_| VectorDrawableError::UnableToParsePosition)?;
Ok((tag, value))
})
.collect::<Result<Vec<_>, _>>()
}

fn main() -> Result<(), VectorDrawableError> {
let args = Args::parse();
println!("{args:#?}");

let raw_font = fs::read(&args.file)
.map_err(VectorDrawableError::ReadFont)?;
let font = FontRef::new(&raw_font)
.map_err(VectorDrawableError::FontRef)?;

let codepoint = if args.icon.starts_with("0x") {
u32::from_str_radix(&args.icon[2..], 16)
.map_err(|_| VectorDrawableError::UnableToParseCodepoint)?
} else {
// TODO: support ligature access
return Err(VectorDrawableError::UnableToParseCodepoint);
};
let gid = font.charmap().map(codepoint)
.ok_or(VectorDrawableError::UnableToMapCodepointToGlyphId)?;

// rebinding (reusing variable names) is much more common in Rust than most other languages
let location = parse_location(&args.pos)?;
let location = font.axes().location(location);
println!("{location:?}");

println!("TODO: draw {gid}");

Ok(())
}
100 changes: 100 additions & 0 deletions font104/6-svg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::{fs, io};

use clap::Parser;
use kurbo::Affine;
use skrifa::{instance::Size, outline::DrawError, raw::{ReadError, TableProvider}, FontRef, MetadataProvider};
use thiserror::Error;
use write_fonts::pens::BezPathPen;

#[derive(Error, Debug)]
pub enum VectorDrawableError {
#[error("Unable to read font {0}")]
ReadFont(io::Error),
#[error("Unable to create a font ref {0}")]
FontRef(ReadError),
#[error("Unable to read 'head' {0}")]
NoHead(ReadError),
#[error("Unintelligible position")]
UnableToParsePosition,
#[error("Unable to parse codepoint")]
UnableToParseCodepoint,
#[error("No mapping for codepoint")]
UnableToMapCodepointToGlyphId,
#[error("No outline for glyph id")]
UnableToLoadOutline,
#[error("Unable to draw glyph {0}")]
UnableToDraw(DrawError),
}

/// A program to generate vector drawables from glyphs in a font
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// Position in designspace. Commas-separated tag:value pairs, e.g. FILL:0,wght:157
#[arg(short, long, default_value = "")]
pos: String,

/// The font file to process
#[arg(short, long)]
file: String,

/// The codepoint for the icon, e.g. 0x855.
#[arg(short, long)]
icon: String,
}

fn parse_location(raw: &str) -> Result<Vec<(&str, f32)>, VectorDrawableError> {
raw.split(",")
.map(|s| {
let parts = s.split(":").collect::<Vec<_>>();
if parts.len() != 2 {
return Err(VectorDrawableError::UnableToParsePosition);
}
let tag = parts[0];
let value = parts[1].parse::<f32>()
.map_err(|_| VectorDrawableError::UnableToParsePosition)?;
Ok((tag, value))
})
.collect::<Result<Vec<_>, _>>()
}

fn main() -> Result<(), VectorDrawableError> {
let args = Args::parse();

let raw_font = fs::read(&args.file)
.map_err(VectorDrawableError::ReadFont)?;
let font = FontRef::new(&raw_font)
.map_err(VectorDrawableError::FontRef)?;

let codepoint = if args.icon.starts_with("0x") {
u32::from_str_radix(&args.icon[2..], 16)
.map_err(|_| VectorDrawableError::UnableToParseCodepoint)?
} else {
// TODO: support ligature access
return Err(VectorDrawableError::UnableToParseCodepoint);
};
let gid = font.charmap().map(codepoint)
.ok_or(VectorDrawableError::UnableToMapCodepointToGlyphId)?;
let upem = font.head().map_err(VectorDrawableError::NoHead)?.units_per_em();

// rebinding (reusing variable names) is much more common in Rust than most other languages
let location = parse_location(&args.pos)?;
let location = font.axes().location(location);

let outlines = font.outline_glyphs();
let glyph = outlines.get(gid)
.ok_or(VectorDrawableError::UnableToLoadOutline)?;

let mut pen = BezPathPen::new();
glyph.draw((Size::unscaled(), &location), &mut pen)
.map_err(|e| VectorDrawableError::UnableToDraw(e))?;
let mut path = pen.into_inner();
path.apply_affine(Affine::FLIP_Y.then_translate((0.0, upem as f64).into()));
let path = path.to_svg();

println!("<svg viewBox=\"0 0 {upem} {upem}\" xmlns=\"http://www.w3.org/2000/svg\">");
println!(" <path d=\"{path}\"/>");
println!("</svg>");

Ok(())
}
59 changes: 58 additions & 1 deletion font104/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,61 @@ Note that Location is in [normalized units](https://github.com/googlefonts/fontc
Stuck? See [4-split.rs](./4-split.rs).
# TODO: finish me :)
### 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
1. By codepoint, each unique icon name is assigned a single [private-use area](https://en.wikipedia.org/wiki/Private_Use_Areas) 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](../font103).
Stuck? See [5-gid.rs](./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](https://docs.rs/skrifa/latest/skrifa/outline/index.html) of drawing an svg path. You can implement your own pen or use [`BezPathPen`](https://docs.rs/write-fonts/latest/write_fonts/pens/struct.BezPathPen.html) (`cargo add write-fonts`) to generate a [`BezPath`](https://docs.rs/kurbo/latest/kurbo/struct.BezPath.html) and call [`BezPath::to_svg`](https://docs.rs/kurbo/latest/kurbo/struct.BezPath.html#method.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](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/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](https://learn.microsoft.com/en-us/typography/opentype/spec/head) table by calling [`.head()`](https://docs.rs/read-fonts/latest/read_fonts/trait.TableProvider.html) on your `FontRef`.
Write a string to stdout similar to:
```xml
<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:
<img width="128" src="./upside_down.svg" />
Blast! Turns out fonts are y-up and svg is y-down. Luckily kurbo (`cargo add kurbo`) has an [`Affine`](https://docs.rs/kurbo/latest/kurbo/struct.Affine.html) implementation and there is a [`BezPath::apply_affine`](https://docs.rs/kurbo/latest/kurbo/struct.BezPath.html#method.apply_affine) method. [`Affine::FLIP_Y`](https://docs.rs/kurbo/latest/kurbo/struct.Affine.html#associatedconstant.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
1. Move the content back up using [`Affine::then_translate`](https://docs.rs/kurbo/latest/kurbo/struct.Affine.html#method.then_translate)
* Or building the appropriate Affine some other way
You should now have an svg of your icon!
Hint:
* If you get an error "perhaps two different versions of crate `kurbo` are being used?" run `cargo tree`
* Probably your `Cargo.toml` declares one version of kurbo and write-fonts depends on another
* If so, update your Cargo.toml to declare the same version as write-fonts
* If you have never run into an affine (aka transform) before 3blue1brown's [Essence of linear algebra](https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab) is highly recommended, particularly the first couple chapters
Stuck? See [6-svg.rs](./6-svg.rs).
### Draw a vector drawable
It's just a slightly different xml wrapper. See [Vector images](https://developer.android.com/develop/ui/views/graphics/vector-drawable-resources) 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.
3 changes: 3 additions & 0 deletions font104/upside_down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d60e2cc

Please sign in to comment.