diff --git a/src/designspace.rs b/src/designspace.rs index 5ac0941d..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]. /// @@ -22,6 +23,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, 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, @@ -78,6 +82,82 @@ 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 +/// ``. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +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 substitution. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Substitution { + /// Substitute this glyph... + #[serde(rename = "@name")] + pub name: Name, + /// ...with this one. + #[serde(rename = "@with")] + pub with: Name, +} + +/// 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 { + /// The conditions. + #[serde(rename = "condition")] + pub conditions: Vec, +} + +/// Describes a single condition. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct Condition { + /// The name of the axis. + #[serde(rename = "@name")] + pub name: String, + /// Lower bounds in design space coordinates. + #[serde(rename = "@minimum", default, skip_serializing_if = "Option::is_none")] + pub minimum: Option, + /// Upper bounds in design space coordinates. + #[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 @@ -183,6 +263,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 /// @@ -391,4 +478,63 @@ 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, + &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(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..24442709 --- /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 + + + + +