diff --git a/Cargo.lock b/Cargo.lock index 9b69e67..2b713ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + [[package]] name = "bitflags" version = "2.8.0" @@ -145,6 +151,12 @@ dependencies = [ "libm", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "env_filter" version = "0.1.3" @@ -182,8 +194,10 @@ dependencies = [ "anyhow", "clap", "env_logger", + "itertools", "kurbo", "log", + "ordered-float", "rustybuzz", "skrifa", ] @@ -200,6 +214,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "kurbo" version = "0.11.1" @@ -222,12 +245,30 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "proc-macro2" version = "1.0.93" diff --git a/Cargo.toml b/Cargo.toml index ae16e83..066ee4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,10 @@ repository = "https://github.com/googlefonts/fontheight" [dependencies] anyhow = "1" clap = { version = "4.5", features = ["derive"] } +itertools = "0.14.0" kurbo = "0.11.1" log = "0.4.25" +ordered-float = "4.6" rustybuzz = "0.20.1" skrifa = "0.26.5" diff --git a/src/locations.rs b/src/locations.rs new file mode 100644 index 0000000..e6b46ff --- /dev/null +++ b/src/locations.rs @@ -0,0 +1,65 @@ +use std::collections::{BTreeSet, HashMap}; + +use itertools::Itertools; +use ordered_float::OrderedFloat; +use skrifa::{raw::collections::int_set::Domain, MetadataProvider}; + +#[derive(Debug)] +pub struct Location { + user_coords: HashMap, +} + +impl Location { + pub fn to_skrifa( + &self, + font: &skrifa::FontRef, + ) -> skrifa::instance::Location { + font.axes().location( + self.user_coords.iter().map(|(tag, coord)| (*tag, *coord)), + ) + } + + pub fn to_rustybuzz(&self) -> Vec { + self.user_coords + .iter() + .map(|(tag, coord)| rustybuzz::Variation { + tag: rustybuzz::ttf_parser::Tag(tag.to_u32()), + value: *coord, + }) + .collect() + } +} + +/// Gets the cartesian product of axis coordinates seen in named instances, axis +/// extremes, and defaults. +pub(crate) fn interesting_locations(font: &skrifa::FontRef) -> Vec { + let mut axis_coords = + vec![BTreeSet::>::new(); font.axes().len()]; + + font.named_instances() + .iter() + .flat_map(|instance| instance.user_coords().enumerate()) + .for_each(|(axis, coord)| { + axis_coords[axis].insert(coord.into()); + }); + + font.axes().iter().for_each(|axis| { + axis_coords[axis.index()].extend(&[ + axis.default_value().into(), + axis.min_value().into(), + axis.max_value().into(), + ]); + }); + + axis_coords + .iter() + .multi_cartesian_product() + .map(|coords| Location { + user_coords: coords + .into_iter() + .zip(font.axes().iter()) + .map(|(coord, axis)| (axis.tag(), From::from(*coord))) + .collect(), + }) + .collect() +} diff --git a/src/main.rs b/src/main.rs index a6fc130..014bc93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod locations; mod pens; mod word_lists; @@ -7,8 +8,9 @@ use anyhow::{anyhow, Context}; use clap::Parser; use env_logger::Env; use kurbo::Shape; +use locations::{interesting_locations, Location}; use log::{error, info, LevelFilter}; -use rustybuzz::{SerializeFlags, UnicodeBuffer}; +use rustybuzz::UnicodeBuffer; use skrifa::{outline::DrawSettings, prelude::Size, MetadataProvider}; use crate::{pens::BezierPen, word_lists::WordList}; @@ -44,44 +46,58 @@ fn _main() -> anyhow::Result<()> { let font_bytes = fs::read(&args.font_path).context("failed to read font file")?; - let font_face = rustybuzz::Face::from_slice(&font_bytes, 0) + let mut font_face = rustybuzz::Face::from_slice(&font_bytes, 0) .context("rustybuzz could not parse font")?; let skrifa_font = skrifa::FontRef::new(&font_bytes) .context("skrifa could not parse font")?; - let instance_extremes = InstanceExtremes::new( - skrifa_font, - skrifa::instance::LocationRef::default(), - )?; - - let test_words = WordList::define("test", ["hello", "apple"]); - test_words.iter().for_each(|word| { - let mut buffer = UnicodeBuffer::new(); - buffer.push_str(word); - buffer.guess_segment_properties(); - let glyph_buffer = rustybuzz::shape(&font_face, &[], buffer); - // TODO: remove empty glyphs and/or .notdef? - let _word_extremes = glyph_buffer - .glyph_infos() - .iter() - .zip(glyph_buffer.glyph_positions()) - .map(|(info, pos)| { - let y_offset = pos.y_offset; - let heights = instance_extremes.get(info.glyph_id).unwrap(); - - ( - y_offset as f64 + heights.lowest, - y_offset as f64 + heights.highest, - ) - }) - .fold(VerticalExtremes::default(), |extremes, (low, high)| { - let VerticalExtremes { highest, lowest } = extremes; - VerticalExtremes { - highest: highest.max(high), - lowest: lowest.min(low), - } + + let locations = interesting_locations(&skrifa_font); + + info!("testing font at {} locations", locations.len()); + locations + .iter() + .try_for_each(|location| -> anyhow::Result<()> { + font_face.set_variations(&location.to_rustybuzz()); + + let instance_extremes = + InstanceExtremes::new(&skrifa_font, location)?; + + let test_words = WordList::define("test", ["hello", "apple"]); + test_words.iter().for_each(|word| { + let mut buffer = UnicodeBuffer::new(); + buffer.push_str(word); + buffer.guess_segment_properties(); + let glyph_buffer = rustybuzz::shape(&font_face, &[], buffer); + // TODO: remove empty glyphs and/or .notdef? + let _word_extremes = glyph_buffer + .glyph_infos() + .iter() + .zip(glyph_buffer.glyph_positions()) + .map(|(info, pos)| { + let y_offset = pos.y_offset; + let heights = + instance_extremes.get(info.glyph_id).unwrap(); + + ( + y_offset as f64 + heights.lowest, + y_offset as f64 + heights.highest, + ) + }) + .fold( + VerticalExtremes::default(), + |extremes, (low, high)| { + let VerticalExtremes { highest, lowest } = extremes; + VerticalExtremes { + highest: highest.max(high), + lowest: lowest.min(low), + } + }, + ); }); - }); + + Ok(()) + })?; Ok(()) } @@ -91,8 +107,8 @@ struct InstanceExtremes(HashMap); impl InstanceExtremes { pub fn new( - font: skrifa::FontRef, - location: skrifa::instance::LocationRef, + font: &skrifa::FontRef, + location: &Location, ) -> anyhow::Result { let instance_extremes = font .outline_glyphs() @@ -101,7 +117,10 @@ impl InstanceExtremes { let mut bez_pen = BezierPen::default(); outline .draw( - DrawSettings::unhinted(Size::unscaled(), location), + DrawSettings::unhinted( + Size::unscaled(), + &location.to_skrifa(font), + ), &mut bez_pen, ) .map_err(|err| {