-
Notifications
You must be signed in to change notification settings - Fork 13
SourceResolver loading #390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -142,6 +142,17 @@ pub enum FontLoadError { | |
| /// The underlying error. | ||
| source: PlistError, | ||
| }, | ||
| /// Failed to load a file through a custom source resolver. | ||
| #[error("failed to read '{path}' from source resolver")] | ||
| ResolverIo { | ||
| /// The requested path. | ||
| path: PathBuf, | ||
| /// The underlying error. | ||
| source: IoError, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think |
||
| }, | ||
| /// Resolver-based loading currently supports only UFO v3 fonts. | ||
| #[error("resolver-based loading currently supports only UFO v3")] | ||
| ResolverUnsupportedFormatVersion, | ||
| /// Norad can currently only open UFO (directory) packages. | ||
| #[error("only UFO (directory) packages are supported")] | ||
| UfoNotADir, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,9 @@ | |
|
|
||
| #![deny(rustdoc::broken_intra_doc_links)] | ||
|
|
||
| use std::collections::BTreeMap; | ||
| use std::fs; | ||
| use std::io::{Cursor, ErrorKind}; | ||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| use serde::{Deserialize, Serialize}; | ||
|
|
@@ -34,6 +36,73 @@ static DEFAULT_METAINFO_CREATOR: &str = "org.linebender.norad"; | |
| pub(crate) static DATA_DIR: &str = "data"; | ||
| pub(crate) static IMAGES_DIR: &str = "images"; | ||
|
|
||
| /// Abstraction over loading UTF-8 UFO files. | ||
| /// | ||
| /// This can be implemented by filesystem-backed loaders, in-memory maps, | ||
| /// or custom loaders. | ||
| pub trait SourceResolver { | ||
| /// Return UTF-8 contents for the provided path. | ||
| /// | ||
| /// Returning `Ok(None)` indicates a missing file. | ||
| fn get_contents(&self, path: &Path) -> Result<Option<String>, FontLoadError>; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What we want here is some very customizable error type that the implementor can use to communicate why the given resource isn't available; The 'best' option is probably an associated type, so this would look like: trait SourceResolver {
type Error;
fn get_contents(&self, path: &Path) -> Result<Option<String>, Self::Error>;
}but this might be a bit fiddly if you're not that comfortable with rust? Because we would probably need bounds on the error type elsewhere, so that the top-level |
||
|
|
||
| /// Resolve a raw request path. | ||
| fn resolve_raw_path(&self, path: &Path, _requested_from: Option<&Path>) -> PathBuf { | ||
| path.to_path_buf() | ||
| } | ||
|
|
||
| /// Canonicalize a path if needed. | ||
| fn canonicalize(&self, path: &Path) -> Result<PathBuf, FontLoadError> { | ||
| Ok(path.to_path_buf()) | ||
| } | ||
|
Comment on lines
+49
to
+57
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. both of these methods are important for resolving include statements in a FEA file but are (I believe) irrelevant for your use-case, and can be removed. |
||
| } | ||
|
|
||
| impl<F> SourceResolver for F | ||
| where | ||
| F: Fn(&Path) -> Option<String>, | ||
| { | ||
| fn get_contents(&self, path: &Path) -> Result<Option<String>, FontLoadError> { | ||
| Ok((self)(path)) | ||
| } | ||
| } | ||
|
|
||
| /// Filesystem-backed [`SourceResolver`]. | ||
| #[derive(Default)] | ||
| pub struct FileSystemResolver { | ||
| project_root: PathBuf, | ||
| } | ||
|
|
||
| impl FileSystemResolver { | ||
| /// Create a resolver rooted at `project_root`. | ||
| pub fn new(project_root: PathBuf) -> Self { | ||
| Self { project_root } | ||
| } | ||
| } | ||
|
|
||
| impl SourceResolver for FileSystemResolver { | ||
| fn get_contents(&self, path: &Path) -> Result<Option<String>, FontLoadError> { | ||
| match fs::read_to_string(path) { | ||
| Ok(contents) => Ok(Some(contents)), | ||
| Err(source) if source.kind() == ErrorKind::NotFound => Ok(None), | ||
| Err(source) => Err(FontLoadError::ResolverIo { path: path.to_path_buf(), source }), | ||
| } | ||
| } | ||
|
|
||
| fn resolve_raw_path(&self, path: &Path, requested_from: Option<&Path>) -> PathBuf { | ||
| if path.is_absolute() { | ||
| return path.to_path_buf(); | ||
| } | ||
| if let Some(parent) = requested_from.and_then(Path::parent) { | ||
| return parent.join(path); | ||
| } | ||
| self.project_root.join(path) | ||
| } | ||
|
|
||
| fn canonicalize(&self, path: &Path) -> Result<PathBuf, FontLoadError> { | ||
| Ok(path.to_path_buf()) | ||
| } | ||
| } | ||
|
|
||
|
Comment on lines
+69
to
+105
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you even need a file-system backed resolver? this is used in fea-rs because the resolver API is the only API for handling |
||
| /// A font object, corresponding to a [UFO directory]. | ||
| /// A Unified Font Object. | ||
| /// | ||
|
|
@@ -212,6 +281,115 @@ impl Font { | |
| Self::load_impl(path.as_ref(), request) | ||
| } | ||
|
|
||
| /// Returns a [`Font`] loaded via a custom [`SourceResolver`]. | ||
| /// | ||
| /// Paths are resolved relative to the resolver root and must be UFO-root | ||
| /// relative (for example `metainfo.plist`, `glyphs/contents.plist`, etc.). | ||
| /// | ||
| /// Note: resolver-based loading currently supports only UFO v3 and does not | ||
| /// include data/images stores. | ||
| pub fn load_with_resolver( | ||
| request: DataRequest, | ||
| resolver: impl SourceResolver, | ||
| ) -> Result<Font, FontLoadError> { | ||
| Self::load_with_resolver_impl(request, &resolver) | ||
| } | ||
|
|
||
| fn load_with_resolver_impl( | ||
| request: DataRequest, | ||
| resolver: &dyn SourceResolver, | ||
| ) -> Result<Font, FontLoadError> { | ||
|
Comment on lines
+298
to
+301
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. have a separate |
||
| let metainfo_str = required_file(resolver, Path::new(METAINFO_FILE), None)?; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this |
||
| let mut meta: MetaInfo = plist::from_reader(Cursor::new(metainfo_str.as_bytes())) | ||
| .map_err(|source| FontLoadError::ParsePlist { name: METAINFO_FILE, source })?; | ||
|
|
||
| if meta.format_version != FormatVersion::V3 { | ||
| return Err(FontLoadError::ResolverUnsupportedFormatVersion); | ||
| } | ||
|
|
||
| let mut lib = if request.lib { | ||
| match optional_file(resolver, Path::new(LIB_FILE), None)? { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
| Some(lib_str) => plist::Value::from_reader(Cursor::new(lib_str.as_bytes())) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you shouldn't need |
||
| .map_err(|source| FontLoadError::ParsePlist { name: LIB_FILE, source })? | ||
| .into_dictionary() | ||
| .ok_or(FontLoadError::LibFileMustBeDictionary)?, | ||
| None => Plist::new(), | ||
| } | ||
| } else { | ||
| Plist::new() | ||
| }; | ||
|
|
||
| let font_info = if let Some(fontinfo_str) = | ||
| optional_file(resolver, Path::new(FONTINFO_FILE), None)? | ||
| { | ||
| let mut font_info: FontInfo = plist::from_reader(Cursor::new(fontinfo_str.as_bytes())) | ||
| .map_err(|source| FontLoadError::ParsePlist { name: FONTINFO_FILE, source })?; | ||
| font_info | ||
| .validate() | ||
| .map_err(crate::error::FontInfoLoadError::InvalidData) | ||
| .map_err(FontLoadError::FontInfo)?; | ||
| font_info.load_object_libs(&mut lib).map_err(FontLoadError::FontInfo)?; | ||
| font_info | ||
| } else { | ||
| Default::default() | ||
| }; | ||
|
|
||
| let groups = if request.groups { | ||
| match optional_file(resolver, Path::new(GROUPS_FILE), None)? { | ||
| Some(groups_str) => { | ||
| let groups: Groups = | ||
| plist::from_reader(Cursor::new(groups_str.as_bytes())).map_err( | ||
| |source| FontLoadError::ParsePlist { name: GROUPS_FILE, source }, | ||
| )?; | ||
| validate_groups(&groups).map_err(FontLoadError::InvalidGroups)?; | ||
| Some(groups) | ||
| } | ||
| None => None, | ||
| } | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let kerning = if request.kerning { | ||
| match optional_file(resolver, Path::new(KERNING_FILE), None)? { | ||
| Some(kerning_str) => { | ||
| let kerning: Kerning = | ||
| plist::from_reader(Cursor::new(kerning_str.as_bytes())).map_err( | ||
| |source| FontLoadError::ParsePlist { name: KERNING_FILE, source }, | ||
| )?; | ||
| Some(kerning) | ||
| } | ||
| None => None, | ||
| } | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let features = if request.features { | ||
| optional_file(resolver, Path::new(FEATURES_FILE), None)?.unwrap_or_default() | ||
| } else { | ||
| Default::default() | ||
| }; | ||
|
|
||
| let layers = load_layer_set_from_resolver(resolver, &request.layers)?; | ||
|
|
||
| let (groups, kerning) = (groups, kerning); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? |
||
|
|
||
| meta.format_version = FormatVersion::V3; | ||
|
|
||
| Ok(Font { | ||
| layers, | ||
| meta, | ||
| font_info, | ||
| lib, | ||
| groups: groups.unwrap_or_default(), | ||
| kerning: kerning.unwrap_or_default(), | ||
| features, | ||
| data: Default::default(), | ||
| images: Default::default(), | ||
| }) | ||
| } | ||
|
|
||
| fn load_impl(path: &Path, request: DataRequest) -> Result<Font, FontLoadError> { | ||
| let metadata = path.metadata().map_err(FontLoadError::AccessUfoDir)?; | ||
| if !metadata.is_dir() { | ||
|
|
@@ -606,6 +784,105 @@ impl Font { | |
| } | ||
| } | ||
|
|
||
| fn optional_file( | ||
| resolver: &dyn SourceResolver, | ||
| path: &Path, | ||
| requested_from: Option<&Path>, | ||
| ) -> Result<Option<String>, FontLoadError> { | ||
| let resolved = resolver.resolve_raw_path(path, requested_from); | ||
| let canonical = resolver.canonicalize(&resolved)?; | ||
| resolver.get_contents(&canonical) | ||
| } | ||
|
|
||
| fn required_file( | ||
| resolver: &dyn SourceResolver, | ||
| path: &Path, | ||
| requested_from: Option<&Path>, | ||
| ) -> Result<String, FontLoadError> { | ||
| optional_file(resolver, path, requested_from)?.ok_or(FontLoadError::MissingMetaInfoFile) | ||
| } | ||
|
|
||
| fn load_layer_set_from_resolver( | ||
| resolver: &dyn SourceResolver, | ||
| filter: &LayerFilter, | ||
| ) -> Result<LayerContents, FontLoadError> { | ||
| let layer_descriptors: Vec<(Name, PathBuf)> = | ||
| match optional_file(resolver, Path::new(LAYER_CONTENTS_FILE), None)? { | ||
| Some(layercontents_str) => { | ||
| plist::from_reader(Cursor::new(layercontents_str.as_bytes())).map_err(|source| { | ||
| FontLoadError::ParsePlist { name: LAYER_CONTENTS_FILE, source } | ||
| })? | ||
| } | ||
| None => vec![(Name::new_raw("public.default"), PathBuf::from("glyphs"))], | ||
| }; | ||
|
|
||
| let mut layers = LayerContents::default(); | ||
| for (layer_name, layer_dir) in layer_descriptors { | ||
| if !filter.should_load(&layer_name, &layer_dir) { | ||
| continue; | ||
| } | ||
|
|
||
| let layer = if layer_dir == Path::new("glyphs") { | ||
| layers.default_layer_mut() | ||
| } else { | ||
| let layer = layers.get_or_create_layer(layer_name.as_str()).map_err(|_| { | ||
| FontLoadError::Layer { | ||
| name: layer_name.to_string(), | ||
| path: layer_dir.clone(), | ||
| source: Box::new(crate::error::LayerLoadError::MissingContentsFile), | ||
| } | ||
| })?; | ||
| layer.path = layer_dir.clone(); | ||
| layer | ||
| }; | ||
|
|
||
| let contents_path = layer_dir.join("contents.plist"); | ||
| let contents_str = | ||
| optional_file(resolver, &contents_path, None)?.ok_or(FontLoadError::Layer { | ||
| name: layer_name.to_string(), | ||
| path: contents_path.clone(), | ||
| source: Box::new(crate::error::LayerLoadError::MissingContentsFile), | ||
| })?; | ||
|
|
||
| let glyph_files: BTreeMap<Name, PathBuf> = | ||
| plist::from_reader(Cursor::new(contents_str.as_bytes())).map_err(|source| { | ||
| FontLoadError::Layer { | ||
| name: layer_name.to_string(), | ||
| path: contents_path.clone(), | ||
| source: Box::new(crate::error::LayerLoadError::ParsePlist { | ||
| name: "contents.plist", | ||
| source, | ||
| }), | ||
| } | ||
| })?; | ||
|
|
||
| for (_glyph_name, glif_relative_path) in glyph_files { | ||
| let glif_path = layer_dir.join(&glif_relative_path); | ||
| let glif_contents = | ||
| optional_file(resolver, &glif_path, None)?.ok_or(FontLoadError::Layer { | ||
| name: layer_name.to_string(), | ||
| path: glif_path.clone(), | ||
| source: Box::new(crate::error::LayerLoadError::MissingContentsFile), | ||
| })?; | ||
| let mut glyph = Glyph::parse_raw(glif_contents.as_bytes()).map_err(|source| { | ||
| FontLoadError::Layer { | ||
| name: layer_name.to_string(), | ||
| path: glif_path.clone(), | ||
| source: Box::new(crate::error::LayerLoadError::Glyph { | ||
| name: glif_relative_path.to_string_lossy().to_string(), | ||
| path: glif_path.clone(), | ||
| source, | ||
| }), | ||
| } | ||
| })?; | ||
| glyph.name = Name::new_raw(&glyph.name); | ||
| layer.insert_glyph(glyph); | ||
| } | ||
| } | ||
|
|
||
| Ok(layers) | ||
| } | ||
|
|
||
| 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 })? | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
total nit but I would prefer this to be right below the main
loadfn, above.