Add in-memory UFO loader and path helpers#387
Add in-memory UFO loader and path helpers#387yanone wants to merge 3 commits intolinebender:masterfrom
Conversation
Introduce Font::load_entries to load a Font from in-memory UFO entries (HashMap of relative paths -> contents). The implementation normalizes virtual paths, detects an effective UFO root, parses metainfo/fontinfo/lib/groups/kerning/features, reads layer descriptors and glyph GLIFs, and normalizes negative openTypeOS2WinDescent values. Added utility functions normalize_virtual_path and join_virtual_path, plus necessary imports (Cursor, BTreeMap, HashMap). Also added DesignSpaceDocument::load_str and unit tests covering basic entry loading and negative OS/2 winDescent handling.
src/font.rs
Outdated
| FontLoadError::ParsePlist { name: FONTINFO_FILE, source } | ||
| })?; | ||
|
|
||
| if let Some(dict) = fontinfo_value.as_dictionary_mut() { |
There was a problem hiding this comment.
This is curious, why is this manipulating data here?
There was a problem hiding this comment.
That was a compatibility layer that accidentally remained in the code where I had to deal with a font with a negative winDescent value which broke the parsing. I just patched it.
There was a problem hiding this comment.
I also updated the formatting. All checks pass now.
|
Broader question than this PR: did we ought to have some file system abstraction trait in |
|
Are there existing filesystem abstractions in wider use? |
|
The closest thing would be accepting |
cmyr
left a comment
There was a problem hiding this comment.
Sorry to let this sit! Overall I think that supporting this use-case makes sense, but I have some general questions about what the API should look like. I've left various questions inline, and once I make sure I'm understanding various decisions we can figure out what the best API would be.
| pub fn load_entries( | ||
| path: impl AsRef<Path>, | ||
| entries: &HashMap<String, String>, | ||
| ) -> Result<Font, FontLoadError> { |
There was a problem hiding this comment.
I want to think a bit about the API here and make sure that we get it right.
-
Is there a reason to use
Stringinstead ofPathBufas a way of identifying the files? to mePathBufwould make more sense, even if we aren't referring to files on disk, because it handles all the logic for handling components, separators, etc. -
Do we need a root path, or could it be implied? Is it mostly useful for error messages?
-
Instead of needing to pass in a
HashMap, another option would be for this constructor to take a closure that would then return various resources, e.g. it could look like,fn load_entries(entires: impl Fn(&Path) -> Option<String>) -> Result<Font, Error>
this isn't a huge difference, but it just makes the API more flexible at the call site, instead of requiring the caller to always put everything in a hashmap.
Curious to hear your thoughts, and we can iterate a bit from there?
| let rooted_metainfo_key = join_virtual_path(&root, METAINFO_FILE); | ||
| let effective_root = | ||
| if normalized_entries.contains_key(&rooted_metainfo_key) { root.as_str() } else { "" }; | ||
|
|
||
| let metainfo_key = join_virtual_path(effective_root, METAINFO_FILE); | ||
| let metainfo_str = | ||
| normalized_entries.get(&metainfo_key).ok_or(FontLoadError::MissingMetaInfoFile)?; |
There was a problem hiding this comment.
so is the idea here that the caller can provide either MyFont.ufo/metainfo.plist or just metainfo.plist? I would prefer to enforce stricter requirements on the caller, and just insist that all paths are relative to the (implied) root of the ufo directory?
| ufo.meta = plist::from_reader(Cursor::new(metainfo_str.as_bytes())) | ||
| .map_err(|source| FontLoadError::ParsePlist { name: METAINFO_FILE, source })?; | ||
|
|
||
| let fontinfo_key = join_virtual_path(effective_root, FONTINFO_FILE); |
There was a problem hiding this comment.
yea it would be nice if we were just always using FONTINFO_FILE as the key and not worrying about a possible root..
|
I'll propose another solution in a couple of days, maybe tomorrow, that's independent of the hashmaps, along the lines of what @RickyDaMa proposed. A general file system abstraction trait where I implement the hashmaps on my side while norad uses a file system wrapper that could wrap anything other than the native file system. It would also be the smallest-possible code change to norad. I'll be back. |
|
So if you're going this route I will point you to how we handled this case in |
|
Here are two proposals. I didn't create PRs yet, but you can compare the branches. Both are a bit verbose because norad reads files in many different places. The first follows your suggestion and implements SourceResolver. Pro: explicit abstraction, cleaner long-term architecture, easiest to extend (different backends). The second is a minimal callback-reader approach. Less verbose changes, but SourceResolver appears to be the better architecture. Pro: smaller and more merge-friendly; keeps current APIs mostly intact with one extra loading hook. |
|
Honestly they both seem fine, so I'm happy with whichever you prefer. It might be that the trait-based abstraction has additional features that are not necessary for this case, which might suggest that the callback makes sense? But no strong feelings, feel free to open a PR with whichever you prefer :) |
Introduce Font::load_entries to load a Font from in-memory UFO entries (HashMap of relative paths -> contents). The implementation normalizes virtual paths, detects an effective UFO root, parses metainfo/fontinfo/lib/groups/kerning/features, reads layer descriptors and glyph GLIFs, and normalizes negative openTypeOS2WinDescent values. Added utility functions normalize_virtual_path and join_virtual_path, plus necessary imports (Cursor, BTreeMap, HashMap). Also added DesignSpaceDocument::load_str and unit tests covering basic entry loading and negative OS/2 winDescent handling.
The purpose for this addition is that it allows tools with no file system access (such as wasm components) to parse .ufo/.designspace fonts.
I had opened an issue for that and thought I'd try to add some momentum with a PR. Cheers.