summaryrefslogtreecommitdiff
path: root/src/env
diff options
context:
space:
mode:
Diffstat (limited to 'src/env')
-rw-r--r--src/env/fs.rs208
-rw-r--r--src/env/image.rs40
-rw-r--r--src/env/mod.rs202
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);
+}