Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
Expand Down Expand Up @@ -604,6 +607,52 @@ impl Font {
pub fn guidelines_mut(&mut self) -> &mut Vec<Guideline> {
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<plist::Dictionary, FontLoadError> {
Expand Down
15 changes: 11 additions & 4 deletions src/groups.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down
147 changes: 146 additions & 1 deletion src/kerning.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -14,6 +19,98 @@ use crate::Name;
/// We use a [`BTreeMap`] because we need sorting for serialization.
pub type Kerning = BTreeMap<Name, BTreeMap<Name, f64>>;

/// 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<Name, Name>,
pub(crate) second: HashMap<Name, Name>,
}

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<Name> {
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<Name> {
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<f64> {
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
Expand Down Expand Up @@ -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};

Expand Down Expand Up @@ -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}"
);
}
}
}
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ impl std::borrow::Borrow<str> for Name {
}
}

impl std::borrow::Borrow<str> for &Name {
fn borrow(&self) -> &str {
self.0.as_ref()
}
}

impl FromStr for Name {
type Err = NamingError;

Expand Down
12 changes: 7 additions & 5 deletions src/upconversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
}
Expand All @@ -51,7 +51,8 @@ pub(crate) fn upconvert_kerning(
let mut groups_first_old_to_new: HashMap<Name, Name> = 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());
Expand All @@ -60,7 +61,8 @@ pub(crate) fn upconvert_kerning(
let mut groups_second_old_to_new: HashMap<Name, Name> = 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());
Expand Down
Loading