diff --git a/font104/3-fileerror.rs b/font104/3-fileerror.rs index 79855e7..a5adf03 100644 --- a/font104/3-fileerror.rs +++ b/font104/3-fileerror.rs @@ -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 diff --git a/font104/4-split.rs b/font104/4-split.rs index ab44f4c..e6cd212 100644 --- a/font104/4-split.rs +++ b/font104/4-split.rs @@ -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, } @@ -27,6 +27,21 @@ struct Args { file: String, } +fn parse_location(raw: &str) -> Result, VectorDrawableError> { + raw.split(",") + .map(|s| { + let parts = s.split(":").collect::>(); + if parts.len() != 2 { + return Err(VectorDrawableError::UnableToParsePosition); + } + let tag = parts[0]; + let value = parts[1].parse::() + .map_err(|_| VectorDrawableError::UnableToParsePosition)?; + Ok((tag, value)) + }) + .collect::, _>>() +} + fn main() -> Result<(), VectorDrawableError> { let args = Args::parse(); println!("{args:#?}"); @@ -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::>(); - if parts.len() != 2 { - return Err(VectorDrawableError::UnableToParsePosition); - } - let tag = parts[0]; - let value = parts[1].parse::() - .map_err(|_| VectorDrawableError::UnableToParsePosition)?; - Ok((tag, value)) - }) - .collect::, _>>()?; - + // 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(()) } diff --git a/font104/5-gid.rs b/font104/5-gid.rs new file mode 100644 index 0000000..1d024f3 --- /dev/null +++ b/font104/5-gid.rs @@ -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, VectorDrawableError> { + raw.split(",") + .map(|s| { + let parts = s.split(":").collect::>(); + if parts.len() != 2 { + return Err(VectorDrawableError::UnableToParsePosition); + } + let tag = parts[0]; + let value = parts[1].parse::() + .map_err(|_| VectorDrawableError::UnableToParsePosition)?; + Ok((tag, value)) + }) + .collect::, _>>() +} + +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(()) +} diff --git a/font104/6-svg.rs b/font104/6-svg.rs new file mode 100644 index 0000000..8c10cca --- /dev/null +++ b/font104/6-svg.rs @@ -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, VectorDrawableError> { + raw.split(",") + .map(|s| { + let parts = s.split(":").collect::>(); + if parts.len() != 2 { + return Err(VectorDrawableError::UnableToParsePosition); + } + let tag = parts[0]; + let value = parts[1].parse::() + .map_err(|_| VectorDrawableError::UnableToParsePosition)?; + Ok((tag, value)) + }) + .collect::, _>>() +} + +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!(""); + println!(" "); + println!(""); + + Ok(()) +} diff --git a/font104/index.md b/font104/index.md index c2ad487..0b62966 100644 --- a/font104/index.md +++ b/font104/index.md @@ -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 :) \ No newline at end of file +### 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 + + + +``` + +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`](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. + + diff --git a/font104/upside_down.svg b/font104/upside_down.svg new file mode 100644 index 0000000..0e450ba --- /dev/null +++ b/font104/upside_down.svg @@ -0,0 +1,3 @@ + + +