use serde::{Deserialize, Serialize};
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use anyhow::Result;
use toml::Value;
use typst::text::{FontStretch, FontStyle, FontWeight};
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
pub struct TypstFont {
pub(crate) family_name: String,
#[serde(default, with = "typst_font_serde")]
pub(crate) style: FontStyle,
#[serde(default)]
pub(crate) weight: FontWeight,
#[serde(default)]
pub(crate) stretch: FontStretch,
}
impl fmt::Display for TypstFont {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let stretch = (self.stretch.to_ratio().get() * 1000.0) as u16;
write!(
f,
"{:<30} (style: {:?}, weight: {:?}, stretch: {})",
self.family_name, self.style, self.weight, stretch
)
}
}
mod typst_font_serde {
use serde::{Deserialize, Deserializer, Serializer};
use typst::text::FontStyle;
pub fn serialize<S>(style: &FontStyle, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let style_str = match style {
FontStyle::Normal => "Normal",
FontStyle::Italic => "Italic",
FontStyle::Oblique => "Oblique",
};
serializer.serialize_str(style_str)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<FontStyle, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"normal" => Ok(FontStyle::Normal),
"italic" => Ok(FontStyle::Italic),
"oblique" => Ok(FontStyle::Oblique),
_ => Err(serde::de::Error::custom(format!(
"Invalid FontStyle: {}",
s
))),
}
}
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct FontConfig {
#[serde(default)]
pub(crate) font_dir: Option<String>, pub(crate) fonts: Vec<TypstFont>, }
pub fn deserialize_fonts_from_toml(toml_content: &str) -> Result<FontConfig> {
let font_config: FontConfig = toml::from_str(preprocess_font_config(toml_content)?.as_str())?;
Ok(font_config)
}
pub fn deserialize_fonts_from_file<P: AsRef<Path>>(file_path: P) -> Result<FontConfig> {
let mut file = File::open(file_path).expect("Font config file not found");
let mut content = String::new();
file.read_to_string(&mut content)?;
deserialize_fonts_from_toml(&content)
}
#[allow(dead_code)]
pub fn serialize_fonts_to_toml(font_config: FontConfig) -> Result<String> {
let toml_string = toml::to_string(&font_config)?;
Ok(toml_string)
}
fn preprocess_font_config(toml_str: &str) -> Result<String> {
let mut toml_value: Value = toml_str.parse::<Value>()?;
if let Some(fonts) = toml_value.get("fonts") {
if let Some(fonts_array) = fonts.as_array() {
let mut expanded_fonts = Vec::new();
for font in fonts_array {
if let Some(weight) = font.get("weight") {
if let Some(weights) = weight.as_array() {
for w in weights {
let mut new_font = font.clone();
if let Some(map) = new_font.as_table_mut() {
map.insert("weight".to_string(), w.clone());
}
expanded_fonts.push(Value::Table(new_font.as_table().unwrap().clone()));
}
} else {
expanded_fonts.push(font.clone());
}
} else {
expanded_fonts.push(font.clone());
}
}
if let Some(table) = toml_value.as_table_mut() {
table.insert("fonts".to_string(), Value::Array(expanded_fonts));
}
}
}
let new_toml_string = toml::to_string(&toml_value)?;
Ok(new_toml_string)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_serialize_fonts_to_toml() {
let fonts_config = FontConfig {
font_dir: Some("fonts".into()),
fonts: vec![
TypstFont {
family_name: "Arial".to_string(),
style: FontStyle::Normal,
weight: FontWeight::from_number(400),
stretch: FontStretch::NORMAL,
},
TypstFont {
family_name: "Times New Roman".to_string(),
style: FontStyle::Italic,
weight: FontWeight::from_number(700),
stretch: FontStretch::ULTRA_EXPANDED,
},
],
};
let toml_string = serialize_fonts_to_toml(fonts_config).unwrap();
let expected_toml = r#"font_dir = "fonts"
[[fonts]]
family_name = "Arial"
style = "Normal"
weight = 400
stretch = 1000
[[fonts]]
family_name = "Times New Roman"
style = "Italic"
weight = 700
stretch = 2000
"#;
assert_eq!(toml_string, expected_toml);
}
#[test]
fn test_deserialize_fonts_from_toml() {
let toml_string = r#"[[fonts]]
family_name = "Noto Sans"
[[fonts]]
family_name = "Stix Two Text"
style = "Italic"
weight = 700
stretch = 1250
[[fonts]]
family_name = "Lato"
style = "Italic"
weight = [500, 700]
"#;
let font_config = deserialize_fonts_from_toml(toml_string).unwrap();
let expected_fonts = vec![
TypstFont {
family_name: "Noto Sans".to_string(),
style: FontStyle::Normal,
weight: FontWeight::from_number(400),
stretch: FontStretch::NORMAL,
},
TypstFont {
family_name: "Stix Two Text".to_string(),
style: FontStyle::Italic,
weight: FontWeight::from_number(700),
stretch: FontStretch::EXPANDED,
},
TypstFont {
family_name: "Lato".to_string(),
style: FontStyle::Italic,
weight: FontWeight::from_number(500),
stretch: FontStretch::NORMAL,
},
TypstFont {
family_name: "Lato".to_string(),
style: FontStyle::Italic,
weight: FontWeight::from_number(700),
stretch: FontStretch::NORMAL,
},
];
assert_eq!(font_config.fonts, expected_fonts);
assert_eq!(font_config.font_dir, None);
}
#[test]
fn test_deserialize_fonts_from_file() {
let config_file =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/font_configs/font_config.toml");
assert!(config_file.exists());
let font_config = deserialize_fonts_from_file(&config_file).unwrap();
let expected_fonts = vec![
TypstFont {
family_name: "Arial".to_string(),
style: FontStyle::Normal,
weight: FontWeight::from_number(400),
stretch: FontStretch::NORMAL,
},
TypstFont {
family_name: "Times New Roman".to_string(),
style: FontStyle::Italic,
weight: FontWeight::from_number(700),
stretch: FontStretch::ULTRA_EXPANDED,
},
];
assert_eq!(font_config.fonts, expected_fonts);
assert_eq!(font_config.font_dir.unwrap(), "fonts".to_string());
}
}