#![deny(rustdoc::broken_intra_doc_links)]
use std::{fs::File, io::BufReader, path::Path};
use crate::error::DesignSpaceLoadError;
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
#[serde(from = "DesignSpaceDocumentXmlRepr")]
pub struct DesignSpaceDocument {
pub format: f32,
pub axes: Vec<Axis>,
pub sources: Vec<Source>,
pub instances: Vec<Instance>,
}
#[derive(Deserialize)]
#[serde(rename = "designspace")]
struct DesignSpaceDocumentXmlRepr {
pub format: f32,
pub axes: AxesXmlRepr,
pub sources: SourcesXmlRepr,
pub instances: InstancesXmlRepr,
}
impl From<DesignSpaceDocumentXmlRepr> for DesignSpaceDocument {
fn from(xml_form: DesignSpaceDocumentXmlRepr) -> Self {
DesignSpaceDocument {
format: xml_form.format,
axes: xml_form.axes.axis,
sources: xml_form.sources.source,
instances: xml_form.instances.instance,
}
}
}
#[derive(Deserialize)]
#[serde(rename = "axes")]
pub struct AxesXmlRepr {
pub axis: Vec<Axis>,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
#[serde(rename = "axis")]
pub struct Axis {
pub name: String,
pub tag: String,
pub default: f32,
#[serde(default)]
pub hidden: bool,
pub minimum: Option<f32>,
pub maximum: Option<f32>,
pub values: Option<Vec<f32>>,
pub map: Option<Vec<AxisMapping>>,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
#[serde(rename = "map")]
pub struct AxisMapping {
pub input: f32,
pub output: f32,
}
#[derive(Deserialize)]
#[serde(rename = "sources")]
struct SourcesXmlRepr {
pub source: Vec<Source>,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
#[serde(from = "SourceXmlRepr")]
pub struct Source {
pub familyname: Option<String>,
pub stylename: Option<String>,
pub name: String,
pub filename: String,
pub layer: Option<String>,
pub location: Vec<Dimension>,
}
#[derive(Deserialize)]
#[serde(rename = "source")]
struct SourceXmlRepr {
pub familyname: Option<String>,
pub stylename: Option<String>,
pub name: String,
pub filename: String,
pub layer: Option<String>,
pub location: LocationXmlRepr,
}
impl From<SourceXmlRepr> for Source {
fn from(xml_form: SourceXmlRepr) -> Self {
Source {
familyname: xml_form.familyname,
stylename: xml_form.stylename,
name: xml_form.name,
filename: xml_form.filename,
layer: xml_form.layer,
location: xml_form.location.dimension,
}
}
}
#[derive(Deserialize)]
#[serde(rename = "instances")]
struct InstancesXmlRepr {
pub instance: Vec<Instance>,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
#[serde(from = "InstanceXmlRepr")]
pub struct Instance {
pub familyname: Option<String>,
pub stylename: Option<String>,
pub name: String,
pub filename: Option<String>,
pub postscriptfontname: Option<String>,
pub stylemapfamilyname: Option<String>,
pub stylemapstylename: Option<String>,
pub location: Vec<Dimension>,
}
#[derive(Deserialize)]
struct InstanceXmlRepr {
pub familyname: Option<String>,
pub stylename: Option<String>,
pub name: String,
pub filename: Option<String>,
pub postscriptfontname: Option<String>,
pub stylemapfamilyname: Option<String>,
pub stylemapstylename: Option<String>,
pub location: LocationXmlRepr,
}
impl From<InstanceXmlRepr> for Instance {
fn from(instance_xml: InstanceXmlRepr) -> Self {
Instance {
familyname: instance_xml.familyname,
stylename: instance_xml.stylename,
name: instance_xml.name,
filename: instance_xml.filename,
postscriptfontname: instance_xml.postscriptfontname,
stylemapfamilyname: instance_xml.stylemapfamilyname,
stylemapstylename: instance_xml.stylemapstylename,
location: instance_xml.location.dimension,
}
}
}
#[derive(Deserialize)]
struct LocationXmlRepr {
pub dimension: Vec<Dimension>,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct Dimension {
pub name: String,
pub uservalue: Option<f32>,
pub xvalue: Option<f32>,
pub yvalue: Option<f32>,
}
impl DesignSpaceDocument {
pub fn load<P: AsRef<Path>>(path: P) -> Result<DesignSpaceDocument, DesignSpaceLoadError> {
let reader = BufReader::new(File::open(path).map_err(DesignSpaceLoadError::Io)?);
quick_xml::de::from_reader(reader).map_err(DesignSpaceLoadError::DeError)
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use pretty_assertions::assert_eq;
use crate::designspace::{AxisMapping, Dimension};
use super::DesignSpaceDocument;
fn dim_name_xvalue(name: &str, xvalue: f32) -> Dimension {
Dimension { name: name.to_string(), uservalue: None, xvalue: Some(xvalue), yvalue: None }
}
#[test]
fn read_single_wght() {
let ds = DesignSpaceDocument::load(Path::new("testdata/single_wght.designspace")).unwrap();
assert_eq!(1, ds.axes.len());
assert_eq!(
&vec![AxisMapping { input: 400., output: 100. }],
ds.axes[0].map.as_ref().unwrap()
);
assert_eq!(1, ds.sources.len());
let weight_100 = dim_name_xvalue("Weight", 100.);
assert_eq!(vec![weight_100.clone()], ds.sources[0].location);
assert_eq!(1, ds.instances.len());
assert_eq!(vec![weight_100], ds.instances[0].location);
}
#[test]
fn read_wght_variable() {
let ds = DesignSpaceDocument::load("testdata/wght.designspace").unwrap();
assert_eq!(1, ds.axes.len());
assert!(ds.axes[0].map.is_none());
assert_eq!(
vec![
("TestFamily-Regular.ufo".to_string(), vec![dim_name_xvalue("Weight", 400.)]),
("TestFamily-Bold.ufo".to_string(), vec![dim_name_xvalue("Weight", 700.)]),
],
ds.sources
.into_iter()
.map(|s| (s.filename, s.location))
.collect::<Vec<(String, Vec<Dimension>)>>()
);
}
}