Skip to content
Draft
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
202 changes: 202 additions & 0 deletions src/designspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,18 @@ pub struct DesignSpaceDocument {
pub axes: Vec<Axis>,
/// Optional avar2-style axis mappings.
pub axis_mappings: Option<AxisMappings>,
/// Optional freestanding location labels.
pub location_labels: Vec<LocationLabel>,
/// One or more rules.
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<VariableFont>,
/// One or more sources.
pub sources: Vec<Source>,
/// One or more instances.
Expand Down Expand Up @@ -66,8 +76,63 @@ pub struct Axis {
/// Localised UI strings for the axis name.
#[serde(rename = "labelname", default, skip_serializing_if = "Vec::is_empty")]
pub label_names: Vec<LocalizedString>,
/// UI strings for axis stops.
// TODO: Get rid of AxisLabels and hoist content into this struct.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub labels: Option<AxisLabels>,
}

/// ...
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct AxisLabels {
#[serde(rename = "ordering", default, skip_serializing_if = "Option::is_none")]
pub axis_ordering: Option<u32>,
#[serde(with = "serde_impls::axis_label", default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<AxisLabel>,
}

/// ...
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct AxisLabel {
/// ...
#[serde(rename = "@name")]
pub name: String,
/// ...
#[serde(rename = "@elidable", default, skip_serializing_if = "is_false")]
pub elidable: bool,
/// ...
#[serde(rename = "@oldersibling", default, skip_serializing_if = "is_false")]
pub older_sibling: bool,
/// ...
#[serde(rename = "@uservalue")]
pub user_value: f32,
/// ...
#[serde(rename = "@userminimum", default)]
pub user_minimum: Option<f32>,
/// ...
#[serde(rename = "@usermaximum", default)]
pub user_maximum: Option<f32>,
/// ...
#[serde(rename = "@linkeduservalue", default)]
pub linked_user_value: Option<f32>,
/// ...
#[serde(rename = "labelname", default, skip_serializing_if = "Vec::is_empty")]
pub label_names: Vec<LocalizedString>,
}

/// ...
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct LocationLabel {
/// ...
#[serde(rename = "@name")]
pub name: String,
/// ...
#[serde(rename = "labelname", default, skip_serializing_if = "Vec::is_empty")]
pub label_names: Vec<LocalizedString>,
/// ...
#[serde(with = "serde_impls::location")]
pub location: Vec<Dimension>,
}
fn is_false(value: &bool) -> bool {
!(*value)
}
Expand Down Expand Up @@ -246,6 +311,53 @@ pub struct Condition {
pub maximum: Option<f32>,
}

/// 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<String>,
/// The axis subset the variable font represents.
#[serde(rename = "axis-subsets", with = "serde_impls::axis_subsets")]
pub axis_subsets: Vec<AxisSubset>,
/// 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)]
#[serde(untagged)]
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")]
user_minimum: Option<f64>,
/// Optionally, the upper end of the range, in user coordinates
#[serde(rename = "@usermaximum", default, skip_serializing_if = "Option::is_none")]
user_maximum: Option<f64>,
/// Optionally, the new default value of subset axis, in user coordinates.
#[serde(rename = "@userdefault", default, skip_serializing_if = "Option::is_none")]
user_default: Option<f64>,
},
/// 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")]
user_value: f64,
},
}

/// A [source].
///
/// [source]: https://fonttools.readthedocs.io/en/latest/designspaceLib/xml.html#id25
Expand Down Expand Up @@ -274,6 +386,8 @@ pub struct Source {
/// Location in designspace coordinates.
#[serde(with = "serde_impls::location")]
pub location: Vec<Dimension>,
#[serde(rename = "familyname", skip_serializing_if = "Vec::is_empty")]
pub localised_familynames: Vec<LocalizedString>,
}

/// An [instance].
Expand Down Expand Up @@ -310,6 +424,8 @@ pub struct Instance {
/// Arbitrary data about this instance
#[serde(default, with = "serde_plist", skip_serializing_if = "Dictionary::is_empty")]
pub lib: Dictionary,
#[serde(rename = "@location", skip_serializing_if = "Option::is_none")]
pub named_location: Option<String>,
}

/// A design space dimension.
Expand Down Expand Up @@ -388,6 +504,8 @@ struct RawDesignSpaceDocument {
format: f32,
#[serde(default, skip_serializing_if = "RawAxes::is_empty")]
axes: RawAxes,
#[serde(default, with = "serde_impls::location_labels", skip_serializing_if = "Vec::is_empty")]
location_labels: Vec<LocationLabel>,
#[serde(default, skip_serializing_if = "Rules::is_empty")]
rules: Rules,
#[serde(default, with = "serde_impls::sources", skip_serializing_if = "Vec::is_empty")]
Expand All @@ -401,6 +519,8 @@ struct RawDesignSpaceDocument {
/// Internal container for axes and their optional mappings.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
struct RawAxes {
#[serde(default, rename = "elidedfallbackname", skip_serializing_if = "Option::is_none")]
elided_fallback_name: Option<String>,
#[serde(default, rename = "axis")]
axis: Vec<Axis>,
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -501,6 +621,7 @@ mod serde_impls {
{
use serde::Deserialize as _;
#[derive(::serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
struct Helper {
$field_name: Vec<$inner>,
}
Expand All @@ -516,6 +637,7 @@ mod serde_impls {
{
use serde::Serialize as _;
#[derive(::serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct Helper<'a> {
$field_name: &'a [$inner],
}
Expand All @@ -529,6 +651,11 @@ mod serde_impls {
serde_from_field!(location, dimension, crate::designspace::Dimension);
serde_from_field!(instances, instance, crate::designspace::Instance);
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);
serde_from_field!(labels, label, crate::designspace::AxisLabels);
serde_from_field!(axis_label, label, crate::designspace::AxisLabel);
serde_from_field!(location_labels, label, crate::designspace::LocationLabel);
}

#[cfg(test)]
Expand Down Expand Up @@ -886,4 +1013,79 @@ mod tests {
let ds = DesignSpaceDocument::load("testdata/wght.designspace").unwrap();
assert!(ds.axis_mappings.is_none());
}

#[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.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![],
labels: 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 { lang: "fa-IR".into(), name: "قطر".into() },
LabelName { lang: "en".into(), name: "Wéíght".into() },
],
labels: vec![]
}
]
);

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);
}
}
2 changes: 2 additions & 0 deletions src/fontinfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,8 @@ struct FontInfoV1 {
year: Option<Integer>, // Does not appear in spec but ufoLib.
}

// TODO: add from_bytes method for loading fontinfo from designspace libs

impl FontInfo {
pub(crate) fn from_bytes(
data: &[u8],
Expand Down
Loading