summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-04-23 17:46:14 +0200
committerLaurenz <laurmaedje@gmail.com>2021-04-23 17:46:14 +0200
commit6292d25afb3080f606844d7e03fec5c80f0140ff (patch)
tree8d2495edccaa6bdaaa4d012be18df15788c640d0 /src
parent72478946c261f04754c11f8a6abf6eb0f43dea31 (diff)
Reengineer font and resource loading 🏞
Diffstat (limited to 'src')
-rw-r--r--src/env.rs153
-rw-r--r--src/env/fs.rs208
-rw-r--r--src/env/image.rs40
-rw-r--r--src/env/mod.rs202
-rw-r--r--src/exec/state.rs6
-rw-r--r--src/font.rs496
-rw-r--r--src/layout/frame.rs8
-rw-r--r--src/layout/shaping.rs44
-rw-r--r--src/library/font.rs26
-rw-r--r--src/library/image.rs11
-rw-r--r--src/library/mod.rs9
-rw-r--r--src/main.rs14
-rw-r--r--src/pdf/mod.rs36
13 files changed, 952 insertions, 301 deletions
diff --git a/src/env.rs b/src/env.rs
deleted file mode 100644
index 3b8fd4cd..00000000
--- a/src/env.rs
+++ /dev/null
@@ -1,153 +0,0 @@
-//! Environment interactions.
-
-use std::any::Any;
-use std::collections::{hash_map::Entry, HashMap};
-use std::fmt::{self, Debug, Formatter};
-use std::fs;
-use std::io::Cursor;
-use std::path::{Path, PathBuf};
-
-use fontdock::{FaceId, FontSource};
-use image::io::Reader as ImageReader;
-use image::{DynamicImage, GenericImageView, ImageFormat};
-use serde::{Deserialize, Serialize};
-
-#[cfg(feature = "fs")]
-use fontdock::{FsIndex, FsSource};
-
-use crate::font::FaceBuf;
-
-/// Encapsulates all environment dependencies (fonts, resources).
-#[derive(Debug)]
-pub struct Env {
- /// Loads fonts from a dynamic font source.
- pub fonts: FontLoader,
- /// Loads resource from the file system.
- pub resources: ResourceLoader,
-}
-
-impl Env {
- /// Create an empty environment for testing purposes.
- pub fn blank() -> Self {
- struct BlankSource;
-
- impl FontSource for BlankSource {
- type Face = FaceBuf;
-
- fn load(&self, _: FaceId) -> Option<Self::Face> {
- None
- }
- }
-
- Self {
- fonts: FontLoader::new(Box::new(BlankSource), vec![]),
- resources: ResourceLoader::new(),
- }
- }
-}
-
-/// A font loader that is backed by a dynamic source.
-pub type FontLoader = fontdock::FontLoader<Box<dyn FontSource<Face = FaceBuf>>>;
-
-/// Simplify font loader construction from an [`FsIndex`].
-#[cfg(feature = "fs")]
-pub trait FsIndexExt {
- /// Create a font loader backed by a boxed [`FsSource`] which serves all
- /// indexed font faces.
- fn into_dynamic_loader(self) -> FontLoader;
-}
-
-#[cfg(feature = "fs")]
-impl FsIndexExt for FsIndex {
- fn into_dynamic_loader(self) -> FontLoader {
- let (files, descriptors) = self.into_vecs();
- FontLoader::new(Box::new(FsSource::new(files)), descriptors)
- }
-}
-
-/// Loads resource from the file system.
-pub struct ResourceLoader {
- paths: HashMap<PathBuf, ResourceId>,
- entries: Vec<Box<dyn Any>>,
-}
-
-/// A unique identifier for a resource.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-#[derive(Serialize, Deserialize)]
-pub struct ResourceId(usize);
-
-impl ResourceLoader {
- /// Create a new resource loader.
- pub fn new() -> Self {
- Self { paths: HashMap::new(), entries: vec![] }
- }
-
- /// Load a resource from a path and parse it.
- pub fn load<P, F, R>(&mut self, path: P, parse: F) -> Option<(ResourceId, &R)>
- where
- P: AsRef<Path>,
- F: FnOnce(Vec<u8>) -> Option<R>,
- R: 'static,
- {
- let path = path.as_ref();
- let id = match self.paths.entry(path.to_owned()) {
- Entry::Occupied(entry) => *entry.get(),
- Entry::Vacant(entry) => {
- let data = fs::read(path).ok()?;
- let resource = parse(data)?;
- let len = self.entries.len();
- self.entries.push(Box::new(resource));
- *entry.insert(ResourceId(len))
- }
- };
-
- Some((id, self.loaded(id)))
- }
-
- /// Retrieve a previously loaded resource by its id.
- ///
- /// # Panics
- /// This panics if no resource with this id was loaded.
- #[track_caller]
- pub fn loaded<R: 'static>(&self, id: ResourceId) -> &R {
- self.entries[id.0].downcast_ref().expect("bad resource type")
- }
-}
-
-impl Debug for ResourceLoader {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.debug_set().entries(self.paths.keys()).finish()
- }
-}
-
-/// 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: Vec<u8>) -> Option<Self> {
- let reader = ImageReader::new(Cursor::new(data)).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/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);
+}
diff --git a/src/exec/state.rs b/src/exec/state.rs
index 82f653e9..f4bc6b7b 100644
--- a/src/exec/state.rs
+++ b/src/exec/state.rs
@@ -1,10 +1,8 @@
use std::fmt::{self, Display, Formatter};
use std::rc::Rc;
-use fontdock::{FontStretch, FontStyle, FontVariant, FontWeight};
-
use crate::color::{Color, RgbaColor};
-use crate::font::VerticalFontMetric;
+use crate::font::{FontStretch, FontStyle, FontVariant, FontWeight, VerticalFontMetric};
use crate::geom::*;
use crate::layout::Fill;
use crate::paper::{Paper, PaperClass, PAPER_A4};
@@ -181,7 +179,7 @@ impl Default for FontState {
variant: FontVariant {
style: FontStyle::Normal,
weight: FontWeight::REGULAR,
- stretch: FontStretch::Normal,
+ stretch: FontStretch::NORMAL,
},
size: Length::pt(11.0),
top_edge: VerticalFontMetric::CapHeight,
diff --git a/src/font.rs b/src/font.rs
index 1ac8fea3..52912664 100644
--- a/src/font.rs
+++ b/src/font.rs
@@ -1,27 +1,60 @@
//! Font handling.
-use std::fmt::{self, Display, Formatter};
+use std::fmt::{self, Debug, Display, Formatter};
-use fontdock::FaceFromVec;
+use serde::{Deserialize, Serialize};
+use crate::env::Buffer;
use crate::geom::Length;
-/// An owned font face.
-pub struct FaceBuf {
- data: Box<[u8]>,
+/// A font face.
+pub struct Face {
+ buffer: Buffer,
index: u32,
- inner: rustybuzz::Face<'static>,
+ ttf: rustybuzz::Face<'static>,
units_per_em: f64,
- ascender: f64,
- cap_height: f64,
- x_height: f64,
- descender: f64,
+ ascender: Em,
+ cap_height: Em,
+ x_height: Em,
+ descender: Em,
}
-impl FaceBuf {
- /// The raw face data.
- pub fn data(&self) -> &[u8] {
- &self.data
+impl Face {
+ /// Parse a font face from a buffer and collection index.
+ pub fn new(buffer: Buffer, index: u32) -> Option<Self> {
+ // SAFETY:
+ // - The slices's location is stable in memory:
+ // - We don't move the underlying vector
+ // - Nobody else can move it since we haved a strong ref to the `Rc`.
+ // - The internal static lifetime is not leaked because its rewritten
+ // to the self-lifetime in `ttf()`.
+ let slice: &'static [u8] =
+ unsafe { std::slice::from_raw_parts(buffer.as_ptr(), buffer.len()) };
+
+ let ttf = rustybuzz::Face::from_slice(slice, index)?;
+
+ // Look up some metrics we may need often.
+ let units_per_em = f64::from(ttf.units_per_em());
+ let ascender = ttf.typographic_ascender().unwrap_or(ttf.ascender());
+ let cap_height = ttf.capital_height().filter(|&h| h > 0).unwrap_or(ascender);
+ let x_height = ttf.x_height().filter(|&h| h > 0).unwrap_or(ascender);
+ let descender = ttf.typographic_descender().unwrap_or(ttf.descender());
+
+ Some(Self {
+ buffer,
+ index,
+ ttf,
+ units_per_em,
+ ascender: Em::from_units(ascender, units_per_em),
+ cap_height: Em::from_units(cap_height, units_per_em),
+ x_height: Em::from_units(x_height, units_per_em),
+ descender: Em::from_units(descender, units_per_em),
+ })
+ }
+
+ /// The underlying buffer.
+ pub fn buffer(&self) -> &Buffer {
+ &self.buffer
}
/// The collection index.
@@ -29,74 +62,27 @@ impl FaceBuf {
self.index
}
- /// Get a reference to the underlying ttf-parser/rustybuzz face.
+ /// A reference to the underlying `ttf-parser` / `rustybuzz` face.
pub fn ttf(&self) -> &rustybuzz::Face<'_> {
// We can't implement Deref because that would leak the internal 'static
// lifetime.
- &self.inner
+ &self.ttf
}
/// Look up a vertical metric.
- pub fn vertical_metric(&self, metric: VerticalFontMetric) -> EmLength {
- self.convert(match metric {
+ pub fn vertical_metric(&self, metric: VerticalFontMetric) -> Em {
+ match metric {
VerticalFontMetric::Ascender => self.ascender,
VerticalFontMetric::CapHeight => self.cap_height,
VerticalFontMetric::XHeight => self.x_height,
- VerticalFontMetric::Baseline => 0.0,
+ VerticalFontMetric::Baseline => Em::ZERO,
VerticalFontMetric::Descender => self.descender,
- })
- }
-
- /// Convert from font units to an em length length.
- pub fn convert(&self, units: impl Into<f64>) -> EmLength {
- EmLength(units.into() / self.units_per_em)
- }
-}
-
-impl FaceFromVec for FaceBuf {
- fn from_vec(vec: Vec<u8>, index: u32) -> Option<Self> {
- let data = vec.into_boxed_slice();
-
- // SAFETY: The slices's location is stable in memory since we don't
- // touch it and it can't be touched from outside this type.
- let slice: &'static [u8] =
- unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
-
- let inner = rustybuzz::Face::from_slice(slice, index)?;
-
- // Look up some metrics we may need often.
- let units_per_em = inner.units_per_em();
- let ascender = inner.typographic_ascender().unwrap_or(inner.ascender());
- let cap_height = inner.capital_height().filter(|&h| h > 0).unwrap_or(ascender);
- let x_height = inner.x_height().filter(|&h| h > 0).unwrap_or(ascender);
- let descender = inner.typographic_descender().unwrap_or(inner.descender());
-
- Some(Self {
- data,
- index,
- inner,
- units_per_em: f64::from(units_per_em),
- ascender: f64::from(ascender),
- cap_height: f64::from(cap_height),
- x_height: f64::from(x_height),
- descender: f64::from(descender),
- })
+ }
}
-}
-/// A length in resolved em units.
-#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
-pub struct EmLength(f64);
-
-impl EmLength {
- /// Convert to a length at the given font size.
- pub fn scale(self, size: Length) -> Length {
- self.0 * size
- }
-
- /// Get the number of em units.
- pub fn get(self) -> f64 {
- self.0
+ /// Convert from font units to an em length.
+ pub fn to_em(&self, units: impl Into<f64>) -> Em {
+ Em::from_units(units, self.units_per_em)
}
}
@@ -132,3 +118,373 @@ impl Display for VerticalFontMetric {
})
}
}
+
+/// A length in em units.
+///
+/// `1em` is the same as the font size.
+#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
+pub struct Em(f64);
+
+impl Em {
+ /// The zero length.
+ pub const ZERO: Self = Self(0.0);
+
+ /// Create an em length.
+ pub fn new(em: f64) -> Self {
+ Self(em)
+ }
+
+ /// Convert units to an em length at the given units per em.
+ pub fn from_units(units: impl Into<f64>, units_per_em: f64) -> Self {
+ Self(units.into() / units_per_em)
+ }
+
+ /// The number of em units.
+ pub fn get(self) -> f64 {
+ self.0
+ }
+
+ /// Convert to a length at the given font size.
+ pub fn to_length(self, font_size: Length) -> Length {
+ self.0 * font_size
+ }
+}
+
+/// Properties of a single font face.
+#[derive(Debug, Clone, PartialEq)]
+pub struct FaceInfo {
+ /// The typographic font family this face is part of.
+ pub family: String,
+ /// Properties that distinguish this face from other faces in the same
+ /// family.
+ pub variant: FontVariant,
+ /// The collection index in the font file.
+ pub index: u32,
+}
+
+/// Properties that distinguish a face from other faces in the same family.
+#[derive(Default, Debug, Copy, Clone, PartialEq)]
+pub struct FontVariant {
+ /// The style of the face (normal / italic / oblique).
+ pub style: FontStyle,
+ /// How heavy the face is (100 - 900).
+ pub weight: FontWeight,
+ /// How condensed or expanded the face is (0.5 - 2.0).
+ pub stretch: FontStretch,
+}
+
+impl FontVariant {
+ /// Create a variant from its three components.
+ pub fn new(style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Self {
+ Self { style, weight, stretch }
+ }
+}
+
+/// The style of a font face.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Serialize, Deserialize)]
+pub enum FontStyle {
+ /// The default style.
+ Normal,
+ /// A cursive style.
+ Italic,
+ /// A slanted style.
+ Oblique,
+}
+
+impl FontStyle {
+ /// Create a font style from a lowercase name like `italic`.
+ pub fn from_str(name: &str) -> Option<FontStyle> {
+ Some(match name {
+ "normal" => Self::Normal,
+ "italic" => Self::Italic,
+ "oblique" => Self::Oblique,
+ _ => return None,
+ })
+ }
+
+ /// The lowercase string representation of this style.
+ pub fn to_str(self) -> &'static str {
+ match self {
+ Self::Normal => "normal",
+ Self::Italic => "italic",
+ Self::Oblique => "oblique",
+ }
+ }
+}
+
+impl Default for FontStyle {
+ fn default() -> Self {
+ Self::Normal
+ }
+}
+
+impl Display for FontStyle {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(self.to_str())
+ }
+}
+
+/// The weight of a font face.
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct FontWeight(u16);
+
+impl FontWeight {
+ /// Thin weight (100).
+ pub const THIN: Self = Self(100);
+
+ /// Extra light weight (200).
+ pub const EXTRALIGHT: Self = Self(200);
+
+ /// Light weight (300).
+ pub const LIGHT: Self = Self(300);
+
+ /// Regular weight (400).
+ pub const REGULAR: Self = Self(400);
+
+ /// Medium weight (500).
+ pub const MEDIUM: Self = Self(500);
+
+ /// Semibold weight (600).
+ pub const SEMIBOLD: Self = Self(600);
+
+ /// Bold weight (700).
+ pub const BOLD: Self = Self(700);
+
+ /// Extrabold weight (800).
+ pub const EXTRABOLD: Self = Self(800);
+
+ /// Black weight (900).
+ pub const BLACK: Self = Self(900);
+
+ /// Create a font weight from a number between 100 and 900, clamping it if
+ /// necessary.
+ pub fn from_number(weight: u16) -> Self {
+ Self(weight.max(100).min(900))
+ }
+
+ /// Create a font weight from a lowercase name like `light`.
+ pub fn from_str(name: &str) -> Option<Self> {
+ Some(match name {
+ "thin" => Self::THIN,
+ "extralight" => Self::EXTRALIGHT,
+ "light" => Self::LIGHT,
+ "regular" => Self::REGULAR,
+ "medium" => Self::MEDIUM,
+ "semibold" => Self::SEMIBOLD,
+ "bold" => Self::BOLD,
+ "extrabold" => Self::EXTRABOLD,
+ "black" => Self::BLACK,
+ _ => return None,
+ })
+ }
+
+ /// The number between 100 and 900.
+ pub fn to_number(self) -> u16 {
+ self.0
+ }
+
+ /// The lowercase string representation of this weight if it is divisible by
+ /// 100.
+ pub fn to_str(self) -> Option<&'static str> {
+ Some(match self {
+ Self::THIN => "thin",
+ Self::EXTRALIGHT => "extralight",
+ Self::LIGHT => "light",
+ Self::REGULAR => "regular",
+ Self::MEDIUM => "medium",
+ Self::SEMIBOLD => "semibold",
+ Self::BOLD => "bold",
+ Self::EXTRABOLD => "extrabold",
+ Self::BLACK => "black",
+ _ => return None,
+ })
+ }
+
+ /// Add (or remove) weight, saturating at the boundaries of 100 and 900.
+ pub fn thicken(self, delta: i16) -> Self {
+ Self((self.0 as i16).saturating_add(delta).max(100).min(900) as u16)
+ }
+
+ /// The absolute number distance between this and another font weight.
+ pub fn distance(self, other: Self) -> u16 {
+ (self.0 as i16 - other.0 as i16).abs() as u16
+ }
+}
+
+impl Default for FontWeight {
+ fn default() -> Self {
+ Self::REGULAR
+ }
+}
+
+impl Display for FontWeight {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self.to_str() {
+ Some(name) => f.pad(name),
+ None => write!(f, "{}", self.0),
+ }
+ }
+}
+
+impl Debug for FontWeight {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.pad(match *self {
+ Self::THIN => "Thin",
+ Self::EXTRALIGHT => "Extralight",
+ Self::LIGHT => "Light",
+ Self::REGULAR => "Regular",
+ Self::MEDIUM => "Medium",
+ Self::SEMIBOLD => "Semibold",
+ Self::BOLD => "Bold",
+ Self::EXTRABOLD => "Extrabold",
+ Self::BLACK => "Black",
+ _ => return write!(f, "{}", self.0),
+ })
+ }
+}
+
+/// The width of a font face.
+#[derive(Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
+pub struct FontStretch(f32);
+
+impl FontStretch {
+ /// Ultra-condensed stretch (50%).
+ pub const ULTRA_CONDENSED: Self = Self(0.5);
+
+ /// Extra-condensed stretch weight (62.5%).
+ pub const EXTRA_CONDENSED: Self = Self(0.625);
+
+ /// Condensed stretch (75%).
+ pub const CONDENSED: Self = Self(0.75);
+
+ /// Semi-condensed stretch (87.5%).
+ pub const SEMI_CONDENSED: Self = Self(0.875);
+
+ /// Normal stretch (100%).
+ pub const NORMAL: Self = Self(1.0);
+
+ /// Semi-expanded stretch (112.5%).
+ pub const SEMI_EXPANDED: Self = Self(1.125);
+
+ /// Expanded stretch (125%).
+ pub const EXPANDED: Self = Self(1.25);
+
+ /// Extra-expanded stretch (150%).
+ pub const EXTRA_EXPANDED: Self = Self(1.5);
+
+ /// Ultra-expanded stretch (200%).
+ pub const ULTRA_EXPANDED: Self = Self(2.0);
+
+ /// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if
+ /// necessary.
+ pub fn from_ratio(ratio: f32) -> Self {
+ Self(ratio.max(0.5).min(2.0))
+ }
+
+ /// Create a font stretch from an OpenType-style number between 1 and 9,
+ /// clamping it if necessary.
+ pub fn from_number(stretch: u16) -> Self {
+ match stretch {
+ 0 | 1 => Self::ULTRA_CONDENSED,
+ 2 => Self::EXTRA_CONDENSED,
+ 3 => Self::CONDENSED,
+ 4 => Self::SEMI_CONDENSED,
+ 5 => Self::NORMAL,
+ 6 => Self::SEMI_EXPANDED,
+ 7 => Self::EXPANDED,
+ 8 => Self::EXTRA_EXPANDED,
+ _ => Self::ULTRA_EXPANDED,
+ }
+ }
+
+ /// Create a font stretch from a lowercase name like `extra-expanded`.
+ pub fn from_str(name: &str) -> Option<Self> {
+ Some(match name {
+ "ultra-condensed" => Self::ULTRA_CONDENSED,
+ "extra-condensed" => Self::EXTRA_CONDENSED,
+ "condensed" => Self::CONDENSED,
+ "semi-condensed" => Self::SEMI_CONDENSED,
+ "normal" => Self::NORMAL,
+ "semi-expanded" => Self::SEMI_EXPANDED,
+ "expanded" => Self::EXPANDED,
+ "extra-expanded" => Self::EXTRA_EXPANDED,
+ "ultra-expanded" => Self::ULTRA_EXPANDED,
+ _ => return None,
+ })
+ }
+
+ /// The ratio between 0.5 and 2.0 corresponding to this stretch.
+ pub fn to_ratio(self) -> f32 {
+ self.0
+ }
+
+ /// The lowercase string representation of this stretch is one of the named
+ /// ones.
+ pub fn to_str(self) -> Option<&'static str> {
+ Some(match self {
+ s if s == Self::ULTRA_CONDENSED => "ultra-condensed",
+ s if s == Self::EXTRA_CONDENSED => "extra-condensed",
+ s if s == Self::CONDENSED => "condensed",
+ s if s == Self::SEMI_CONDENSED => "semi-condensed",
+ s if s == Self::NORMAL => "normal",
+ s if s == Self::SEMI_EXPANDED => "semi-expanded",
+ s if s == Self::EXPANDED => "expanded",
+ s if s == Self::EXTRA_EXPANDED => "extra-expanded",
+ s if s == Self::ULTRA_EXPANDED => "ultra-expanded",
+ _ => return None,
+ })
+ }
+
+ /// The absolute ratio distance between this and another font stretch.
+ pub fn distance(self, other: Self) -> f32 {
+ (self.0 - other.0).abs()
+ }
+}
+
+impl Default for FontStretch {
+ fn default() -> Self {
+ Self::NORMAL
+ }
+}
+
+impl Display for FontStretch {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self.to_str() {
+ Some(name) => f.pad(name),
+ None => write!(f, "{}", self.0),
+ }
+ }
+}
+
+impl Debug for FontStretch {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(match *self {
+ s if s == Self::ULTRA_CONDENSED => "UltraCondensed",
+ s if s == Self::EXTRA_CONDENSED => "ExtraCondensed",
+ s if s == Self::CONDENSED => "Condensed",
+ s if s == Self::SEMI_CONDENSED => "SemiCondensed",
+ s if s == Self::NORMAL => "Normal",
+ s if s == Self::SEMI_EXPANDED => "SemiExpanded",
+ s if s == Self::EXPANDED => "Expanded",
+ s if s == Self::EXTRA_EXPANDED => "ExtraExpanded",
+ s if s == Self::ULTRA_EXPANDED => "UltraExpanded",
+ _ => return write!(f, "{}", self.0),
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_font_weight_distance() {
+ let d = |a, b| FontWeight(a).distance(FontWeight(b));
+ assert_eq!(d(500, 200), 300);
+ assert_eq!(d(500, 500), 0);
+ assert_eq!(d(500, 900), 400);
+ assert_eq!(d(10, 100), 90);
+ }
+}
diff --git a/src/layout/frame.rs b/src/layout/frame.rs
index 24ba65ce..9890e33f 100644
--- a/src/layout/frame.rs
+++ b/src/layout/frame.rs
@@ -1,7 +1,5 @@
-use fontdock::FaceId;
-
use crate::color::Color;
-use crate::env::ResourceId;
+use crate::env::{FaceId, ResourceId};
use crate::geom::{Length, Path, Point, Size};
use serde::{Deserialize, Serialize};
@@ -114,15 +112,13 @@ pub enum Shape {
pub enum Fill {
/// The fill is a color.
Color(Color),
- /// The fill is an image.
- Image(Image),
}
/// An image element.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct Image {
/// The image resource.
- pub res: ResourceId,
+ pub id: ResourceId,
/// The size of the image in the document.
pub size: Size,
}
diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs
index 47d19a62..e6e28a4a 100644
--- a/src/layout/shaping.rs
+++ b/src/layout/shaping.rs
@@ -2,14 +2,13 @@ use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::ops::Range;
-use fontdock::FaceId;
use rustybuzz::UnicodeBuffer;
use ttf_parser::GlyphId;
use super::{Element, Frame, Glyph, LayoutContext, Text};
-use crate::env::FontLoader;
+use crate::env::FaceId;
use crate::exec::FontProps;
-use crate::font::FaceBuf;
+use crate::font::Face;
use crate::geom::{Dir, Length, Point, Size};
use crate::util::SliceExt;
@@ -75,10 +74,10 @@ impl<'a> ShapedText<'a> {
glyphs: vec![],
};
- let face = ctx.env.fonts.face(face_id);
+ let face = ctx.env.face(face_id);
for glyph in group {
- let x_advance = face.convert(glyph.x_advance).scale(self.props.size);
- let x_offset = face.convert(glyph.x_offset).scale(self.props.size);
+ let x_advance = face.to_em(glyph.x_advance).to_length(self.props.size);
+ let x_offset = face.to_em(glyph.x_offset).to_length(self.props.size);
text.glyphs.push(Glyph {
id: glyph.glyph_id.0,
x_advance,
@@ -101,7 +100,7 @@ impl<'a> ShapedText<'a> {
text_range: Range<usize>,
) -> ShapedText<'a> {
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
- let (size, baseline) = measure(&mut ctx.env.fonts, glyphs, self.props);
+ let (size, baseline) = measure(ctx, glyphs, self.props);
Self {
text: &self.text[text_range],
dir: self.dir,
@@ -187,15 +186,13 @@ pub fn shape<'a>(
dir: Dir,
props: &'a FontProps,
) -> ShapedText<'a> {
- let loader = &mut ctx.env.fonts;
-
let mut glyphs = vec![];
let families = props.families.iter();
if !text.is_empty() {
- shape_segment(loader, &mut glyphs, 0, text, dir, props, families, None);
+ shape_segment(ctx, &mut glyphs, 0, text, dir, props, families, None);
}
- let (size, baseline) = measure(loader, &glyphs, props);
+ let (size, baseline) = measure(ctx, &glyphs, props);
ShapedText {
text,
@@ -209,7 +206,7 @@ pub fn shape<'a>(
/// Shape text with font fallback using the `families` iterator.
fn shape_segment<'a>(
- loader: &mut FontLoader,
+ ctx: &mut LayoutContext,
glyphs: &mut Vec<ShapedGlyph>,
base: usize,
text: &str,
@@ -222,7 +219,7 @@ fn shape_segment<'a>(
let (face_id, fallback) = loop {
// Try to load the next available font family.
match families.next() {
- Some(family) => match loader.query(family, props.variant) {
+ Some(family) => match ctx.env.query_face(family, props.variant) {
Some(id) => break (id, true),
None => {}
},
@@ -249,7 +246,7 @@ fn shape_segment<'a>(
});
// Shape!
- let buffer = rustybuzz::shape(loader.face(face_id).ttf(), &[], buffer);
+ let buffer = rustybuzz::shape(ctx.env.face(face_id).ttf(), &[], buffer);
let infos = buffer.glyph_infos();
let pos = buffer.glyph_positions();
@@ -313,7 +310,7 @@ fn shape_segment<'a>(
// Recursively shape the tofu sequence with the next family.
shape_segment(
- loader,
+ ctx,
glyphs,
base + range.start,
&text[range],
@@ -331,34 +328,35 @@ fn shape_segment<'a>(
/// Measure the size and baseline of a run of shaped glyphs with the given
/// properties.
fn measure(
- loader: &mut FontLoader,
+ ctx: &mut LayoutContext,
glyphs: &[ShapedGlyph],
props: &FontProps,
) -> (Size, Length) {
let mut width = Length::ZERO;
let mut top = Length::ZERO;
let mut bottom = Length::ZERO;
- let mut expand_vertical = |face: &FaceBuf| {
- top = top.max(face.vertical_metric(props.top_edge).scale(props.size));
- bottom = bottom.max(-face.vertical_metric(props.bottom_edge).scale(props.size));
+ let mut expand_vertical = |face: &Face| {
+ top = top.max(face.vertical_metric(props.top_edge).to_length(props.size));
+ bottom =
+ bottom.max(-face.vertical_metric(props.bottom_edge).to_length(props.size));
};
if glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the
// first available font.
for family in props.families.iter() {
- if let Some(face_id) = loader.query(family, props.variant) {
- expand_vertical(loader.face(face_id));
+ if let Some(face_id) = ctx.env.query_face(family, props.variant) {
+ expand_vertical(ctx.env.face(face_id));
break;
}
}
} else {
for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
- let face = loader.face(face_id);
+ let face = ctx.env.face(face_id);
expand_vertical(face);
for glyph in group {
- width += face.convert(glyph.x_advance).scale(props.size);
+ width += face.to_em(glyph.x_advance).to_length(props.size);
}
}
}
diff --git a/src/library/font.rs b/src/library/font.rs
index 6afc617b..cf329567 100644
--- a/src/library/font.rs
+++ b/src/library/font.rs
@@ -1,5 +1,5 @@
+use crate::font::{FontStretch, FontStyle, FontWeight};
use crate::layout::Fill;
-use fontdock::{FontStretch, FontStyle, FontWeight};
use super::*;
@@ -156,7 +156,12 @@ typify! {
FontWeight: "font weight",
Value::Int(number) => {
let [min, max] = [Self::THIN, Self::BLACK];
- let message = || format!("should be between {:#?} and {:#?}", min, max);
+ let message = || format!(
+ "should be between {} and {}",
+ min.to_number(),
+ max.to_number(),
+ );
+
return if number < i64::from(min.to_number()) {
CastResult::Warn(min, message())
} else if number > i64::from(max.to_number()) {
@@ -170,11 +175,18 @@ typify! {
typify! {
FontStretch: "font stretch",
Value::Relative(relative) => {
- let f = |stretch: Self| Relative::new(stretch.to_ratio());
- let [min, max] = [f(Self::UltraCondensed), f(Self::UltraExpanded)];
- let value = Self::from_ratio(relative.get());
- return if relative < min || relative > max {
- CastResult::Warn(value, format!("should be between {} and {}", min, max))
+ let [min, max] = [Self::ULTRA_CONDENSED, Self::ULTRA_EXPANDED];
+ let message = || format!(
+ "should be between {} and {}",
+ Relative::new(min.to_ratio() as f64),
+ Relative::new(max.to_ratio() as f64),
+ );
+
+ let ratio = relative.get() as f32;
+ let value = Self::from_ratio(ratio);
+
+ return if ratio < min.to_ratio() || ratio > max.to_ratio() {
+ CastResult::Warn(value, message())
} else {
CastResult::Ok(value)
};
diff --git a/src/library/image.rs b/src/library/image.rs
index 09b56336..134590bb 100644
--- a/src/library/image.rs
+++ b/src/library/image.rs
@@ -20,10 +20,11 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
Value::template("image", move |ctx| {
if let Some(path) = &path {
- let loaded = ctx.env.resources.load(&path.v, ImageResource::parse);
- if let Some((res, img)) = loaded {
+ let loaded = ctx.env.load_resource(&path.v, ImageResource::parse);
+ if let Some(id) = loaded {
+ let img = ctx.env.resource::<ImageResource>(id);
let dimensions = img.buf.dimensions();
- ctx.push(ImageNode { res, dimensions, width, height });
+ ctx.push(ImageNode { id, dimensions, width, height });
} else {
ctx.diag(error!(path.span, "failed to load image"));
}
@@ -35,7 +36,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
#[derive(Debug, Clone, PartialEq)]
struct ImageNode {
/// The resource id of the image file.
- res: ResourceId,
+ id: ResourceId,
/// The pixel dimensions of the image.
dimensions: (u32, u32),
/// The fixed width, if any.
@@ -74,7 +75,7 @@ impl Layout for ImageNode {
};
let mut frame = Frame::new(size, size.height);
- frame.push(Point::ZERO, Element::Image(Image { res: self.res, size }));
+ frame.push(Point::ZERO, Element::Image(Image { id: self.id, size }));
vec![frame]
}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 738348ee..5018f0b4 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -29,12 +29,11 @@ pub use spacing::*;
use std::fmt::{self, Display, Formatter};
-use fontdock::{FontStyle, FontWeight};
-
-use crate::eval::{AnyValue, FuncValue, Scope};
-use crate::eval::{EvalContext, FuncArgs, TemplateValue, Value};
+use crate::eval::{
+ AnyValue, EvalContext, FuncArgs, FuncValue, Scope, TemplateValue, Value,
+};
use crate::exec::{Exec, FontFamily};
-use crate::font::VerticalFontMetric;
+use crate::font::{FontStyle, FontWeight, VerticalFontMetric};
use crate::geom::*;
use crate::syntax::{Node, Spanned};
diff --git a/src/main.rs b/src/main.rs
index 80990035..05827681 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,10 +2,9 @@ use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, bail, Context};
-use fontdock::FsIndex;
use typst::diag::Pass;
-use typst::env::{Env, FsIndexExt, ResourceLoader};
+use typst::env::{Env, FsLoader};
use typst::exec::State;
use typst::library;
use typst::parse::LineMap;
@@ -35,14 +34,11 @@ fn main() -> anyhow::Result<()> {
let src = fs::read_to_string(src_path).context("Failed to read from source file.")?;
- let mut index = FsIndex::new();
- index.search_dir("fonts");
- index.search_system();
+ let mut loader = FsLoader::new();
+ loader.search_dir("fonts");
+ loader.search_system();
- let mut env = Env {
- fonts: index.into_dynamic_loader(),
- resources: ResourceLoader::new(),
- };
+ let mut env = Env::new(loader);
let scope = library::_new();
let state = State::default();
diff --git a/src/pdf/mod.rs b/src/pdf/mod.rs
index 36527544..82acbbaa 100644
--- a/src/pdf/mod.rs
+++ b/src/pdf/mod.rs
@@ -4,7 +4,6 @@ use std::cmp::Eq;
use std::collections::HashMap;
use std::hash::Hash;
-use fontdock::FaceId;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
use miniz_oxide::deflate;
use pdf_writer::{
@@ -14,8 +13,8 @@ use pdf_writer::{
use ttf_parser::{name_id, GlyphId};
use crate::color::Color;
-use crate::env::{Env, ImageResource, ResourceId};
-use crate::font::{EmLength, VerticalFontMetric};
+use crate::env::{Env, FaceId, ImageResource, ResourceId};
+use crate::font::{Em, VerticalFontMetric};
use crate::geom::{self, Length, Size};
use crate::layout::{Element, Fill, Frame, Image, Shape};
@@ -53,11 +52,11 @@ impl<'a> PdfExporter<'a> {
match element {
Element::Text(shaped) => fonts.insert(shaped.face_id),
Element::Image(image) => {
- let img = env.resources.loaded::<ImageResource>(image.res);
+ let img = env.resource::<ImageResource>(image.id);
if img.buf.color().has_alpha() {
alpha_masks += 1;
}
- images.insert(image.res);
+ images.insert(image.id);
}
Element::Geometry(_) => {}
}
@@ -141,8 +140,8 @@ impl<'a> PdfExporter<'a> {
let y = (page.size.height - pos.y).to_pt() as f32;
match element {
- &Element::Image(Image { res, size: Size { width, height } }) => {
- let name = format!("Im{}", self.images.map(res));
+ &Element::Image(Image { id, size: Size { width, height } }) => {
+ let name = format!("Im{}", self.images.map(id));
let w = width.to_pt() as f32;
let h = height.to_pt() as f32;
@@ -208,7 +207,7 @@ impl<'a> PdfExporter<'a> {
fn write_fonts(&mut self) {
for (refs, face_id) in self.refs.fonts().zip(self.fonts.layout_indices()) {
- let face = self.env.fonts.face(face_id);
+ let face = self.env.face(face_id);
let ttf = face.ttf();
let name = ttf
@@ -237,10 +236,10 @@ impl<'a> PdfExporter<'a> {
let global_bbox = ttf.global_bounding_box();
let bbox = Rect::new(
- face.convert(global_bbox.x_min).to_pdf(),
- face.convert(global_bbox.y_min).to_pdf(),
- face.convert(global_bbox.x_max).to_pdf(),
- face.convert(global_bbox.y_max).to_pdf(),
+ face.to_em(global_bbox.x_min).to_pdf(),
+ face.to_em(global_bbox.y_min).to_pdf(),
+ face.to_em(global_bbox.x_max).to_pdf(),
+ face.to_em(global_bbox.y_max).to_pdf(),
);
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
@@ -268,7 +267,7 @@ impl<'a> PdfExporter<'a> {
let num_glyphs = ttf.number_of_glyphs();
(0 .. num_glyphs).map(|g| {
let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0);
- face.convert(x).to_pdf()
+ face.to_em(x).to_pdf()
})
});
@@ -305,7 +304,7 @@ impl<'a> PdfExporter<'a> {
.system_info(system_info);
// Write the face's bytes.
- self.writer.stream(refs.data, face.data());
+ self.writer.stream(refs.data, face.buffer());
}
}
@@ -313,7 +312,7 @@ impl<'a> PdfExporter<'a> {
let mut masks_seen = 0;
for (id, resource) in self.refs.images().zip(self.images.layout_indices()) {
- let img = self.env.resources.loaded::<ImageResource>(resource);
+ let img = self.env.resource::<ImageResource>(resource);
let (width, height) = img.buf.dimensions();
// Add the primary image.
@@ -361,7 +360,6 @@ fn write_fill(content: &mut Content, fill: Fill) {
Fill::Color(Color::Rgba(c)) => {
content.fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0);
}
- Fill::Image(_) => todo!(),
}
}
@@ -567,13 +565,13 @@ where
}
}
-/// Additional methods for [`EmLength`].
-trait EmLengthExt {
+/// Additional methods for [`Em`].
+trait EmExt {
/// Convert an em length to a number of PDF font units.
fn to_pdf(self) -> f32;
}
-impl EmLengthExt for EmLength {
+impl EmExt for Em {
fn to_pdf(self) -> f32 {
1000.0 * self.get() as f32
}