From 011865ab5c8943abcb64c7b545e265d1a65db32a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 18 Aug 2021 14:23:48 +0200 Subject: [PATCH] Memory loader --- src/font.rs | 41 +++++++++++++++++- src/loading/fs.rs | 77 +++++++-------------------------- src/loading/mem.rs | 104 +++++++++++++++++++++++++++++++++++++++++++++ src/loading/mod.rs | 2 + 4 files changed, 162 insertions(+), 62 deletions(-) create mode 100644 src/loading/mem.rs diff --git a/src/font.rs b/src/font.rs index 300774bbf..978fb601a 100644 --- a/src/font.rs +++ b/src/font.rs @@ -3,11 +3,12 @@ use std::collections::{hash_map::Entry, HashMap}; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::Add; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::rc::Rc; use decorum::N64; use serde::{Deserialize, Serialize}; +use ttf_parser::name_id; use crate::geom::Length; use crate::loading::{FileHash, Loader}; @@ -376,6 +377,44 @@ pub struct FaceInfo { pub variant: FontVariant, } +impl FaceInfo { + /// Determine metadata about all faces that are found in the given data. + pub fn parse<'a>( + path: &'a Path, + data: &'a [u8], + ) -> impl Iterator + 'a { + let count = ttf_parser::fonts_in_collection(data).unwrap_or(1); + (0 .. count).filter_map(move |index| { + fn find_name(face: &ttf_parser::Face, name_id: u16) -> Option { + face.names().find_map(|entry| { + (entry.name_id() == name_id).then(|| entry.to_string()).flatten() + }) + } + + let face = ttf_parser::Face::from_slice(data, index).ok()?; + let family = find_name(&face, name_id::TYPOGRAPHIC_FAMILY) + .or_else(|| find_name(&face, name_id::FAMILY))?; + + let variant = FontVariant { + style: match (face.is_italic(), face.is_oblique()) { + (false, false) => FontStyle::Normal, + (true, _) => FontStyle::Italic, + (_, true) => FontStyle::Oblique, + }, + weight: FontWeight::from_number(face.weight().to_number()), + stretch: FontStretch::from_number(face.width().to_number()), + }; + + Some(FaceInfo { + path: path.to_owned(), + index, + family, + variant, + }) + }) + } +} + /// Properties that distinguish a face from other faces in the same family. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Serialize, Deserialize)] diff --git a/src/loading/fs.rs b/src/loading/fs.rs index 8d905ed26..46e3dba17 100644 --- a/src/loading/fs.rs +++ b/src/loading/fs.rs @@ -5,13 +5,12 @@ use std::rc::Rc; use memmap2::Mmap; use same_file::Handle; -use ttf_parser::{name_id, Face}; use walkdir::WalkDir; use super::{FileHash, Loader}; -use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight}; +use crate::font::FaceInfo; -/// Loads fonts and images from the local file system. +/// Loads fonts and files from the local file system. /// /// _This is only available when the `fs` feature is enabled._ #[derive(Debug, Default, Clone)] @@ -25,13 +24,13 @@ impl FsLoader { Self { faces: vec![] } } - /// Builder-style variant of `search_system`. + /// Builder-style variant of [`search_system`](Self::search_system). pub fn with_system(mut self) -> Self { self.search_system(); self } - /// Builder-style variant of `search_path`. + /// Builder-style variant of [`search_path`](Self::search_path). pub fn with_path(mut self, dir: impl AsRef) -> Self { self.search_path(dir); self @@ -89,8 +88,8 @@ impl FsLoader { /// /// If the path is a directory, all contained fonts will be searched for /// recursively. - pub fn search_path(&mut self, dir: impl AsRef) { - let walk = WalkDir::new(dir) + pub fn search_path(&mut self, path: impl AsRef) { + let walk = WalkDir::new(path) .follow_links(true) .sort_by(|a, b| a.file_name().cmp(b.file_name())) .into_iter() @@ -99,13 +98,11 @@ impl FsLoader { for entry in walk { let path = entry.path(); if let Some(ext) = path.extension().and_then(|s| s.to_str()) { - match ext { - #[rustfmt::skip] - "ttf" | "otf" | "TTF" | "OTF" | - "ttc" | "otc" | "TTC" | "OTC" => { - self.search_file(path).ok(); - } - _ => {} + if matches!( + ext, + "ttf" | "otf" | "TTF" | "OTF" | "ttc" | "otc" | "TTC" | "OTC", + ) { + self.search_file(path); } } } @@ -115,56 +112,14 @@ impl FsLoader { /// /// The file may form a font collection and contain multiple font faces, /// which will then all be indexed. - fn search_file(&mut self, path: impl AsRef) -> io::Result<()> { + fn search_file(&mut self, path: impl AsRef) { let path = path.as_ref(); let path = path.strip_prefix(".").unwrap_or(path); - - let file = File::open(path)?; - let mmap = unsafe { Mmap::map(&file)? }; - - for i in 0 .. ttf_parser::fonts_in_collection(&mmap).unwrap_or(1) { - let face = Face::from_slice(&mmap, i) - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; - - self.parse_face(path, &face, i)?; + if let Ok(file) = File::open(path) { + if let Ok(mmap) = unsafe { Mmap::map(&file) } { + self.faces.extend(FaceInfo::parse(&path, &mmap)); + } } - - Ok(()) - } - - /// Parse a single face and insert it into the `families`. This either - /// merges with an existing family entry if they have the same trimmed - /// family name, or creates a new one. - fn parse_face(&mut self, path: &Path, face: &Face<'_>, index: u32) -> io::Result<()> { - fn find_name(face: &Face, name_id: u16) -> Option { - face.names().find_map(|entry| { - (entry.name_id() == name_id).then(|| entry.to_string()).flatten() - }) - } - - let family = find_name(face, name_id::TYPOGRAPHIC_FAMILY) - .or_else(|| find_name(face, name_id::FAMILY)) - .ok_or("unknown font family") - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; - - let variant = FontVariant { - style: match (face.is_italic(), face.is_oblique()) { - (false, false) => FontStyle::Normal, - (true, _) => FontStyle::Italic, - (_, true) => FontStyle::Oblique, - }, - weight: FontWeight::from_number(face.weight().to_number()), - stretch: FontStretch::from_number(face.width().to_number()), - }; - - self.faces.push(FaceInfo { - path: path.to_owned(), - index, - family, - variant, - }); - - Ok(()) } } diff --git a/src/loading/mem.rs b/src/loading/mem.rs new file mode 100644 index 000000000..d7fb178f1 --- /dev/null +++ b/src/loading/mem.rs @@ -0,0 +1,104 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::io; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +use super::{FileHash, Loader}; +use crate::font::FaceInfo; +use crate::util::PathExt; + +/// Loads fonts and files from an in-memory storage. +#[derive(Debug, Default, Clone)] +pub struct MemLoader { + faces: Vec, + files: HashMap>, +} + +impl MemLoader { + /// Create a new from-memory loader. + pub fn new() -> Self { + Self { faces: vec![], files: HashMap::new() } + } + + /// Builder-style variant of [`insert`](Self::insert). + pub fn with(mut self, path: P, data: D) -> Self + where + P: AsRef, + D: Into>, + { + self.insert(path, data); + self + } + + /// Builder-style method to wrap the loader in an [`Rc`] to make it usable + /// with the [`Context`](crate::Context). + pub fn wrap(self) -> Rc { + Rc::new(self) + } + + /// Insert a path-file mapping. If the data forms a font, then that font + /// will be available for layouting. + /// + /// The data can either be owned or referenced, but the latter only if its + /// lifetime is `'static`. + pub fn insert(&mut self, path: P, data: D) + where + P: AsRef, + D: Into>, + { + let path = path.as_ref().normalize(); + let data = data.into(); + self.faces.extend(FaceInfo::parse(&path, &data)); + self.files.insert(path, data); + } +} + +impl Loader for MemLoader { + fn faces(&self) -> &[FaceInfo] { + &self.faces + } + + fn resolve(&self, path: &Path) -> io::Result { + let norm = path.normalize(); + if self.files.contains_key(&norm) { + Ok(FileHash(fxhash::hash64(&norm))) + } else { + Err(io::ErrorKind::NotFound.into()) + } + } + + fn load(&self, path: &Path) -> io::Result> { + self.files + .get(&path.normalize()) + .map(|cow| cow.clone().into_owned()) + .ok_or_else(|| io::ErrorKind::NotFound.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::font::FontVariant; + + #[test] + fn test_recognize_and_load_font() { + let data = include_bytes!("../../fonts/PTSans-Regular.ttf"); + let path = Path::new("PTSans.ttf"); + let loader = MemLoader::new().with(path, &data[..]); + + // Test that the found was found. + let info = &loader.faces[0]; + assert_eq!(info.path, path); + assert_eq!(info.index, 0); + assert_eq!(info.family, "PT Sans"); + assert_eq!(info.variant, FontVariant::default()); + assert_eq!(loader.faces.len(), 1); + + // Test that the file can be loaded. + assert_eq!( + loader.load(Path::new("directory/../PTSans.ttf")).unwrap(), + data + ); + } +} diff --git a/src/loading/mod.rs b/src/loading/mod.rs index 7d697310c..e6cca0127 100644 --- a/src/loading/mod.rs +++ b/src/loading/mod.rs @@ -2,9 +2,11 @@ #[cfg(feature = "fs")] mod fs; +mod mem; #[cfg(feature = "fs")] pub use fs::*; +pub use mem::*; use std::io; use std::path::Path;