From 602f61e79f9fafcf7818b9f69c17ee5c3645081a Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Sun, 18 Feb 2024 21:20:26 +0800 Subject: [PATCH 1/7] Fix date and data deserialization from a Designspace file --- src/serde_xml_plist.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/serde_xml_plist.rs b/src/serde_xml_plist.rs index 5638be81..42c8ac22 100644 --- a/src/serde_xml_plist.rs +++ b/src/serde_xml_plist.rs @@ -32,6 +32,7 @@ mod de { use super::*; use serde::de::{Error as DeError, Visitor}; use serde::Deserialize; + use std::borrow::Cow; use std::fmt::Display; use std::marker::PhantomData; use std::str::FromStr; @@ -106,15 +107,21 @@ mod de { Some(ValueKeyword::Data) => { //FIXME: remove this + base64 dep when/if we merge // - let b64_str = map.next_value::<&str>()?; + // NOTE: Use Cow here because serde needs ownership if it has + // to modify the string in any form, e.g. when deserializing + // from a Designspace file. + let b64_str = map.next_value::>()?; base64_standard - .decode(b64_str) + .decode(&*b64_str) .map(Value::Data) .map_err(|e| A::Error::custom(format!("Invalid XML data: '{e}'"))) } Some(ValueKeyword::Date) => { - let date_str = map.next_value::<&str>()?; - plist::Date::from_xml_format(date_str).map_err(A::Error::custom).map(Value::Date) + // NOTE: Use Cow here because serde needs ownership if it has + // to modify the string in any form, e.g. when deserializing + // from a Designspace file. + let date_str = map.next_value::>()?; + plist::Date::from_xml_format(&date_str).map_err(A::Error::custom).map(Value::Date) } Some(ValueKeyword::Real) => map.next_value::().map(Value::Real), Some(ValueKeyword::Integer) => { From dc48ff26a3c37b4a8d70bfeb33dac2cde834b295 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Sun, 18 Feb 2024 22:00:37 +0800 Subject: [PATCH 2/7] WIP --- src/designspace.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++ src/fontinfo.rs | 2 + 2 files changed, 104 insertions(+) diff --git a/src/designspace.rs b/src/designspace.rs index bf312103..274364a8 100644 --- a/src/designspace.rs +++ b/src/designspace.rs @@ -26,6 +26,14 @@ pub struct DesignSpaceDocument { /// One or more rules. #[serde(default, skip_serializing_if = "Rules::is_empty")] pub rules: Rules, + /// Zero or more variable fonts. + #[serde( + rename = "variable-fonts", + with = "serde_impls::variable_fonts", + default, + skip_serializing_if = "Vec::is_empty" + )] + pub variable_fonts: Vec, /// One or more sources. #[serde(with = "serde_impls::sources", skip_serializing_if = "Vec::is_empty")] pub sources: Vec, @@ -162,6 +170,54 @@ pub struct Condition { pub maximum: Option, } +/// Describes a single variable font. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct VariableFont { + /// The name of the variable font. + #[serde(rename = "@name")] + pub name: String, + /// The optional file name of the variable font. + #[serde(rename = "@filename", default, skip_serializing_if = "Option::is_none")] + pub filename: Option, + /// The axis subset the variable font represents. + #[serde( + rename = "axis-subsets", + with = "serde_impls::axis_subsets", + skip_serializing_if = "Vec::is_empty" + )] + pub axis_subsets: Vec, + /// Additional arbitrary user data + #[serde(default, with = "serde_plist", skip_serializing_if = "Dictionary::is_empty")] + pub lib: Dictionary, +} + +/// Describes a single axis subset for a variable font. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum AxisSubset { + /// Describes a range of an axis. + Range { + /// The name of the axis under consideration. + name: String, + /// Optionally, the lower end of the range, in user coordinates + #[serde(rename = "@userminimum", default, skip_serializing_if = "Option::is_none")] + user_minimum: Option, + /// Optionally, the upper end of the range, in user coordinates + #[serde(rename = "@usermaximum", default, skip_serializing_if = "Option::is_none")] + user_maximum: Option, + /// Optionally, the new default value of subset axis, in user coordinates. + #[serde(rename = "@userdefault", default, skip_serializing_if = "Option::is_none")] + user_default: Option, + }, + /// Describes a single point of an axis. + Discrete { + /// The name of the axis under consideration. + name: String, + /// The single point of the axis. + #[serde(rename = "@uservalue")] + user_value: f64, + }, +} + /// A [source]. /// /// [source]: https://fonttools.readthedocs.io/en/latest/designspaceLib/xml.html#id25 @@ -335,6 +391,7 @@ mod serde_impls { { use ::serde::Deserialize as _; #[derive(::serde::Deserialize)] + #[serde(rename_all = "kebab-case")] struct Helper { $field_name: Vec<$inner>, } @@ -350,6 +407,7 @@ mod serde_impls { { use ::serde::Serialize as _; #[derive(::serde::Serialize)] + #[serde(rename_all = "kebab-case")] struct Helper<'a> { $field_name: &'a [$inner], } @@ -364,6 +422,8 @@ mod serde_impls { serde_from_field!(instances, instance, crate::designspace::Instance); serde_from_field!(axes, axis, crate::designspace::Axis); serde_from_field!(sources, source, crate::designspace::Source); + serde_from_field!(variable_fonts, variable_font, crate::designspace::VariableFont); + serde_from_field!(axis_subsets, axis_subset, crate::designspace::AxisSubset); } #[cfg(test)] @@ -575,4 +635,46 @@ mod tests { } ); } + + #[test] + fn load_save_round_trip_mutatorsans2() { + // Given + let dir = TempDir::new().unwrap(); + let ds_test_save_location = dir.path().join("MutatorSans2.designspace"); + + // When + let ds_initial = DesignSpaceDocument::load("testdata/MutatorSans2.designspace").unwrap(); + ds_initial.save(&ds_test_save_location).expect("failed to save designspace"); + let ds_after = DesignSpaceDocument::load(ds_test_save_location) + .expect("failed to load saved designspace"); + + let mut vf_lib = Dictionary::new(); + let mut vf_fontinfo = Dictionary::new(); + vf_fontinfo.insert("familyName".into(), "My Font Narrow VF".into()); + vf_fontinfo.insert("styleName".into(), "Regular".into()); + vf_fontinfo.insert("postscriptFontName".into(), "MyFontNarrVF-Regular".into()); + vf_fontinfo + .insert("trademark".into(), "My Font Narrow VF is a registered trademark...".into()); + vf_lib.insert("public.fontInfo".into(), vf_fontinfo.into()); + + // Then + assert_eq!( + &ds_after.variable_fonts, + &[VariableFont { + name: "MyFontNarrVF".into(), + filename: None, + axis_subsets: vec![ + AxisSubset::Range { + name: "Weight".into(), + user_minimum: None, + user_maximum: None, + user_default: None + }, + AxisSubset::Discrete { name: "Width".into(), user_value: 75.0 }, + ], + lib: vf_lib + }] + ); + assert_eq!(ds_initial, ds_after); + } } diff --git a/src/fontinfo.rs b/src/fontinfo.rs index cf8a2ed8..d2b03206 100644 --- a/src/fontinfo.rs +++ b/src/fontinfo.rs @@ -489,6 +489,8 @@ struct FontInfoV1 { year: Option, // Does not appear in spec but ufoLib. } +// TODO: add from_bytes method for loading fontinfo from designspace libs + impl FontInfo { /// Returns [`FontInfo`] from a file, upgrading from the supplied `format_version` to the highest /// internally supported version. From ac0b0579d92b9e85b4cf6b37dc12f7b22d3d19e9 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Sun, 18 Feb 2024 22:14:36 +0800 Subject: [PATCH 3/7] WIP --- src/designspace.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/designspace.rs b/src/designspace.rs index 274364a8..2356c574 100644 --- a/src/designspace.rs +++ b/src/designspace.rs @@ -193,6 +193,7 @@ pub struct VariableFont { /// Describes a single axis subset for a variable font. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] pub enum AxisSubset { /// Describes a range of an axis. Range { From e15f8f2734b14a6ff94c038cad73b14dcd29343b Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 20 Feb 2024 20:07:25 +0800 Subject: [PATCH 4/7] WIP --- testdata/MutatorSans2.designspace | 217 ++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 testdata/MutatorSans2.designspace diff --git a/testdata/MutatorSans2.designspace b/testdata/MutatorSans2.designspace new file mode 100644 index 00000000..2f69ef0d --- /dev/null +++ b/testdata/MutatorSans2.designspace @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + public.fontInfo + + familyName + My Font Narrow VF + styleName + Regular + postscriptFontName + MyFontNarrVF-Regular + trademark + My Font Narrow VF is a registered trademark... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + hasLoadedLib + Absolutely! + anArray + + + class + aristocracy + heft + 42.42 + + 6 + + isWorking + + isBroken + + bestBefore + 2345-01-24T23:22:21Z + payload + + dSBnb3QgMHduZWQ= + + + + From 927f04a9cf7690d63934a72679f01fcdae5097ae Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 20 Feb 2024 20:49:17 +0800 Subject: [PATCH 5/7] WIP --- src/designspace.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/designspace.rs b/src/designspace.rs index 2356c574..46642321 100644 --- a/src/designspace.rs +++ b/src/designspace.rs @@ -180,11 +180,7 @@ pub struct VariableFont { #[serde(rename = "@filename", default, skip_serializing_if = "Option::is_none")] pub filename: Option, /// The axis subset the variable font represents. - #[serde( - rename = "axis-subsets", - with = "serde_impls::axis_subsets", - skip_serializing_if = "Vec::is_empty" - )] + #[serde(rename = "axis-subsets", with = "serde_impls::axis_subsets")] pub axis_subsets: Vec, /// Additional arbitrary user data #[serde(default, with = "serde_plist", skip_serializing_if = "Dictionary::is_empty")] @@ -198,6 +194,7 @@ pub enum AxisSubset { /// Describes a range of an axis. Range { /// The name of the axis under consideration. + #[serde(rename = "@name")] name: String, /// Optionally, the lower end of the range, in user coordinates #[serde(rename = "@userminimum", default, skip_serializing_if = "Option::is_none")] @@ -212,6 +209,7 @@ pub enum AxisSubset { /// Describes a single point of an axis. Discrete { /// The name of the axis under consideration. + #[serde(rename = "@name")] name: String, /// The single point of the axis. #[serde(rename = "@uservalue")] From 1cbb39d534ead958c2eb4dae4bced318c9dbc9b7 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 27 Feb 2024 17:19:12 +0800 Subject: [PATCH 6/7] labelname WIP --- src/designspace.rs | 45 +++++++++++++++++++++++++++++++ testdata/MutatorSans2.designspace | 5 +++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/designspace.rs b/src/designspace.rs index 46642321..f85d5fa8 100644 --- a/src/designspace.rs +++ b/src/designspace.rs @@ -75,6 +75,20 @@ pub struct Axis { /// Mapping between user space coordinates and design space coordinates. #[serde(skip_serializing_if = "Option::is_none")] pub map: Option>, + /// ... + #[serde(rename = "labelname", default, skip_serializing_if = "Vec::is_empty")] + pub label_names: Vec, +} + +/// Maps an xml:lang language tag to a localised name. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct LabelName { + /// Language tag. + #[serde(rename = "@lang")] + pub lang: String, + /// Localised name. + #[serde(rename = "$text")] + pub name: String, } fn is_false(value: &bool) -> bool { @@ -657,6 +671,37 @@ mod tests { vf_lib.insert("public.fontInfo".into(), vf_fontinfo.into()); // Then + assert_eq!( + &ds_after.axes, + &[ + Axis { + name: "width".into(), + tag: "wdth".into(), + default: 0.0, + hidden: false, + minimum: Some(0.0), + maximum: Some(1000.0), + values: None, + map: None, + label_names: vec![] + }, + Axis { + name: "weight".into(), + tag: "wght".into(), + default: 0.0, + hidden: false, + minimum: Some(0.0), + maximum: Some(1000.0), + values: None, + map: None, + label_names: vec![ + LabelName { xml_lang: "fa-IR".into(), name: "قطر".into() }, + LabelName { xml_lang: "en".into(), name: "Wéíght".into() }, + ] + } + ] + ); + assert_eq!( &ds_after.variable_fonts, &[VariableFont { diff --git a/testdata/MutatorSans2.designspace b/testdata/MutatorSans2.designspace index 2f69ef0d..991cfc7d 100644 --- a/testdata/MutatorSans2.designspace +++ b/testdata/MutatorSans2.designspace @@ -2,7 +2,10 @@ - + + قطر + Wéíght + From 05458b840528143e0c333841d639ff3b6895c873 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 27 Feb 2024 17:40:20 +0000 Subject: [PATCH 7/7] WIP --- src/designspace.rs | 43 ++++++++++++++++++++++++++++--- testdata/MutatorSans2.designspace | 7 +++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/designspace.rs b/src/designspace.rs index f85d5fa8..eef673d8 100644 --- a/src/designspace.rs +++ b/src/designspace.rs @@ -78,6 +78,9 @@ pub struct Axis { /// ... #[serde(rename = "labelname", default, skip_serializing_if = "Vec::is_empty")] pub label_names: Vec, + /// ... + #[serde(with = "serde_impls::labels", default, skip_serializing_if = "Vec::is_empty")] + pub labels: Vec