From a057cbe06e8fb9e7caecaff5cdb7e03af26bb0b0 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Sat, 4 Apr 2020 19:48:50 +0100 Subject: [PATCH 1/3] Add test data --- .../fontinfo.plist | 8 ++++ .../glyphs/X_.glif | 3 ++ .../glyphs/contents.plist | 8 ++++ .../groups.plist | 28 +++++++++++ .../kerning.plist | 46 +++++++++++++++++++ .../metainfo.plist | 10 ++++ .../fontinfo.plist | 8 ++++ .../glyphs/X_.glif | 3 ++ .../glyphs/contents.plist | 8 ++++ .../groups.plist | 28 +++++++++++ .../kerning.plist | 46 +++++++++++++++++++ .../metainfo.plist | 10 ++++ .../glyphname_groupname_groups_expected.plist | 46 +++++++++++++++++++ ...glyphname_groupname_kerning_expected.plist | 46 +++++++++++++++++++ 14 files changed, 298 insertions(+) create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/fontinfo.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/glyphs/X_.glif create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/glyphs/contents.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/groups.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/kerning.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/metainfo.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/fontinfo.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/glyphs/X_.glif create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/glyphs/contents.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/groups.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/kerning.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/metainfo.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_groups_expected.plist create mode 100644 testdata/upconversion_kerning/glyphname_groupname_kerning_expected.plist diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/fontinfo.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/fontinfo.plist new file mode 100644 index 00000000..e098e8d3 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/fontinfo.plist @@ -0,0 +1,8 @@ + + + + + familyName + Test + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/glyphs/X_.glif b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/glyphs/X_.glif new file mode 100644 index 00000000..22123d78 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/glyphs/X_.glif @@ -0,0 +1,3 @@ + + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/glyphs/contents.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/glyphs/contents.plist new file mode 100644 index 00000000..c8b76cc0 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/glyphs/contents.plist @@ -0,0 +1,8 @@ + + + + + X + X_.glif + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/groups.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/groups.plist new file mode 100644 index 00000000..a32bce20 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/groups.plist @@ -0,0 +1,28 @@ + + + + + BGroup + + B + + CGroup + + C + Ccedilla + + DGroup + + D + + Not A Kerning Group + + A + + X + + X + X.sc + + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/kerning.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/kerning.plist new file mode 100644 index 00000000..b0acf92b --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/kerning.plist @@ -0,0 +1,46 @@ + + + + + A + + A + 1 + B + 2 + CGroup + 3 + DGroup + 4 + + BGroup + + A + 5 + B + 6 + CGroup + 7 + DGroup + 8 + + CGroup + + A + 9 + B + 10 + CGroup + 11 + DGroup + 12 + + X + + A + 13 + CGroup + 14 + + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/metainfo.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/metainfo.plist new file mode 100644 index 00000000..90c4ab65 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + test + formatVersion + 1 + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/fontinfo.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/fontinfo.plist new file mode 100644 index 00000000..e098e8d3 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/fontinfo.plist @@ -0,0 +1,8 @@ + + + + + familyName + Test + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/glyphs/X_.glif b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/glyphs/X_.glif new file mode 100644 index 00000000..22123d78 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/glyphs/X_.glif @@ -0,0 +1,3 @@ + + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/glyphs/contents.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/glyphs/contents.plist new file mode 100644 index 00000000..c8b76cc0 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/glyphs/contents.plist @@ -0,0 +1,8 @@ + + + + + X + X_.glif + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/groups.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/groups.plist new file mode 100644 index 00000000..a32bce20 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/groups.plist @@ -0,0 +1,28 @@ + + + + + BGroup + + B + + CGroup + + C + Ccedilla + + DGroup + + D + + Not A Kerning Group + + A + + X + + X + X.sc + + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/kerning.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/kerning.plist new file mode 100644 index 00000000..b0acf92b --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/kerning.plist @@ -0,0 +1,46 @@ + + + + + A + + A + 1 + B + 2 + CGroup + 3 + DGroup + 4 + + BGroup + + A + 5 + B + 6 + CGroup + 7 + DGroup + 8 + + CGroup + + A + 9 + B + 10 + CGroup + 11 + DGroup + 12 + + X + + A + 13 + CGroup + 14 + + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/metainfo.plist b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/metainfo.plist new file mode 100644 index 00000000..21f0a5cd --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + test + formatVersion + 2 + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_groups_expected.plist b/testdata/upconversion_kerning/glyphname_groupname_groups_expected.plist new file mode 100644 index 00000000..0c949031 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_groups_expected.plist @@ -0,0 +1,46 @@ + + + + + BGroup + + B + + CGroup + + C + Ccedilla + + DGroup + + D + + Not A Kerning Group + + A + + X + + X + X.sc + + public.kern1.BGroup + + B + + public.kern1.CGroup + + C + Ccedilla + + public.kern2.CGroup + + C + Ccedilla + + public.kern2.DGroup + + D + + + diff --git a/testdata/upconversion_kerning/glyphname_groupname_kerning_expected.plist b/testdata/upconversion_kerning/glyphname_groupname_kerning_expected.plist new file mode 100644 index 00000000..b41ae0f2 --- /dev/null +++ b/testdata/upconversion_kerning/glyphname_groupname_kerning_expected.plist @@ -0,0 +1,46 @@ + + + + + A + + A + 1 + B + 2 + public.kern2.CGroup + 3 + public.kern2.DGroup + 4 + + X + + A + 13 + public.kern2.CGroup + 14 + + public.kern1.BGroup + + A + 5 + B + 6 + public.kern2.CGroup + 7 + public.kern2.DGroup + 8 + + public.kern1.CGroup + + A + 9 + B + 10 + public.kern2.CGroup + 11 + public.kern2.DGroup + 12 + + + From 8fe735870bd59bd941044383735d41ff5fe59dad Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Sun, 5 Apr 2020 14:31:09 +0100 Subject: [PATCH 2/3] Add type aliases for Groups and Kerning Also, make Groups a mapping of Strings to Vecs of GlyphNames (Arc). --- src/ufo.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/ufo.rs b/src/ufo.rs index 036280a9..3968f18f 100644 --- a/src/ufo.rs +++ b/src/ufo.rs @@ -28,6 +28,14 @@ static DEFAULT_LAYER_NAME: &str = "public.default"; static DEFAULT_GLYPHS_DIRNAME: &str = "glyphs"; static DEFAULT_METAINFO_CREATOR: &str = "org.linebender.norad"; +/// Groups is a map of group name to a list of glyph names. It's a BTreeMap because we need sorting +/// for serialization. +pub type Groups = BTreeMap>; +/// Kerning is a map of first half of a kerning pair (glyph name or group name) to the second half +/// of a pair (glyph name or group name), which maps to the kerning value (high-level view: +/// (first, second) => value). It's a BTreeMap because we need sorting for serialization. +pub type Kerning = BTreeMap>; + /// A Unified Font Object. #[derive(Clone)] pub struct Ufo { @@ -35,9 +43,8 @@ pub struct Ufo { pub font_info: Option, pub layers: Vec, pub lib: Option, - // groups and kerning: BTreeMap because we need sorting for deserialization. - pub groups: Option>>, - pub kerning: Option>>, + pub groups: Option, + pub kerning: Option, pub features: Option, __non_exhaustive: (), } @@ -147,9 +154,8 @@ impl Ufo { let groups_path = path.join(GROUPS_FILE); let groups = if groups_path.exists() { - let groups: BTreeMap> = plist::from_file(groups_path)?; + let groups: Groups = plist::from_file(groups_path)?; validate_groups(&groups).map_err(Error::GroupsError)?; - Some(groups) } else { None @@ -157,9 +163,7 @@ impl Ufo { let kerning_path = path.join(KERNING_FILE); let kerning = if kerning_path.exists() { - let kerning: BTreeMap> = - plist::from_file(kerning_path)?; - + let kerning: Kerning = plist::from_file(kerning_path)?; Some(kerning) } else { None @@ -168,7 +172,6 @@ impl Ufo { let features_path = path.join(FEATURES_FILE); let features = if features_path.exists() { let features = fs::read_to_string(features_path)?; - Some(features) } else { None @@ -341,9 +344,7 @@ impl Ufo { /// Validate the contents of the groups.plist file according to the rules in the /// [Unified Font Object v3 specification for groups.plist](http://unifiedfontobject.org/versions/ufo3/groups.plist/#specification). -fn validate_groups( - groups_map: &BTreeMap>, -) -> Result<(), GroupsValidationError> { +fn validate_groups(groups_map: &Groups) -> Result<(), GroupsValidationError> { let mut kern1_set = HashSet::new(); let mut kern2_set = HashSet::new(); for (group_name, group_glyph_names) in groups_map { @@ -406,14 +407,8 @@ mod tests { font_obj.lib.unwrap().get("com.typemytype.robofont.compileSettings.autohint"), Some(&plist::Value::Boolean(true)) ); - - assert_eq!( - font_obj.groups.unwrap().get("public.kern1.@MMK_L_A"), - Some(&vec!["A".to_string()]) - ); - + assert_eq!(font_obj.groups.unwrap().get("public.kern1.@MMK_L_A"), Some(&vec!["A".into()])); assert_eq!(font_obj.kerning.unwrap().get("B").unwrap().get("H").unwrap(), &-40.0); - assert_eq!(font_obj.features.unwrap(), "# this is the feature from lightWide\n"); } From 93a5e4c93cc6cc4e0361e49a994a43c772b25c42 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Sun, 5 Apr 2020 14:33:45 +0100 Subject: [PATCH 3/3] Implement UFO v1 and v2 kerning data upconversion - Upgrade loaded UFO's meta version to v3 - Add GroupsUpconversionError, shares inner error with GroupsError --- Cargo.toml | 1 + src/error.rs | 3 + src/lib.rs | 1 + src/ufo.rs | 18 +- src/upconversion.rs | 408 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 src/upconversion.rs diff --git a/Cargo.toml b/Cargo.toml index 2d1678c0..4c21641c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,4 @@ optional = true failure = "0.1.6" serde_test = "1.0.102" tempdir = "0.3.7" +maplit = "1.0.2" diff --git a/src/error.rs b/src/error.rs index 830642e3..9b1d936e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,9 +21,11 @@ pub enum Error { PlistError(PlistError), FontInfoError, GroupsError(GroupsValidationError), + GroupsUpconversionError(GroupsValidationError), ExpectedPlistDictionaryError, } +/// An error representing a failure to validate UFO groups. #[derive(Debug)] pub enum GroupsValidationError { InvalidName, @@ -93,6 +95,7 @@ impl std::fmt::Display for Error { Error::PlistError(e) => e.fmt(f), Error::FontInfoError => write!(f, "FontInfo contains invalid data"), Error::GroupsError(ge) => ge.fmt(f), + Error::GroupsUpconversionError(ge) => write!(f, "Upconverting UFO v1 or v2 kerning data to v3 failed: {}", ge), Error::ExpectedPlistDictionaryError => write!(f, "The files groups.plist, kerning.plist and lib.plist must contain plist dictionaries."), } } diff --git a/src/lib.rs b/src/lib.rs index 9c16c800..a4105a98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ pub mod glyph; mod layer; mod shared_types; mod ufo; +mod upconversion; pub use error::Error; pub use fontinfo::FontInfo; diff --git a/src/ufo.rs b/src/ufo.rs index 3968f18f..663ee210 100644 --- a/src/ufo.rs +++ b/src/ufo.rs @@ -14,6 +14,7 @@ use crate::error::GroupsValidationError; use crate::fontinfo::FontInfo; use crate::glyph::{Glyph, GlyphName}; use crate::layer::Layer; +use crate::upconversion::upconvert_kerning; use crate::Error; use plist; @@ -130,7 +131,7 @@ impl Ufo { // minimize monomorphization fn load_impl(path: &Path) -> Result { let meta_path = path.join(METAINFO_FILE); - let meta: MetaInfo = plist::from_file(meta_path)?; + let mut meta: MetaInfo = plist::from_file(meta_path)?; let fontinfo_path = path.join(FONTINFO_FILE); let font_info = if fontinfo_path.exists() { let font_info: FontInfo = plist::from_file(fontinfo_path)?; @@ -197,6 +198,21 @@ impl Ufo { .collect(); let layers = layers?; + // Upconvert UFO v1 or v2 kerning data if necessary. To upconvert, we need at least + // a groups.plist file, while a kerning.plist is optional. + let (groups, kerning) = match (meta.format_version, groups, kerning) { + (FormatVersion::V3, g, k) => (g, k), // For v3, we do nothing. + (_, None, k) => (None, k), // Without a groups.plist, there's nothing to upgrade. + (_, Some(g), k) => { + let (groups, kerning) = + upconvert_kerning(&g, &k.unwrap_or_default(), &glyph_names); + validate_groups(&groups).map_err(Error::GroupsUpconversionError)?; + (Some(groups), Some(kerning)) + } + }; + + meta.format_version = FormatVersion::V3; + Ok(Ufo { layers, meta, diff --git a/src/upconversion.rs b/src/upconversion.rs new file mode 100644 index 00000000..2cfb2a43 --- /dev/null +++ b/src/upconversion.rs @@ -0,0 +1,408 @@ +use std::collections::{BTreeMap, HashMap, HashSet}; + +use crate::glyph::GlyphName; +use crate::ufo::{Groups, Kerning}; + +/// Convert kerning groups and pairs from v1 and v2 informal conventions to v3 formal conventions. +/// Converted groups are added (duplicated) rather than replacing the old ones to preserve all data +/// that external entities might rely on. Kerning pairs are updated to reflect the new group names. +/// +/// This is an adaptation from the fontTools.ufoLib reference implementation. It will not check if +/// the upgraded groups pass validation. +pub fn upconvert_kerning( + groups: &Groups, + kerning: &Kerning, + glyph_set: &HashSet, +) -> (Groups, Kerning) { + // Gather known kerning groups based on the prefixes. This will catch groups that exist in + // `groups` but are not referenced in `kerning`. + let (mut groups_first, mut groups_second) = find_known_kerning_groups(&groups); + + // Make lists of groups referenced in kerning pairs, based on their side. + for (first, seconds) in kerning { + if groups.contains_key(first) + && !glyph_set.contains(first.as_str()) + && !first.starts_with("public.kern1.") + { + groups_first.insert(first.to_string()); + } + for second in seconds.keys() { + if groups.contains_key(second) + && !glyph_set.contains(second.as_str()) + && !second.starts_with("public.kern2.") + { + groups_second.insert(second.to_string()); + } + } + } + + // Duplicate kerning groups with a new name. + let mut groups_new = groups.clone(); + + let mut groups_first_old_to_new: HashMap<&String, String> = HashMap::new(); + for first in &groups_first { + let first_new = make_unique_group_name( + format!("public.kern1.{}", first.replace("@MMK_L_", "")), + &groups_new, + ); + groups_first_old_to_new.insert(first, first_new.to_string()); + groups_new.insert(first_new, groups_new.get(first).unwrap().clone()); + } + let mut groups_second_old_to_new: HashMap<&String, String> = HashMap::new(); + for second in &groups_second { + let second_new = make_unique_group_name( + format!("public.kern2.{}", second.replace("@MMK_R_", "")), + &groups_new, + ); + groups_second_old_to_new.insert(second, second_new.to_string()); + groups_new.insert(second_new, groups_new.get(second).unwrap().clone()); + } + + // Update all kerning pairs that have an old kerning group in them with the new name. + let mut kerning_new: Kerning = Kerning::new(); + + for (first, seconds) in kerning { + let first_new = groups_first_old_to_new.get(first).unwrap_or(first); + let mut seconds_new: BTreeMap = BTreeMap::new(); + for (second, value) in seconds { + let second_new = groups_second_old_to_new.get(second).unwrap_or(second); + seconds_new.insert(second_new.to_string(), *value); + } + kerning_new.insert(first_new.to_string(), seconds_new); + } + + (groups_new, kerning_new) +} + +fn make_unique_group_name(name: String, existing_groups: &Groups) -> String { + if !existing_groups.contains_key(&name) { + return name; + } + + let mut counter = 1; + let mut new_name = name.to_string(); + while existing_groups.contains_key(&new_name) { + new_name = format!("{}{}", name, counter); + counter += 1; + } + + new_name +} + +fn find_known_kerning_groups(groups: &Groups) -> (HashSet, HashSet) { + let mut groups_first: HashSet = HashSet::new(); + let mut groups_second: HashSet = HashSet::new(); + + for name in groups.keys() { + if name.starts_with("@MMK_L_") { + groups_first.insert(name.to_string()); + } else if name.starts_with("@MMK_R_") { + groups_second.insert(name.to_string()); + } + } + + (groups_first, groups_second) +} + +#[cfg(test)] +mod tests { + extern crate maplit; + + use super::*; + use crate::ufo::{FormatVersion, Ufo}; + use maplit::btreemap; + + #[test] + fn test_upconvert_kerning_just_groups() { + let groups: Groups = btreemap! { + "@MMK_L_1".into() => vec!["a".into()], + "@MMK_L_2".into() => vec!["b".into()], + "@MMK_L_3".into() => vec!["c".into()], + "@MMK_R_1".into() => vec!["d".into()], + "@MMK_R_2".into() => vec!["e".into()], + "@MMK_R_3".into() => vec!["f".into()], + "@MMK_l_1".into() => vec!["g".into(), "h".into()], + "@MMK_r_1".into() => vec!["i".into()], + "@MMK_X_1".into() => vec!["j".into()], + "foo".into() => vec![], + }; + let kerning: Kerning = Kerning::new(); + let glyph_set: HashSet = vec![ + "a".into(), + "b".into(), + "c".into(), + "d".into(), + "e".into(), + "f".into(), + "g".into(), + "h".into(), + "i".into(), + "j".into(), + ] + .into_iter() + .collect(); + + let (groups_new, kerning_new) = upconvert_kerning(&groups, &kerning, &glyph_set); + + assert_eq!( + groups_new, + btreemap! { + "@MMK_L_1".into() => vec!["a".into()], + "@MMK_L_2".into() => vec!["b".into()], + "@MMK_L_3".into() => vec!["c".into()], + "@MMK_R_1".into() => vec!["d".into()], + "@MMK_R_2".into() => vec!["e".into()], + "@MMK_R_3".into() => vec!["f".into()], + "@MMK_l_1".into() => vec!["g".into(),"h".into()], + "@MMK_r_1".into() => vec!["i".into()], + "@MMK_X_1".into() => vec!["j".into()], + "foo".into() => vec![], + "public.kern1.1".into() => vec!["a".into()], + "public.kern1.2".into() => vec!["b".into()], + "public.kern1.3".into() => vec!["c".into()], + "public.kern2.1".into() => vec!["d".into()], + "public.kern2.2".into() => vec!["e".into()], + "public.kern2.3".into() => vec!["f".into()], + } + ); + assert_eq!(kerning_new, kerning); + } + + #[test] + fn test_upconvert_kerning_unknown_prefixes() { + let groups: Groups = btreemap! { + "BGroup".into() => vec!["B".into()], + "CGroup".into() => vec!["C".into()], + "DGroup".into() => vec!["D".into()], + }; + let kerning: Kerning = btreemap! { + "A".into() => btreemap!{ + "A".into() => 1.0, + "B".into() => 2.0, + "CGroup".into() => 3.0, + "DGroup".into() => 4.0, + }, + "BGroup".into() => btreemap!{ + "A".into() => 5.0, + "B".into() => 6.0, + "CGroup".into() => 7.0, + "DGroup".into() => 8.0, + }, + "CGroup".into() => btreemap!{ + "A".into() => 9.0, + "B".into() => 10.0, + "CGroup".into() => 11.0, + "DGroup".into() => 12.0, + }, + }; + let glyph_set: HashSet = HashSet::new(); + + let (groups_new, kerning_new) = upconvert_kerning(&groups, &kerning, &glyph_set); + + assert_eq!( + groups_new, + btreemap! { + "BGroup".into() => vec!["B".into()], + "CGroup".into() => vec!["C".into()], + "DGroup".into() => vec!["D".into()], + "public.kern1.BGroup".into() => vec!["B".into()], + "public.kern1.CGroup".into() => vec!["C".into()], + "public.kern2.CGroup".into() => vec!["C".into()], + "public.kern2.DGroup".into() => vec!["D".into()], + } + ); + assert_eq!( + kerning_new, + btreemap! { + "A".into() => btreemap!{ + "A".into() => 1.0, + "B".into() => 2.0, + "public.kern2.CGroup".into() => 3.0, + "public.kern2.DGroup".into() => 4.0, + }, + "public.kern1.BGroup".into() => btreemap!{ + "A".into() => 5.0, + "B".into() => 6.0, + "public.kern2.CGroup".into() => 7.0, + "public.kern2.DGroup".into() => 8.0, + }, + "public.kern1.CGroup".into() => btreemap!{ + "A".into() => 9.0, + "B".into() => 10.0, + "public.kern2.CGroup".into() => 11.0, + "public.kern2.DGroup".into() => 12.0, + } + } + ); + } + + #[test] + fn test_upconvert_kerning_known_prefixes() { + let groups: Groups = btreemap! { + "@MMK_L_BGroup".into() => vec!["B".into()], + "@MMK_L_CGroup".into() => vec!["C".into()], + "@MMK_L_XGroup".into() => vec!["X".into()], + "@MMK_R_CGroup".into() => vec!["C".into()], + "@MMK_R_DGroup".into() => vec!["D".into()], + "@MMK_R_XGroup".into() => vec!["X".into()], + }; + let kerning: Kerning = btreemap! { + "A".into() => btreemap!{ + "A".into() => 1.0, + "B".into() => 2.0, + "@MMK_R_CGroup".into() => 3.0, + "@MMK_R_DGroup".into() => 4.0, + }, + "@MMK_L_BGroup".into() => btreemap!{ + "A".into() => 5.0, + "B".into() => 6.0, + "@MMK_R_CGroup".into() => 7.0, + "@MMK_R_DGroup".into() => 8.0, + }, + "@MMK_L_CGroup".into() => btreemap!{ + "A".into() => 9.0, + "B".into() => 10.0, + "@MMK_R_CGroup".into() => 11.0, + "@MMK_R_DGroup".into() => 12.0, + }, + }; + let glyph_set: HashSet = HashSet::new(); + + let (groups_new, kerning_new) = upconvert_kerning(&groups, &kerning, &glyph_set); + + assert_eq!( + groups_new, + btreemap! { + "@MMK_L_BGroup".into() => vec!["B".into()], + "@MMK_L_CGroup".into() => vec!["C".into()], + "@MMK_L_XGroup".into() => vec!["X".into()], + "@MMK_R_CGroup".into() => vec!["C".into()], + "@MMK_R_DGroup".into() => vec!["D".into()], + "@MMK_R_XGroup".into() => vec!["X".into()], + "public.kern1.BGroup".into() => vec!["B".into()], + "public.kern1.CGroup".into() => vec!["C".into()], + "public.kern1.XGroup".into() => vec!["X".into()], + "public.kern2.CGroup".into() => vec!["C".into()], + "public.kern2.DGroup".into() => vec!["D".into()], + "public.kern2.XGroup".into() => vec!["X".into()], + } + ); + assert_eq!( + kerning_new, + btreemap! { + "A".into() => btreemap!{ + "A".into() => 1.0, + "B".into() => 2.0, + "public.kern2.CGroup".into() => 3.0, + "public.kern2.DGroup".into() => 4.0, + }, + "public.kern1.BGroup".into() => btreemap!{ + "A".into() => 5.0, + "B".into() => 6.0, + "public.kern2.CGroup".into() => 7.0, + "public.kern2.DGroup".into() => 8.0, + }, + "public.kern1.CGroup".into() => btreemap!{ + "A".into() => 9.0, + "B".into() => 10.0, + "public.kern2.CGroup".into() => 11.0, + "public.kern2.DGroup".into() => 12.0, + } + } + ); + } + + #[test] + fn test_upconvert_kerning_mixed_prefixes() { + let groups: Groups = btreemap! { + "BGroup".into() => vec!["B".into()], + "@MMK_L_CGroup".into() => vec!["C".into()], + "@MMK_R_CGroup".into() => vec!["C".into()], + "DGroup".into() => vec!["D".into()], + }; + let kerning: Kerning = btreemap! { + "A".into() => btreemap!{ + "A".into() => 1.0, + "B".into() => 2.0, + "@MMK_R_CGroup".into() => 3.0, + "DGroup".into() => 4.0, + }, + "BGroup".into() => btreemap!{ + "A".into() => 5.0, + "B".into() => 6.0, + "@MMK_R_CGroup".into() => 7.0, + "DGroup".into() => 8.0, + }, + "@MMK_L_CGroup".into() => btreemap!{ + "A".into() => 9.0, + "B".into() => 10.0, + "@MMK_R_CGroup".into() => 11.0, + "DGroup".into() => 12.0, + }, + }; + let glyph_set: HashSet = HashSet::new(); + + let (groups_new, kerning_new) = upconvert_kerning(&groups, &kerning, &glyph_set); + + assert_eq!( + groups_new, + btreemap! { + "BGroup".into() => vec!["B".into()], + "@MMK_L_CGroup".into() => vec!["C".into()], + "@MMK_R_CGroup".into() => vec!["C".into()], + "DGroup".into() => vec!["D".into()], + "public.kern1.BGroup".into() => vec!["B".into()], + "public.kern1.CGroup".into() => vec!["C".into()], + "public.kern2.CGroup".into() => vec!["C".into()], + "public.kern2.DGroup".into() => vec!["D".into()], + } + ); + assert_eq!( + kerning_new, + btreemap! { + "A".into() => btreemap!{ + "A".into() => 1.0, + "B".into() => 2.0, + "public.kern2.CGroup".into() => 3.0, + "public.kern2.DGroup".into() => 4.0, + }, + "public.kern1.BGroup".into() => btreemap!{ + "A".into() => 5.0, + "B".into() => 6.0, + "public.kern2.CGroup".into() => 7.0, + "public.kern2.DGroup".into() => 8.0, + }, + "public.kern1.CGroup".into() => btreemap!{ + "A".into() => 9.0, + "B".into() => 10.0, + "public.kern2.CGroup".into() => 11.0, + "public.kern2.DGroup".into() => 12.0, + } + } + ); + } + + #[test] + fn test_upconvert_kerning_glyphname_groupname() { + let ufo_v1 = + Ufo::load("testdata/upconversion_kerning/glyphname_groupname_UFOv1.ufo").unwrap(); + let ufo_v2 = + Ufo::load("testdata/upconversion_kerning/glyphname_groupname_UFOv2.ufo").unwrap(); + + let groups_expected: Groups = plist::from_file( + "testdata/upconversion_kerning/glyphname_groupname_groups_expected.plist", + ) + .unwrap(); + let kerning_expected: Kerning = plist::from_file( + "testdata/upconversion_kerning/glyphname_groupname_kerning_expected.plist", + ) + .unwrap(); + + assert_eq!(ufo_v1.meta.format_version, FormatVersion::V3); + assert_eq!(ufo_v2.meta.format_version, FormatVersion::V3); + assert_eq!(ufo_v1.groups.unwrap(), groups_expected); + assert_eq!(ufo_v2.groups.unwrap(), groups_expected); + assert_eq!(ufo_v1.kerning.unwrap(), kerning_expected); + assert_eq!(ufo_v2.kerning.unwrap(), kerning_expected); + } +}