From 1c5f6b0ee0cd1771f73b67d34f5bb4f52c31378a Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 22 Aug 2023 15:55:41 +0100 Subject: [PATCH 1/3] Ser/de Designspace rules --- src/designspace.rs | 104 +++++++++++ testdata/MutatorSans.designspace | 285 +++++++++++++++++++++++++++++++ 2 files changed, 389 insertions(+) create mode 100644 testdata/MutatorSans.designspace diff --git a/src/designspace.rs b/src/designspace.rs index 5ac0941d..9e56c565 100644 --- a/src/designspace.rs +++ b/src/designspace.rs @@ -22,6 +22,9 @@ pub struct DesignSpaceDocument { /// One or more axes. #[serde(with = "serde_impls::axes", skip_serializing_if = "Vec::is_empty")] pub axes: Vec, + /// One or more rules. + #[serde(default, with = "serde_impls::rules", skip_serializing_if = "Vec::is_empty")] + pub rules: Vec, /// One or more sources. #[serde(with = "serde_impls::sources", skip_serializing_if = "Vec::is_empty")] pub sources: Vec, @@ -78,6 +81,56 @@ pub struct AxisMapping { pub output: f32, } +/// Describes a single set of substitution rules. +/// +/// Does not support standalone elements outside a . +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename = "rule")] +pub struct Rule { + /// Name of the rule. + #[serde(rename = "@name")] + pub name: Option, + /// Condition sets. If any condition is true, the rule is applied. + #[serde(rename = "conditionset")] + pub condition_sets: Vec, + /// Subtitutions (in, out). + #[serde(rename = "sub")] + pub substitutions: Vec, +} + +/// Describes a single set of substitution rules. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct Substitution { + /// Substitute this glyph... + #[serde(rename = "@name")] + pub name: String, + /// ...with this one. + #[serde(rename = "@with")] + pub with: String, +} + +/// Describes a single set of substitution rules. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct ConditionSet { + /// Substitute this glyph... + #[serde(rename = "condition")] + pub conditions: Vec, +} + +/// Describes a single set of substitution rules. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct Condition { + /// Substitute this glyph... + #[serde(rename = "@name")] + pub name: String, + /// Minimum in design space coordinates. If omitted, assumed to be -infinity. + #[serde(rename = "@minimum", default, skip_serializing_if = "Option::is_none")] + pub minimum: Option, + /// Maximum in design space coordinates. If omitted, assumed to be infinity to mean . + #[serde(rename = "@maximum", default, skip_serializing_if = "Option::is_none")] + pub maximum: Option, +} + /// A [source]. /// /// [source]: https://fonttools.readthedocs.io/en/latest/designspaceLib/xml.html#id25 @@ -273,6 +326,7 @@ 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!(rules, rule, crate::designspace::Rule); } #[cfg(test)] @@ -391,4 +445,54 @@ mod tests { // Then assert_eq!(ds_initial, ds_after); } + + #[test] + fn load_save_round_trip_mutatorsans() { + // Given + let dir = tempdir::TempDir::new("norad_designspace_load_save_round_trip_ms").unwrap(); + let ds_test_save_location = dir.path().join("MutatorSans.designspace"); + + // When + let ds_initial = DesignSpaceDocument::load("testdata/MutatorSans.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"); + + // Then + assert_eq!( + &ds_after.rules, + &[ + Rule { + name: Some("fold_I_serifs".into()), + condition_sets: vec![ConditionSet { + conditions: vec![Condition { + name: "width".into(), + minimum: Some(0.0), + maximum: Some(328.0), + }], + }], + substitutions: vec![Substitution { name: "I".into(), with: "I.narrow".into() }], + }, + Rule { + name: Some("fold_S_terminals".into()), + condition_sets: vec![ConditionSet { + conditions: vec![ + Condition { + name: "width".into(), + minimum: Some(0.0), + maximum: Some(1000.0), + }, + Condition { + name: "weight".into(), + minimum: Some(0.0), + maximum: Some(500.0), + }, + ], + }], + substitutions: vec![Substitution { name: "S".into(), with: "S.closed".into() }], + }, + ] + ); + assert_eq!(ds_initial, ds_after); + } } diff --git a/testdata/MutatorSans.designspace b/testdata/MutatorSans.designspace new file mode 100644 index 00000000..aeaaacc8 --- /dev/null +++ b/testdata/MutatorSans.designspace @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.letterror.mathModelPref + previewMutatorMath + com.letterror.skateboard.interactionSources + + horizontal + + width + + ignore + + vertical + + weight + + + com.letterror.skateboard.interestingLocation + + + + weight + 775.609 + width + 794.522 + + S1 + + + + weight + 855.549 + width + 795.978 + + S2 + + + + weight + 1194.939375384999 + width + 898.8087507107668 + + S3 + + + + weight + 161.67457510442006 + width + 404.05720203707176 + + This is horrible + + + + weight + 467.73409948979594 + width + 538.7016581632644 + + Stem == 200? + + + + weight + 658.597 + width + 93.05199999999985 + + My_new_location + + + com.letterror.skateboard.previewLocation + + weight + 177.33442834877678 + width + 747.3306156281592 + + com.letterror.skateboard.previewText + HE + com.superpolator.data + + axiscolors + + weight + + 0.5 + 0.5 + 0.5 + 1.0 + + width + + 0.5 + 0.5 + 0.5 + 1.0 + + + instancefolder + instances + lineInverted + + lineStacked + sequence + lineViewFilled + + previewtext + SUPER + snippets + + + + + From fecac967939c7064d7e22152d10fbb7c8b693a97 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 22 Aug 2023 17:01:52 +0100 Subject: [PATCH 2/3] Deal with the processing attribute Co-authored-by: Ricky Atkins --- src/designspace.rs | 123 ++++++++++++++++++++----------- testdata/MutatorSans.designspace | 2 +- 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/src/designspace.rs b/src/designspace.rs index 9e56c565..7b1b6b00 100644 --- a/src/designspace.rs +++ b/src/designspace.rs @@ -23,8 +23,8 @@ pub struct DesignSpaceDocument { #[serde(with = "serde_impls::axes", skip_serializing_if = "Vec::is_empty")] pub axes: Vec, /// One or more rules. - #[serde(default, with = "serde_impls::rules", skip_serializing_if = "Vec::is_empty")] - pub rules: Vec, + #[serde(default, skip_serializing_if = "Rules::is_empty")] + pub rules: Rules, /// One or more sources. #[serde(with = "serde_impls::sources", skip_serializing_if = "Vec::is_empty")] pub sources: Vec, @@ -81,11 +81,37 @@ pub struct AxisMapping { pub output: f32, } +/// Describes the substitution [rules] of the Designspace. +/// +/// [rules]: https://fonttools.readthedocs.io/en/latest/designspaceLib/xml.html#rules-element +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct Rules { + /// Indicates whether substitution rules should be applied before or after + /// other glyph substitution features. + #[serde(rename = "@processing")] + pub processing: RuleProcessing, + /// The rules. + #[serde(default, rename = "rule")] + pub rules: Vec, +} + +/// Indicates whether substitution rules should be applied before or after other +/// glyph substitution features. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RuleProcessing { + /// Apply before other substitution features. + #[default] + First, + /// Apply after other substitution features. + Last, +} + /// Describes a single set of substitution rules. /// -/// Does not support standalone elements outside a . +/// Does not support standalone `` elements outside a +/// ``. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename = "rule")] pub struct Rule { /// Name of the rule. #[serde(rename = "@name")] @@ -98,7 +124,7 @@ pub struct Rule { pub substitutions: Vec, } -/// Describes a single set of substitution rules. +/// Describes a single substitution. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct Substitution { /// Substitute this glyph... @@ -109,24 +135,24 @@ pub struct Substitution { pub with: String, } -/// Describes a single set of substitution rules. +/// Describes a set of conditions that must all be met for the rule to apply. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct ConditionSet { - /// Substitute this glyph... + /// The conditions. #[serde(rename = "condition")] pub conditions: Vec, } -/// Describes a single set of substitution rules. +/// Describes a single condition. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct Condition { - /// Substitute this glyph... + /// The name of the axis. #[serde(rename = "@name")] pub name: String, - /// Minimum in design space coordinates. If omitted, assumed to be -infinity. + /// Lower bounds in design space coordinates. #[serde(rename = "@minimum", default, skip_serializing_if = "Option::is_none")] pub minimum: Option, - /// Maximum in design space coordinates. If omitted, assumed to be infinity to mean . + /// Upper bounds in design space coordinates. #[serde(rename = "@maximum", default, skip_serializing_if = "Option::is_none")] pub maximum: Option, } @@ -236,6 +262,13 @@ impl DesignSpaceDocument { } } +impl Rules { + /// Returns `true` if there are no rules. + fn is_empty(&self) -> bool { + self.rules.is_empty() + } +} + mod serde_impls { /// Produces a self-contained module to (de)serialise an XML list of a given type /// @@ -326,7 +359,6 @@ 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!(rules, rule, crate::designspace::Rule); } #[cfg(test)] @@ -461,37 +493,46 @@ mod tests { // Then assert_eq!( &ds_after.rules, - &[ - Rule { - name: Some("fold_I_serifs".into()), - condition_sets: vec![ConditionSet { - conditions: vec![Condition { - name: "width".into(), - minimum: Some(0.0), - maximum: Some(328.0), - }], - }], - substitutions: vec![Substitution { name: "I".into(), with: "I.narrow".into() }], - }, - Rule { - name: Some("fold_S_terminals".into()), - condition_sets: vec![ConditionSet { - conditions: vec![ - Condition { + &Rules { + processing: RuleProcessing::Last, + rules: vec![ + Rule { + name: Some("fold_I_serifs".into()), + condition_sets: vec![ConditionSet { + conditions: vec![Condition { name: "width".into(), minimum: Some(0.0), - maximum: Some(1000.0), - }, - Condition { - name: "weight".into(), - minimum: Some(0.0), - maximum: Some(500.0), - }, - ], - }], - substitutions: vec![Substitution { name: "S".into(), with: "S.closed".into() }], - }, - ] + maximum: Some(328.0), + }], + }], + substitutions: vec![Substitution { + name: "I".into(), + with: "I.narrow".into() + }], + }, + Rule { + name: Some("fold_S_terminals".into()), + condition_sets: vec![ConditionSet { + conditions: vec![ + Condition { + name: "width".into(), + minimum: Some(0.0), + maximum: Some(1000.0), + }, + Condition { + name: "weight".into(), + minimum: Some(0.0), + maximum: Some(500.0), + }, + ], + }], + substitutions: vec![Substitution { + name: "S".into(), + with: "S.closed".into() + }], + }, + ] + } ); assert_eq!(ds_initial, ds_after); } diff --git a/testdata/MutatorSans.designspace b/testdata/MutatorSans.designspace index aeaaacc8..24442709 100644 --- a/testdata/MutatorSans.designspace +++ b/testdata/MutatorSans.designspace @@ -4,7 +4,7 @@ - + From 89672547b972a506b95e76e8763f918da62b780b Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 22 Aug 2023 18:34:48 +0100 Subject: [PATCH 3/3] Use glyph `Name`s for substitutions --- src/designspace.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/designspace.rs b/src/designspace.rs index 7b1b6b00..9d45c09a 100644 --- a/src/designspace.rs +++ b/src/designspace.rs @@ -9,6 +9,7 @@ use plist::Dictionary; use crate::error::{DesignSpaceLoadError, DesignSpaceSaveError}; use crate::serde_xml_plist as serde_plist; +use crate::Name; /// A [designspace]. /// @@ -125,14 +126,14 @@ pub struct Rule { } /// Describes a single substitution. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Substitution { /// Substitute this glyph... #[serde(rename = "@name")] - pub name: String, + pub name: Name, /// ...with this one. #[serde(rename = "@with")] - pub with: String, + pub with: Name, } /// Describes a set of conditions that must all be met for the rule to apply.