Memory loader
This commit is contained in:
parent
594809e35b
commit
011865ab5c
41
src/font.rs
41
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<Item = FaceInfo> + '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<String> {
|
||||
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)]
|
||||
|
@ -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<Path>) -> 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<Path>) {
|
||||
let walk = WalkDir::new(dir)
|
||||
pub fn search_path(&mut self, path: impl AsRef<Path>) {
|
||||
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<Path>) -> io::Result<()> {
|
||||
fn search_file(&mut self, path: impl AsRef<Path>) {
|
||||
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<String> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
104
src/loading/mem.rs
Normal file
104
src/loading/mem.rs
Normal file
@ -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<FaceInfo>,
|
||||
files: HashMap<PathBuf, Cow<'static, [u8]>>,
|
||||
}
|
||||
|
||||
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<P, D>(mut self, path: P, data: D) -> Self
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
D: Into<Cow<'static, [u8]>>,
|
||||
{
|
||||
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<Self> {
|
||||
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<P, D>(&mut self, path: P, data: D)
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
D: Into<Cow<'static, [u8]>>,
|
||||
{
|
||||
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<FileHash> {
|
||||
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<Vec<u8>> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user