diff options
Diffstat (limited to 'src/env')
| -rw-r--r-- | src/env/fs.rs | 208 | ||||
| -rw-r--r-- | src/env/image.rs | 40 | ||||
| -rw-r--r-- | src/env/mod.rs | 202 |
3 files changed, 450 insertions, 0 deletions
diff --git a/src/env/fs.rs b/src/env/fs.rs new file mode 100644 index 00000000..98378722 --- /dev/null +++ b/src/env/fs.rs @@ -0,0 +1,208 @@ +use std::collections::{hash_map::Entry, HashMap}; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +use memmap2::Mmap; +use ttf_parser::{name_id, Face}; +use walkdir::WalkDir; + +use super::{Buffer, Loader}; +use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight}; + +/// Loads fonts and resources from the local file system. +/// +/// _This is only available when the `fs` feature is enabled._ +#[derive(Default, Debug, Clone)] +pub struct FsLoader { + faces: Vec<FaceInfo>, + paths: Vec<PathBuf>, + cache: FileCache, +} + +/// Maps from paths to loaded file buffers. When the buffer is `None` the file +/// does not exist or couldn't be read. +type FileCache = HashMap<PathBuf, Option<Buffer>>; + +impl FsLoader { + /// Create a new loader without any fonts. + pub fn new() -> Self { + Self { + faces: vec![], + paths: vec![], + cache: HashMap::new(), + } + } + + /// Search for fonts in the operating system's font directories. + #[cfg(all(unix, not(target_os = "macos")))] + pub fn search_system(&mut self) { + self.search_dir("/usr/share/fonts"); + self.search_dir("/usr/local/share/fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_dir(dir); + } + } + + /// Search for fonts in the operating system's font directories. + #[cfg(target_os = "macos")] + pub fn search_system(&mut self) { + self.search_dir("/Library/Fonts"); + self.search_dir("/Network/Library/Fonts"); + self.search_dir("/System/Library/Fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_dir(dir); + } + } + + /// Search for fonts in the operating system's font directories. + #[cfg(windows)] + pub fn search_system(&mut self) { + let windir = + std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string()); + + self.search_dir(Path::new(&windir).join("Fonts")); + + if let Some(roaming) = dirs::config_dir() { + self.search_dir(roaming.join("Microsoft\\Windows\\Fonts")); + } + + if let Some(local) = dirs::cache_dir() { + self.search_dir(local.join("Microsoft\\Windows\\Fonts")); + } + } + + /// Search for all fonts in a directory. + pub fn search_dir(&mut self, dir: impl AsRef<Path>) { + let walk = WalkDir::new(dir) + .follow_links(true) + .sort_by(|a, b| a.file_name().cmp(b.file_name())) + .into_iter() + .filter_map(|e| e.ok()); + + 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(); + } + _ => {} + } + } + } + } + + /// Index the font faces in the file at the given path. + /// + /// The file may form a font collection and contain multiple font faces, + /// which will then all be indexed. + pub fn search_file(&mut self, path: impl AsRef<Path>) -> io::Result<()> { + let path = path.as_ref(); + + 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)?; + } + + 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<String> { + face.names().find_map(|entry| { + if entry.name_id() == name_id { + entry.to_string() + } else { + None + } + }) + } + + 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()), + }; + + // Merge with an existing entry for the same family name. + self.faces.push(FaceInfo { family, variant, index }); + self.paths.push(path.to_owned()); + + Ok(()) + } +} + +impl Loader for FsLoader { + fn faces(&self) -> &[FaceInfo] { + &self.faces + } + + fn load_face(&mut self, idx: usize) -> Option<Buffer> { + load(&mut self.cache, &self.paths[idx]) + } + + fn load_file(&mut self, url: &str) -> Option<Buffer> { + load(&mut self.cache, Path::new(url)) + } +} + +fn load(cache: &mut FileCache, path: &Path) -> Option<Buffer> { + match cache.entry(path.to_owned()) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => { + let buffer = std::fs::read(path).ok().map(Rc::new); + entry.insert(buffer).clone() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_index_font_dir() { + let mut loader = FsLoader::new(); + loader.search_dir("fonts"); + loader.paths.sort(); + + assert_eq!(loader.paths, &[ + Path::new("fonts/EBGaramond-Bold.ttf"), + Path::new("fonts/EBGaramond-BoldItalic.ttf"), + Path::new("fonts/EBGaramond-Italic.ttf"), + Path::new("fonts/EBGaramond-Regular.ttf"), + Path::new("fonts/Inconsolata-Bold.ttf"), + Path::new("fonts/Inconsolata-Regular.ttf"), + Path::new("fonts/LatinModernMath.otf"), + Path::new("fonts/NotoSansArabic-Regular.ttf"), + Path::new("fonts/NotoSerifCJKsc-Regular.otf"), + Path::new("fonts/NotoSerifHebrew-Bold.ttf"), + Path::new("fonts/NotoSerifHebrew-Regular.ttf"), + Path::new("fonts/PTSans-Regular.ttf"), + Path::new("fonts/TwitterColorEmoji.ttf"), + ]); + } +} diff --git a/src/env/image.rs b/src/env/image.rs new file mode 100644 index 00000000..4bdb5483 --- /dev/null +++ b/src/env/image.rs @@ -0,0 +1,40 @@ +use std::fmt::{self, Debug, Formatter}; +use std::io::Cursor; + +use image::io::Reader as ImageReader; +use image::{DynamicImage, GenericImageView, ImageFormat}; + +use super::Buffer; + +/// A loaded image resource. +pub struct ImageResource { + /// The original format the image was encoded in. + pub format: ImageFormat, + /// The decoded image. + pub buf: DynamicImage, +} + +impl ImageResource { + /// Parse an image resource from raw data in a supported format. + /// + /// The image format is determined automatically. + pub fn parse(data: Buffer) -> Option<Self> { + let cursor = Cursor::new(data.as_ref()); + let reader = ImageReader::new(cursor).with_guessed_format().ok()?; + let format = reader.format()?; + let buf = reader.decode().ok()?; + Some(Self { format, buf }) + } +} + +impl Debug for ImageResource { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let (width, height) = self.buf.dimensions(); + f.debug_struct("ImageResource") + .field("format", &self.format) + .field("color", &self.buf.color()) + .field("width", &width) + .field("height", &height) + .finish() + } +} diff --git a/src/env/mod.rs b/src/env/mod.rs new file mode 100644 index 00000000..af3872dd --- /dev/null +++ b/src/env/mod.rs @@ -0,0 +1,202 @@ +//! Font and resource loading. + +#[cfg(feature = "fs")] +mod fs; +mod image; + +pub use self::image::*; +#[cfg(feature = "fs")] +pub use fs::*; + +use std::any::Any; +use std::collections::{hash_map::Entry, HashMap}; +use std::rc::Rc; + +use serde::{Deserialize, Serialize}; + +use crate::font::{Face, FaceInfo, FontVariant}; + +/// Handles font and resource loading. +pub struct Env { + /// The loader that serves the font face and file buffers. + loader: Box<dyn Loader>, + /// Loaded resources indexed by [`ResourceId`]. + resources: Vec<Box<dyn Any>>, + /// Maps from URL to loaded resource. + urls: HashMap<String, ResourceId>, + /// Faces indexed by [`FaceId`]. `None` if not yet loaded. + faces: Vec<Option<Face>>, + /// Maps a family name to the ids of all faces that are part of the family. + families: HashMap<String, Vec<FaceId>>, +} + +impl Env { + /// Create an environment from a `loader`. + pub fn new(loader: impl Loader + 'static) -> Self { + let infos = loader.faces(); + + let mut faces = vec![]; + let mut families = HashMap::<String, Vec<FaceId>>::new(); + + for (i, info) in infos.iter().enumerate() { + let id = FaceId(i as u32); + faces.push(None); + families + .entry(info.family.to_lowercase()) + .and_modify(|vec| vec.push(id)) + .or_insert_with(|| vec![id]); + } + + Self { + loader: Box::new(loader), + resources: vec![], + urls: HashMap::new(), + faces, + families, + } + } + + /// Create an empty environment for testing purposes. + pub fn blank() -> Self { + struct BlankLoader; + + impl Loader for BlankLoader { + fn faces(&self) -> &[FaceInfo] { + &[] + } + + fn load_face(&mut self, _: usize) -> Option<Buffer> { + None + } + + fn load_file(&mut self, _: &str) -> Option<Buffer> { + None + } + } + + Self::new(BlankLoader) + } + + /// Query for and load the font face from the given `family` that most + /// closely matches the given `variant`. + pub fn query_face(&mut self, family: &str, variant: FontVariant) -> Option<FaceId> { + // Check whether a family with this name exists. + let ids = self.families.get(family)?; + let infos = self.loader.faces(); + + let mut best = None; + let mut best_key = None; + + // Find the best matching variant of this font. + for &id in ids { + let current = infos[id.0 as usize].variant; + + // This is a perfect match, no need to search further. + if current == variant { + best = Some(id); + break; + } + + // If this is not a perfect match, we compute a key that we want to + // minimize among all variants. This key prioritizes style, then + // stretch distance and then weight distance. + let key = ( + current.style != variant.style, + current.stretch.distance(variant.stretch), + current.weight.distance(variant.weight), + ); + + if best_key.map_or(true, |b| key < b) { + best = Some(id); + best_key = Some(key); + } + } + + // Load the face if it's not already loaded. + let idx = best?.0 as usize; + let slot = &mut self.faces[idx]; + if slot.is_none() { + let index = infos[idx].index; + let buffer = self.loader.load_face(idx)?; + let face = Face::new(buffer, index)?; + *slot = Some(face); + } + + best + } + + /// Load a file from a local or remote URL, parse it into a cached resource + /// and return a unique identifier that allows to retrieve the parsed + /// resource through [`resource()`](Self::resource). + pub fn load_resource<F, R>(&mut self, url: &str, parse: F) -> Option<ResourceId> + where + F: FnOnce(Buffer) -> Option<R>, + R: 'static, + { + Some(match self.urls.entry(url.to_string()) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let buffer = self.loader.load_file(url)?; + let resource = parse(buffer)?; + let len = self.resources.len(); + self.resources.push(Box::new(resource)); + *entry.insert(ResourceId(len as u32)) + } + }) + } + + /// Get a reference to a queried face. + /// + /// # Panics + /// This panics if no face with this id was loaded. This function should + /// only be called with ids returned by [`query_face()`](Self::query_face). + #[track_caller] + pub fn face(&self, id: FaceId) -> &Face { + self.faces[id.0 as usize].as_ref().expect("font face was not loaded") + } + + /// Get a reference to a loaded resource. + /// + /// This panics if no resource with this id was loaded. This function should + /// only be called with ids returned by + /// [`load_resource()`](Self::load_resource). + #[track_caller] + pub fn resource<R: 'static>(&self, id: ResourceId) -> &R { + self.resources[id.0 as usize] + .downcast_ref() + .expect("bad resource type") + } +} + +/// Loads fonts and resources from a remote or local source. +pub trait Loader { + /// Descriptions of all font faces this loader serves. + fn faces(&self) -> &[FaceInfo]; + + /// Load the font face with the given index in [`faces()`](Self::faces). + fn load_face(&mut self, idx: usize) -> Option<Buffer>; + + /// Load a file from a URL. + fn load_file(&mut self, url: &str) -> Option<Buffer>; +} + +/// A shared byte buffer. +pub type Buffer = Rc<Vec<u8>>; + +/// A unique identifier for a loaded font face. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct FaceId(u32); + +impl FaceId { + /// A blank initialization value. + pub const MAX: Self = Self(u32::MAX); +} + +/// A unique identifier for a loaded resource. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct ResourceId(u32); + +impl ResourceId { + /// A blank initialization value. + pub const MAX: Self = Self(u32::MAX); +} |
