#![deny(rustdoc::broken_intra_doc_links)]
use std::fs;
use std::path::{Path, PathBuf};
use crate::data_request::LayerFilter;
use crate::datastore::{DataStore, ImageStore};
use crate::error::{FontLoadError, FontWriteError};
use crate::fontinfo::FontInfo;
use crate::glyph::Glyph;
use crate::groups::{validate_groups, Groups};
use crate::guideline::Guideline;
use crate::kerning::Kerning;
use crate::layer::{Layer, LayerSet, LAYER_CONTENTS_FILE};
use crate::name::Name;
use crate::names::NameList;
use crate::shared_types::{Plist, PUBLIC_OBJECT_LIBS_KEY};
use crate::upconversion;
use crate::write::{self, WriteOptions};
use crate::DataRequest;
static METAINFO_FILE: &str = "metainfo.plist";
static FONTINFO_FILE: &str = "fontinfo.plist";
pub(crate) static LIB_FILE: &str = "lib.plist";
static GROUPS_FILE: &str = "groups.plist";
static KERNING_FILE: &str = "kerning.plist";
static FEATURES_FILE: &str = "features.fea";
static DEFAULT_METAINFO_CREATOR: &str = "org.linebender.norad";
pub(crate) static DATA_DIR: &str = "data";
pub(crate) static IMAGES_DIR: &str = "images";
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct Font {
pub meta: MetaInfo,
pub font_info: FontInfo,
pub layers: LayerSet,
pub lib: Plist,
pub groups: Groups,
pub kerning: Kerning,
pub features: String,
pub data: DataStore,
pub images: ImageStore,
}
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
#[repr(u8)]
pub enum FormatVersion {
V1 = 1,
V2 = 2,
V3 = 3,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MetaInfo {
pub creator: Option<String>,
pub format_version: FormatVersion,
#[serde(default, skip_serializing_if = "is_zero")]
pub format_version_minor: u32,
}
fn is_zero(v: &u32) -> bool {
*v == 0
}
impl Default for MetaInfo {
fn default() -> Self {
MetaInfo {
creator: Some(DEFAULT_METAINFO_CREATOR.to_string()),
format_version: FormatVersion::V3,
format_version_minor: 0,
}
}
}
impl Font {
pub fn new() -> Self {
Font::default()
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Font, FontLoadError> {
Self::load_requested_data(path, DataRequest::all())
}
pub fn load_requested_data(
path: impl AsRef<Path>,
request: DataRequest,
) -> Result<Font, FontLoadError> {
Self::load_impl(path.as_ref(), request)
}
fn load_impl(path: &Path, request: DataRequest) -> Result<Font, FontLoadError> {
let metadata = path.metadata().map_err(FontLoadError::AccessUfoDir)?;
if !metadata.is_dir() {
return Err(FontLoadError::UfoNotADir);
}
let meta_path = path.join(METAINFO_FILE);
if !meta_path.exists() {
return Err(FontLoadError::MissingMetaInfoFile);
}
let mut meta: MetaInfo = plist::from_file(&meta_path)
.map_err(|source| FontLoadError::ParsePlist { name: METAINFO_FILE, source })?;
let lib_path = path.join(LIB_FILE);
let mut lib =
if request.lib && lib_path.exists() { load_lib(&lib_path)? } else { Plist::new() };
let fontinfo_path = path.join(FONTINFO_FILE);
let mut font_info = if fontinfo_path.exists() {
load_fontinfo(&fontinfo_path, &meta, &mut lib)?
} else {
Default::default()
};
let groups_path = path.join(GROUPS_FILE);
let groups = if request.groups && groups_path.exists() {
Some(load_groups(&groups_path)?)
} else {
None
};
let kerning_path = path.join(KERNING_FILE);
let kerning = if request.kerning && kerning_path.exists() {
Some(load_kerning(&kerning_path)?)
} else {
None
};
let features_path = path.join(FEATURES_FILE);
let mut features = if request.features && features_path.exists() {
load_features(&features_path)?
} else {
Default::default()
};
let glyph_names = NameList::default();
let layers = load_layer_set(path, &meta, &glyph_names, &request.layers)?;
let data = if request.data && path.join(DATA_DIR).exists() {
DataStore::new(path).map_err(FontLoadError::DataStore)?
} else {
Default::default()
};
let images = if request.images && path.join(IMAGES_DIR).exists() {
ImageStore::new(path).map_err(FontLoadError::ImagesStore)?
} else {
Default::default()
};
let (groups, kerning) = match (meta.format_version, groups, kerning) {
(FormatVersion::V3, g, k) => (g, k), (_, None, k) => (None, k), (_, Some(g), k) => {
let (groups, kerning) =
upconversion::upconvert_kerning(&g, &k.unwrap_or_default(), &glyph_names);
validate_groups(&groups).map_err(FontLoadError::GroupsUpconversionFailure)?;
(Some(groups), Some(kerning))
}
};
if meta.format_version == FormatVersion::V1 && lib_path.exists() {
if let Some(features_upgraded) =
upconversion::upconvert_ufov1_robofab_data(&lib_path, &mut lib, &mut font_info)?
{
if !features_upgraded.is_empty() {
features = features_upgraded;
}
}
}
meta.format_version = FormatVersion::V3;
Ok(Font {
layers,
meta,
font_info,
lib,
groups: groups.unwrap_or_default(),
kerning: kerning.unwrap_or_default(),
features,
data,
images,
})
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), FontWriteError> {
let path = path.as_ref();
self.save_impl(path, &Default::default())
}
pub fn save_with_options(
&self,
path: impl AsRef<Path>,
options: &WriteOptions,
) -> Result<(), FontWriteError> {
let path = path.as_ref();
self.save_impl(path, options)
}
fn save_impl(&self, path: &Path, options: &WriteOptions) -> Result<(), FontWriteError> {
if self.meta.format_version != FormatVersion::V3 {
return Err(FontWriteError::Downgrade);
}
if self.lib.contains_key(PUBLIC_OBJECT_LIBS_KEY) {
return Err(FontWriteError::PreexistingPublicObjectLibsKey);
}
validate_groups(&self.groups).map_err(FontWriteError::InvalidGroups)?;
self.font_info.validate().map_err(FontWriteError::InvalidFontInfo)?;
for (path, entry) in self.data.iter().chain(self.images.iter()) {
if let Err(source) = entry {
return Err(FontWriteError::InvalidStoreEntry { path: path.clone(), source });
};
}
if path.exists() {
fs::remove_dir_all(path).map_err(FontWriteError::Cleanup)?;
}
fs::create_dir(path).map_err(FontWriteError::CreateUfoDir)?;
let metainfo_path = path.join(METAINFO_FILE);
if self.meta.creator == Some(DEFAULT_METAINFO_CREATOR.into()) {
write::write_xml_to_file(&metainfo_path, &self.meta, options)
.map_err(|source| FontWriteError::CustomFile { name: METAINFO_FILE, source })?;
} else {
write::write_xml_to_file(&metainfo_path, &MetaInfo::default(), options)
.map_err(|source| FontWriteError::CustomFile { name: METAINFO_FILE, source })?;
}
if !self.font_info.is_empty() {
write::write_xml_to_file(&path.join(FONTINFO_FILE), &self.font_info, options)
.map_err(|source| FontWriteError::CustomFile { name: FONTINFO_FILE, source })?;
}
let mut lib = self.lib.clone();
let font_object_libs = self.font_info.dump_object_libs();
if !font_object_libs.is_empty() {
lib.insert(PUBLIC_OBJECT_LIBS_KEY.into(), font_object_libs.into());
}
if !lib.is_empty() {
crate::util::recursive_sort_plist_keys(&mut lib);
write::write_xml_to_file(&path.join(LIB_FILE), &lib, options)
.map_err(|source| FontWriteError::CustomFile { name: LIB_FILE, source })?;
}
if !self.groups.is_empty() {
write::write_xml_to_file(&path.join(GROUPS_FILE), &self.groups, options)
.map_err(|source| FontWriteError::CustomFile { name: GROUPS_FILE, source })?;
}
if !self.kerning.is_empty() {
let kerning_serializer = crate::kerning::KerningSerializer { kerning: &self.kerning };
write::write_xml_to_file(&path.join(KERNING_FILE), &kerning_serializer, options)
.map_err(|source| FontWriteError::CustomFile { name: KERNING_FILE, source })?;
}
if !self.features.is_empty() {
let feature_file_path = path.join(FEATURES_FILE);
if self.features.as_bytes().contains(&b'\r') {
fs::write(&feature_file_path, self.features.replace("\r\n", "\n"))
.map_err(FontWriteError::FeatureFile)?;
} else {
fs::write(&feature_file_path, &self.features)
.map_err(FontWriteError::FeatureFile)?;
}
}
let contents: Vec<(&str, &PathBuf)> =
self.layers.iter().map(|l| (l.name.as_ref(), &l.path)).collect();
write::write_xml_to_file(&path.join(LAYER_CONTENTS_FILE), &contents, options)
.map_err(|source| FontWriteError::CustomFile { name: LAYER_CONTENTS_FILE, source })?;
for layer in self.layers.iter() {
let layer_path = path.join(&layer.path);
layer.save_with_options(&layer_path, options).map_err(|source| {
FontWriteError::Layer {
name: layer.name.to_string(),
path: layer_path,
source: Box::new(source),
}
})?;
}
if !self.data.is_empty() {
let data_dir = path.join(DATA_DIR);
for (data_path, contents) in self.data.iter() {
let data = contents.expect("internal error: should have been checked");
let destination = data_dir.join(data_path);
let destination_parent = destination.parent().unwrap();
fs::create_dir_all(destination_parent).map_err(|source| {
FontWriteError::CreateStoreDir { path: destination_parent.into(), source }
})?;
fs::write(&destination, &*data)
.map_err(|source| FontWriteError::Data { path: destination, source })?;
}
}
if !self.images.is_empty() {
let images_dir = path.join(IMAGES_DIR);
fs::create_dir(&images_dir) .map_err(|source| FontWriteError::CreateStoreDir {
path: images_dir.clone(),
source,
})?;
for (image_path, contents) in self.images.iter() {
let data = contents.expect("internal error: should have been checked");
let destination = images_dir.join(image_path);
fs::write(&destination, &*data)
.map_err(|source| FontWriteError::Image { path: destination, source })?;
}
}
Ok(())
}
pub fn default_layer(&self) -> &Layer {
self.layers.default_layer()
}
pub fn default_layer_mut(&mut self) -> &mut Layer {
self.layers.default_layer_mut()
}
pub fn iter_layers(&self) -> impl Iterator<Item = &Layer> {
self.layers.iter()
}
pub fn iter_names(&self) -> impl Iterator<Item = Name> + '_ {
self.layers.default_layer().glyphs.keys().cloned()
}
pub fn get_glyph(&self, key: &str) -> Option<&Glyph> {
self.default_layer().get_glyph(key)
}
pub fn get_glyph_mut(&mut self, key: &str) -> Option<&mut Glyph> {
self.default_layer_mut().get_glyph_mut(key)
}
pub fn glyph_count(&self) -> usize {
self.default_layer().len()
}
pub fn guidelines(&self) -> &[Guideline] {
self.font_info.guidelines.as_deref().unwrap_or(&[])
}
pub fn guidelines_mut(&mut self) -> &mut Vec<Guideline> {
self.font_info.guidelines.get_or_insert_with(Default::default)
}
}
fn load_lib(lib_path: &Path) -> Result<plist::Dictionary, FontLoadError> {
plist::Value::from_file(lib_path)
.map_err(|source| FontLoadError::ParsePlist { name: LIB_FILE, source })?
.into_dictionary()
.ok_or(FontLoadError::LibFileMustBeDictionary)
}
fn load_fontinfo(
fontinfo_path: &Path,
meta: &MetaInfo,
lib: &mut plist::Dictionary,
) -> Result<FontInfo, FontLoadError> {
let font_info: FontInfo = FontInfo::from_file(fontinfo_path, meta.format_version, lib)
.map_err(FontLoadError::FontInfo)?;
Ok(font_info)
}
fn load_groups(groups_path: &Path) -> Result<Groups, FontLoadError> {
let groups: Groups = plist::from_file(groups_path)
.map_err(|source| FontLoadError::ParsePlist { name: GROUPS_FILE, source })?;
validate_groups(&groups).map_err(FontLoadError::InvalidGroups)?;
Ok(groups)
}
fn load_kerning(kerning_path: &Path) -> Result<Kerning, FontLoadError> {
let kerning: Kerning = plist::from_file(kerning_path)
.map_err(|source| FontLoadError::ParsePlist { name: KERNING_FILE, source })?;
Ok(kerning)
}
fn load_features(features_path: &Path) -> Result<String, FontLoadError> {
let features = fs::read_to_string(features_path).map_err(FontLoadError::FeatureFile)?;
Ok(features)
}
fn load_layer_set(
ufo_path: &Path,
meta: &MetaInfo,
glyph_names: &NameList,
filter: &LayerFilter,
) -> Result<LayerSet, FontLoadError> {
let layercontents_path = ufo_path.join(LAYER_CONTENTS_FILE);
if meta.format_version == FormatVersion::V3 && !layercontents_path.exists() {
return Err(FontLoadError::MissingLayerContentsFile);
}
LayerSet::load(ufo_path, glyph_names, filter)
}
#[cfg(test)]
mod tests {
use std::ops::Deref;
use tempdir::TempDir;
use crate::error::LayerLoadError;
use super::*;
#[test]
fn new_is_v3() {
let font = Font::new();
assert_eq!(font.meta.format_version, FormatVersion::V3);
}
#[test]
fn downgrade_unsupported() {
let dir = tempdir::TempDir::new("Test.ufo").unwrap();
let mut font = Font::new();
font.meta.format_version = FormatVersion::V1;
assert!(font.save(&dir).is_err());
font.meta.format_version = FormatVersion::V2;
assert!(font.save(&dir).is_err());
font.meta.format_version = FormatVersion::V3;
assert!(font.save(&dir).is_ok());
}
#[test]
fn loading() {
let path = "testdata/MutatorSansLightWide.ufo";
let font_obj = Font::load(path).unwrap();
assert_eq!(font_obj.iter_layers().count(), 2);
font_obj.layers.get("background").expect("missing layer");
assert_eq!(
font_obj.lib.get("com.typemytype.robofont.compileSettings.autohint"),
Some(&plist::Value::Boolean(true))
);
assert_eq!(font_obj.groups.get("public.kern1.@MMK_L_A"), Some(&vec![Name::new_raw("A")]));
#[allow(clippy::float_cmp)]
{
assert_eq!(font_obj.kerning.get("B").and_then(|k| k.get("H")), Some(&-40.0));
}
assert_eq!(font_obj.features, "# this is the feature from lightWide\n");
}
#[test]
fn load_save_feature_file_line_endings() {
let font_obj = Font::load("testdata/lineendings/Tester-LineEndings.ufo").unwrap();
let tmp = TempDir::new("test").unwrap();
let ufopath = tmp.path().join("test.ufo");
let feapath = ufopath.join("features.fea");
font_obj.save(ufopath).unwrap();
let test_fea = fs::read_to_string(feapath).unwrap();
let expected_fea = String::from("feature ss01 {\n featureNames {\n name \"Bogus feature\";\n name 1 \"Bogus feature\";\n };\n sub one by two;\n} ss01;\n");
assert_eq!(test_fea, expected_fea);
}
#[test]
fn loading_invalid_ufo_dir_path() {
let path = "totally/bogus/filepath/font.ufo";
let font_load_res = Font::load(path);
assert!(matches!(font_load_res, Err(FontLoadError::AccessUfoDir(_))));
}
#[test]
fn loading_missing_metainfo_plist_path() {
let path = "testdata/ufo/Tester-MissingMetaInfo.ufo";
let font_load_res = Font::load(path);
assert!(matches!(font_load_res, Err(FontLoadError::MissingMetaInfoFile)));
}
#[test]
fn loading_missing_layercontents_plist_path() {
let path = "testdata/ufo/Tester-MissingLayerContents.ufo";
let font_load_res = Font::load(path);
assert!(matches!(font_load_res, Err(FontLoadError::MissingLayerContentsFile)));
}
#[test]
fn loading_missing_glyphs_contents_plist_path() {
let path = "testdata/ufo/Tester-MissingGlyphsContents.ufo";
let font_load_res = Font::load(path);
let Err(FontLoadError::Layer { source, .. }) = font_load_res else {
panic!("expected FontLoadError, found '{:?}'", font_load_res);
};
if !matches!(source.deref(), LayerLoadError::MissingContentsFile) {
panic!("expected MissingContentsFile, found '{:?}'", source);
}
}
#[test]
fn loading_missing_glyphs_contents_plist_path_background_layer() {
let path = "testdata/ufo/Tester-MissingGlyphsContents-BackgroundLayer.ufo";
let font_load_res = Font::load(path);
let Err(FontLoadError::Layer { source, .. }) = font_load_res else {
panic!("expected FontLoadError, found '{:?}'", font_load_res);
};
if !matches!(source.deref(), LayerLoadError::MissingContentsFile) {
panic!("expected MissingContentsFile, found '{:?}'", source);
}
}
#[test]
fn data_request() {
let path = "testdata/MutatorSansLightWide.ufo";
let font_obj = Font::load_requested_data(path, DataRequest::none()).unwrap();
assert_eq!(font_obj.iter_layers().count(), 1);
assert!(font_obj.layers.default_layer().is_empty());
assert_eq!(font_obj.lib, Plist::new());
assert!(font_obj.groups.is_empty());
assert!(font_obj.kerning.is_empty());
assert!(font_obj.features.is_empty());
}
#[test]
fn upconvert_ufov1_robofab_data() {
let path = "testdata/fontinfotest_v1.ufo";
let font = Font::load(path).unwrap();
assert_eq!(font.meta.format_version, FormatVersion::V3);
let font_info = font.font_info;
assert_eq!(font_info.postscript_blue_fuzz, Some(1.));
assert_eq!(font_info.postscript_blue_scale, Some(0.039625));
assert_eq!(font_info.postscript_blue_shift, Some(7.));
assert_eq!(
font_info.postscript_blue_values,
Some(vec![-10., 0., 482., 492., 694., 704., 739., 749.])
);
assert_eq!(font_info.postscript_other_blues, Some(vec![-260., -250.]));
assert_eq!(font_info.postscript_family_blues, Some(vec![500.0, 510.0]));
assert_eq!(font_info.postscript_family_other_blues, Some(vec![-260., -250.]));
assert_eq!(font_info.postscript_force_bold, Some(true));
assert_eq!(font_info.postscript_stem_snap_h, Some(vec![100., 120.]));
assert_eq!(font_info.postscript_stem_snap_v, Some(vec![80., 90.]));
assert_eq!(font.lib.keys().collect::<Vec<&String>>(), vec!["org.robofab.testFontLibData"]);
assert_eq!(
font.features,
"@myClass = [A B];\n\nfeature liga {\n sub A A by b;\n} liga;\n"
);
}
#[test]
fn upconversion_fontinfo_v123() {
let ufo_v1 = Font::load("testdata/fontinfotest_v1.ufo").unwrap();
let ufo_v2 = Font::load("testdata/fontinfotest_v2.ufo").unwrap();
let ufo_v3 = Font::load("testdata/fontinfotest_v3.ufo").unwrap();
assert_eq!(ufo_v1, ufo_v3);
assert_eq!(ufo_v2, ufo_v3);
}
#[test]
fn metainfo() {
let path = "testdata/MutatorSansLightWide.ufo/metainfo.plist";
let meta: MetaInfo = plist::from_file(path).expect("failed to load metainfo");
assert_eq!(meta.creator, Some("org.robofab.ufoLib".into()));
}
#[test]
fn serialize_metainfo() {
use serde_test::{assert_ser_tokens, Token};
let meta1 = MetaInfo::default();
assert_ser_tokens(
&meta1,
&[
Token::Struct { name: "MetaInfo", len: 2 },
Token::Str("creator"),
Token::Some,
Token::Str(DEFAULT_METAINFO_CREATOR),
Token::Str("formatVersion"),
Token::U8(3),
Token::StructEnd,
],
);
let meta2 = MetaInfo { format_version_minor: 123, ..Default::default() };
assert_ser_tokens(
&meta2,
&[
Token::Struct { name: "MetaInfo", len: 3 },
Token::Str("creator"),
Token::Some,
Token::Str(DEFAULT_METAINFO_CREATOR),
Token::Str("formatVersion"),
Token::U8(3),
Token::Str("formatVersionMinor"),
Token::U32(123),
Token::StructEnd,
],
);
}
#[test]
fn save_with_options_with_writeoptions_parameter() {
let opt = WriteOptions::default();
let ufo = Font::default();
let tmp = TempDir::new("test").unwrap();
ufo.save_with_options(tmp, &opt).unwrap()
}
}