diff --git a/src/font.rs b/src/font.rs index 627df40a..0703b7fe 100644 --- a/src/font.rs +++ b/src/font.rs @@ -2,8 +2,9 @@ #![deny(rustdoc::broken_intra_doc_links)] -use std::fs; +use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::{fs, iter}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -13,9 +14,11 @@ use crate::datastore::{DataStore, ImageStore}; use crate::error::{FontLoadError, FontWriteError}; use crate::fontinfo::FontInfo; use crate::glyph::Glyph; -use crate::groups::{validate_groups, Groups}; +use crate::groups::{ + validate_groups, Groups, FIRST_KERNING_GROUP_PREFIX, SECOND_KERNING_GROUP_PREFIX, +}; use crate::guideline::Guideline; -use crate::kerning::Kerning; +use crate::kerning::{Kerning, KerningResolver}; use crate::layer::{Layer, LayerContents, LAYER_CONTENTS_FILE}; use crate::name::Name; use crate::names::NameList; @@ -604,6 +607,52 @@ impl Font { pub fn guidelines_mut(&mut self) -> &mut Vec { self.font_info.guidelines.get_or_insert_with(Default::default) } + + /// Builds a [`KerningResolver`], which can do full kerning lookups, + /// including group resolution. + /// + /// Note: creating a [`KerningResolver`] will prevent you from mutating + /// the font until it is dropped. + /// + /// ``` + /// # use norad::{Font, Name}; + /// use maplit::btreemap; + /// + /// let mut font = Font::new(); + /// font.groups = btreemap! { + /// Name::new("public.kern1.A").unwrap() => vec![ + /// Name::new("A").unwrap(), + /// ], + /// }; + /// font.kerning = btreemap! { + /// Name::new("public.kern1.A").unwrap() => btreemap! { + /// Name::new("V").unwrap() => -15.0, + /// }, + /// }; + /// let resolver = font.kerning_resolver(); + /// assert_eq!( + /// resolver.get("A", "V"), + /// Some(-15.0), + /// ); + /// ``` + pub fn kerning_resolver(&'_ self) -> KerningResolver<'_> { + self.groups.iter().fold( + KerningResolver { + kerning: &self.kerning, + first: HashMap::new(), + second: HashMap::new(), + }, + |mut kr, (group_name, members)| { + let inverted = members.iter().cloned().zip(iter::repeat(group_name.clone())); + if group_name.starts_with(FIRST_KERNING_GROUP_PREFIX) { + kr.first.extend(inverted); + } else if group_name.starts_with(SECOND_KERNING_GROUP_PREFIX) { + kr.second.extend(inverted); + } + kr + }, + ) + } } fn load_lib(lib_path: &Path) -> Result { diff --git a/src/groups.rs b/src/groups.rs index 2a76c8c9..7cf1e427 100644 --- a/src/groups.rs +++ b/src/groups.rs @@ -1,8 +1,15 @@ +//! Helper types & constants for working with groups. + use std::collections::{BTreeMap, HashSet}; use crate::error::GroupsValidationError; use crate::Name; +/// The UFO standard group prefix for kerns in the first position. +pub const FIRST_KERNING_GROUP_PREFIX: &str = "public.kern1."; +/// The UFO standard group prefix for kerns in the second position. +pub const SECOND_KERNING_GROUP_PREFIX: &str = "public.kern2."; + /// A map of group name to a list of glyph names. /// /// We use a [`BTreeMap`] because we need sorting for serialization. @@ -18,8 +25,8 @@ pub(crate) fn validate_groups(groups_map: &Groups) -> Result<(), GroupsValidatio return Err(GroupsValidationError::InvalidName); } - if group_name.starts_with("public.kern1.") { - if group_name.len() == 13 { + if group_name.starts_with(FIRST_KERNING_GROUP_PREFIX) { + if group_name.len() == FIRST_KERNING_GROUP_PREFIX.len() { // Prefix but no actual name. return Err(GroupsValidationError::InvalidName); } @@ -31,8 +38,8 @@ pub(crate) fn validate_groups(groups_map: &Groups) -> Result<(), GroupsValidatio }); } } - } else if group_name.starts_with("public.kern2.") { - if group_name.len() == 13 { + } else if group_name.starts_with(SECOND_KERNING_GROUP_PREFIX) { + if group_name.len() == SECOND_KERNING_GROUP_PREFIX.len() { // Prefix but no actual name. return Err(GroupsValidationError::InvalidName); } diff --git a/src/kerning.rs b/src/kerning.rs index ed2c6ea5..dd28aab7 100644 --- a/src/kerning.rs +++ b/src/kerning.rs @@ -1,7 +1,12 @@ -use std::collections::BTreeMap; +//! Helper types for working with kerning. +//! +//! To find the kerning value for a glyph/group pair, see +//! [`Font::kerning_resolver`](crate::Font::kerning_resolver) and then +//! [`KerningResolver::get`]. use serde::ser::{SerializeMap, Serializer}; use serde::Serialize; +use std::collections::{BTreeMap, HashMap}; use crate::Name; @@ -14,6 +19,98 @@ use crate::Name; /// We use a [`BTreeMap`] because we need sorting for serialization. pub type Kerning = BTreeMap>; +/// A utility struct to facilitate kerning lookups, including resolving group membership. +/// +/// Created by calling [`Font::kerning_resolver`](crate::Font::kerning_resolver). +/// +/// ``` +/// # use norad::{Font, Name}; +/// use maplit::btreemap; +/// +/// let mut font = Font::new(); +/// font.groups = btreemap! { +/// Name::new("public.kern1.A").unwrap() => vec![ +/// Name::new("A").unwrap(), +/// ], +/// }; +/// font.kerning = btreemap! { +/// Name::new("public.kern1.A").unwrap() => btreemap! { +/// Name::new("V").unwrap() => -15.0, +/// }, +/// }; +/// let resolver = font.kerning_resolver(); +/// assert_eq!( +/// resolver.get("A", "V"), +/// Some(-15.0), +/// ); +/// ``` +#[derive(Debug)] +pub struct KerningResolver<'font> { + pub(crate) kerning: &'font Kerning, + pub(crate) first: HashMap, + pub(crate) second: HashMap, +} + +impl KerningResolver<'_> { + /// Get the group (if any) for the glyph name when it's first in a kerning + /// pair. + #[inline] + pub fn get_first_group(&self, glyph_name: &str) -> Option { + self.first.get(glyph_name).cloned() + } + + /// Get the group (if any) for the glyph name when it's second in a + /// kerning pair. + #[inline] + pub fn get_second_group(&self, glyph_name: &str) -> Option { + self.second.get(glyph_name).cloned() + } + + /// Retrieve the kerning value (if any) between a pair of elements. + /// + /// The elements can be either individual glyphs (by name) or kerning groups + /// (by name), or any combination of the two. + // ^ note: this works without any special consideration in the code + // because glyph names are forbidden from using the group prefix, + // thus meaning the group name lookup will always fail if a group + // was passed in + pub fn get(&self, first: &str, second: &str) -> Option { + let kerning_lookup = |first: &str, second: &str| { + self.kerning.get(first).and_then(|first| first.get(second)).copied() + }; + + // glyph name glyph name + if let Some(kern) = kerning_lookup(first, second) { + return Some(kern); + } + + // glyph name group name + let second_group = self.get_second_group(second); + if let Some(second_group) = &second_group { + if let Some(kern) = kerning_lookup(first, second_group.as_str()) { + return Some(kern); + } + } + + // group name glyph name + let first_group = self.get_first_group(first); + if let Some(first_group) = &first_group { + if let Some(kern) = kerning_lookup(first_group.as_str(), second) { + return Some(kern); + } + } + + // group name group name + if let Some((first_group, second_group)) = first_group.zip(second_group) { + if let Some(kern) = kerning_lookup(first_group.as_str(), second_group.as_str()) { + return Some(kern); + } + } + + None + } +} + /// A helper for serializing kerning values. /// /// `KerningSerializer` is a crutch to serialize kerning values as integers if they are @@ -61,6 +158,7 @@ impl Serialize for KerningInnerSerializer<'_> { #[cfg(test)] mod tests { use super::*; + use crate::Font; use maplit::btreemap; use serde_test::{assert_ser_tokens, Token}; @@ -95,4 +193,51 @@ mod tests { ], ); } + + #[test] + fn test_kerning_resolution() { + // Test data taken from https://unifiedfontobject.org/versions/ufo3/kerning.plist/#exceptions + let font = Font { + groups: btreemap! { + Name::new_raw("public.kern1.O") => vec![ + Name::new_raw("O"), + Name::new_raw("D"), + Name::new_raw("Q"), + ], + Name::new_raw("public.kern2.E") => vec![ + Name::new_raw("E"), + Name::new_raw("F"), + ], + }, + kerning: btreemap! { + Name::new_raw("public.kern1.O") => btreemap! { + Name::new_raw("public.kern2.E") => -100f64, + Name::new_raw("F") => -200f64, + }, + Name::new_raw("Q") => btreemap! { + Name::new_raw("public.kern2.E") => -250f64, + }, + Name::new_raw("D") => btreemap! { + Name::new_raw("F") => -300f64, + }, + }, + ..Default::default() + }; + + let resolver = font.kerning_resolver(); + for (left, right, expected) in [ + ("O", "E", -100f64), + ("O", "F", -200f64), + ("D", "E", -100f64), + ("D", "F", -300f64), + ("Q", "E", -250f64), + ("Q", "F", -250f64), + ] { + assert_eq!( + resolver.get(left, right), + Some(expected), + "kerning_lookup incorrect for /{left}/{right}" + ); + } + } } diff --git a/src/lib.rs b/src/lib.rs index 56b06864..3b3ca224 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,10 +72,10 @@ pub mod error; mod font; pub mod fontinfo; mod glyph; -mod groups; +pub mod groups; mod guideline; mod identifier; -mod kerning; +pub mod kerning; mod layer; mod name; mod names; diff --git a/src/name.rs b/src/name.rs index 22c7a907..319b09ec 100644 --- a/src/name.rs +++ b/src/name.rs @@ -93,6 +93,12 @@ impl std::borrow::Borrow for Name { } } +impl std::borrow::Borrow for &Name { + fn borrow(&self) -> &str { + self.0.as_ref() + } +} + impl FromStr for Name { type Err = NamingError; diff --git a/src/upconversion.rs b/src/upconversion.rs index 9914b52f..72cb310d 100644 --- a/src/upconversion.rs +++ b/src/upconversion.rs @@ -6,7 +6,7 @@ use serde::Deserialize; use crate::error::FontLoadError; use crate::font::LIB_FILE; use crate::fontinfo::FontInfo; -use crate::groups::Groups; +use crate::groups::{Groups, FIRST_KERNING_GROUP_PREFIX, SECOND_KERNING_GROUP_PREFIX}; use crate::kerning::Kerning; use crate::names::NameList; use crate::Name; @@ -31,14 +31,14 @@ pub(crate) fn upconvert_kerning( for (first, seconds) in kerning { if groups.contains_key(first) && !glyph_set.contains(first) - && !first.starts_with("public.kern1.") + && !first.starts_with(FIRST_KERNING_GROUP_PREFIX) { groups_first.insert(first.clone()); } for second in seconds.keys() { if groups.contains_key(second) && !glyph_set.contains(second) - && !second.starts_with("public.kern2.") + && !second.starts_with(SECOND_KERNING_GROUP_PREFIX) { groups_second.insert(second.clone()); } @@ -51,7 +51,8 @@ pub(crate) fn upconvert_kerning( let mut groups_first_old_to_new: HashMap = HashMap::new(); for first in &groups_first { let first_new = make_unique_group_name( - Name::new(&format!("public.kern1.{}", first.replace("@MMK_L_", ""))).unwrap(), + Name::new(&format!("{FIRST_KERNING_GROUP_PREFIX}{}", first.replace("@MMK_L_", ""))) + .unwrap(), &groups_new, ); groups_first_old_to_new.insert(first.clone(), first_new.clone()); @@ -60,7 +61,8 @@ pub(crate) fn upconvert_kerning( let mut groups_second_old_to_new: HashMap = HashMap::new(); for second in &groups_second { let second_new = make_unique_group_name( - Name::new(&format!("public.kern2.{}", second.replace("@MMK_R_", ""))).unwrap(), + Name::new(&format!("{SECOND_KERNING_GROUP_PREFIX}{}", second.replace("@MMK_R_", ""))) + .unwrap(), &groups_new, ); groups_second_old_to_new.insert(second.clone(), second_new.clone());