From 0bfee5b7772338fd39bbf708d3e31ea7bcec859b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 28 May 2021 12:44:44 +0200 Subject: Refactored loading and cache architecture --- src/env/fs.rs | 215 ------------------------------------------------ src/env/image.rs | 47 ----------- src/env/mod.rs | 243 ------------------------------------------------------- 3 files changed, 505 deletions(-) delete mode 100644 src/env/fs.rs delete mode 100644 src/env/image.rs delete mode 100644 src/env/mod.rs (limited to 'src/env') diff --git a/src/env/fs.rs b/src/env/fs.rs deleted file mode 100644 index 969ee9e0..00000000 --- a/src/env/fs.rs +++ /dev/null @@ -1,215 +0,0 @@ -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 serde::{Deserialize, Serialize}; -use ttf_parser::{name_id, Face}; -use walkdir::WalkDir; - -use super::{Buffer, Loader}; -use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight}; - -/// Loads fonts and images from the local file system. -/// -/// _This is only available when the `fs` feature is enabled._ -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct FsLoader { - faces: Vec, - files: Vec, - #[serde(skip)] - 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>; - -impl FsLoader { - /// Create a new loader without any fonts. - pub fn new() -> Self { - Self { - faces: vec![], - files: 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_path("/usr/share/fonts"); - self.search_path("/usr/local/share/fonts"); - - if let Some(dir) = dirs::font_dir() { - self.search_path(dir); - } - } - - /// Search for fonts in the operating system's font directories. - #[cfg(target_os = "macos")] - pub fn search_system(&mut self) { - self.search_path("/Library/Fonts"); - self.search_path("/Network/Library/Fonts"); - self.search_path("/System/Library/Fonts"); - - if let Some(dir) = dirs::font_dir() { - self.search_path(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_path(Path::new(&windir).join("Fonts")); - - if let Some(roaming) = dirs::config_dir() { - self.search_path(roaming.join("Microsoft\\Windows\\Fonts")); - } - - if let Some(local) = dirs::cache_dir() { - self.search_path(local.join("Microsoft\\Windows\\Fonts")); - } - } - - /// Search for all fonts at a path. - /// - /// 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) - .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. - fn search_file(&mut self, path: impl AsRef) -> io::Result<()> { - 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)?; - } - - 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()), - }; - - // Merge with an existing entry for the same family name. - self.faces.push(FaceInfo { family, variant, index }); - self.files.push(path.to_owned()); - - Ok(()) - } - - /// Paths to font files, parallel to [`faces()`](Self::faces). - pub fn files(&self) -> &[PathBuf] { - &self.files - } -} - -impl Loader for FsLoader { - fn faces(&self) -> &[FaceInfo] { - &self.faces - } - - fn load_face(&mut self, idx: usize) -> Option { - load(&mut self.cache, &self.files[idx]) - } - - fn load_file(&mut self, path: &str) -> Option { - load(&mut self.cache, Path::new(path)) - } -} - -/// Load from the file system using a cache. -fn load(cache: &mut FileCache, path: &Path) -> Option { - 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_path("fonts"); - - assert_eq!(loader.files, &[ - 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 deleted file mode 100644 index 365ff312..00000000 --- a/src/env/image.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; -use std::io::Cursor; - -use image::io::Reader as ImageReader; -use image::{DynamicImage, GenericImageView, ImageFormat}; - -/// A loaded image. -pub struct Image { - /// The original format the image was encoded in. - pub format: ImageFormat, - /// The decoded image. - pub buf: DynamicImage, -} - -impl Image { - /// Parse an image from raw data in a supported format. - /// - /// The image format is determined automatically. - pub fn parse(data: &[u8]) -> Option { - let cursor = Cursor::new(data); - let reader = ImageReader::new(cursor).with_guessed_format().ok()?; - let format = reader.format()?; - let buf = reader.decode().ok()?; - Some(Self { format, buf }) - } - - /// The width of the image. - pub fn width(&self) -> u32 { - self.buf.width() - } - - /// The height of the image. - pub fn height(&self) -> u32 { - self.buf.height() - } -} - -impl Debug for Image { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Image") - .field("format", &self.format) - .field("color", &self.buf.color()) - .field("width", &self.width()) - .field("height", &self.height()) - .finish() - } -} diff --git a/src/env/mod.rs b/src/env/mod.rs deleted file mode 100644 index 84be3e81..00000000 --- a/src/env/mod.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Font and image loading. - -#[cfg(feature = "fs")] -mod fs; -mod image; - -pub use self::image::*; -#[cfg(feature = "fs")] -pub use fs::*; - -use std::collections::{hash_map::Entry, HashMap}; -use std::rc::Rc; - -use serde::{Deserialize, Serialize}; - -use crate::font::{Face, FaceInfo, FontVariant}; - -/// Handles font and image loading. -pub struct Env { - /// The loader that serves the font face and file buffers. - loader: Box, - /// Faces indexed by [`FaceId`]. `None` if not yet loaded. - faces: Vec>, - /// Maps a family name to the ids of all faces that are part of the family. - families: HashMap>, - /// Loaded images indexed by [`ImageId`]. - images: Vec, - /// Maps from paths to loaded images. - paths: HashMap, - /// Callback for loaded font faces. - on_face_load: Option>, - /// Callback for loaded images. - on_image_load: Option>, -} - -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::>::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), - faces, - families, - images: vec![], - paths: HashMap::new(), - on_face_load: None, - on_image_load: None, - } - } - - /// 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 { - None - } - - fn load_file(&mut self, _: &str) -> Option { - 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 { - // 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 id = best?; - let idx = id.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)?; - if let Some(callback) = &self.on_face_load { - callback(id, &face); - } - *slot = Some(face); - } - - best - } - - /// Get a reference to a queried face. - /// - /// 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") - } - - /// Register a callback which is invoked when a font face was loaded. - pub fn on_face_load(&mut self, f: F) - where - F: Fn(FaceId, &Face) + 'static, - { - self.on_face_load = Some(Box::new(f)); - } - - /// Load and decode an image file from a path. - pub fn load_image(&mut self, path: &str) -> Option { - Some(match self.paths.entry(path.to_string()) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - let buffer = self.loader.load_file(path)?; - let image = Image::parse(&buffer)?; - let id = ImageId(self.images.len() as u32); - if let Some(callback) = &self.on_image_load { - callback(id, &image); - } - self.images.push(image); - *entry.insert(id) - } - }) - } - - /// Get a reference to a loaded image. - /// - /// This panics if no image with this id was loaded. This function should - /// only be called with ids returned by [`load_image()`](Self::load_image). - #[track_caller] - pub fn image(&self, id: ImageId) -> &Image { - &self.images[id.0 as usize] - } - - /// Register a callback which is invoked when an image was loaded. - pub fn on_image_load(&mut self, f: F) - where - F: Fn(ImageId, &Image) + 'static, - { - self.on_image_load = Some(Box::new(f)); - } -} - -/// Loads fonts and images 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; - - /// Load a file from a path. - fn load_file(&mut self, path: &str) -> Option; -} - -/// A shared byte buffer. -pub type Buffer = Rc>; - -/// 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); - - /// Create a face id from the raw underlying value. - /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub fn from_raw(v: u32) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub fn into_raw(self) -> u32 { - self.0 - } -} - -/// A unique identifier for a loaded image. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct ImageId(u32); - -impl ImageId { - /// Create an image id from the raw underlying value. - /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub fn from_raw(v: u32) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub fn into_raw(self) -> u32 { - self.0 - } -} -- cgit v1.2.3