diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8afaa61ce..43fec7d6b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,13 +22,6 @@ jobs: with: path: typst - - name: Checkout fontdock - uses: actions/checkout@v2 - with: - repository: typst/fontdock - token: ${{ secrets.TYPSTC_ACTION_TOKEN }} - path: fontdock - - name: Checkout pdf-writer uses: actions/checkout@v2 with: diff --git a/Cargo.toml b/Cargo.toml index 38be3d795..6c77b29ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,9 @@ authors = ["The Typst Project Developers"] edition = "2018" [features] -default = ["fs"] -fs = ["fontdock/fs"] -cli = ["fs", "anyhow"] +default = ["cli", "fs"] +cli = ["anyhow", "fs"] +fs = ["dirs", "memmap2", "walkdir"] [workspace] members = ["bench"] @@ -21,17 +21,19 @@ debug = 0 opt-level = 2 [dependencies] -fontdock = { path = "../fontdock", features = ["serde"], default-features = false } image = { version = "0.23", default-features = false, features = ["jpeg", "png"] } miniz_oxide = "0.3" pdf-writer = { path = "../pdf-writer" } rustybuzz = { git = "https://github.com/laurmaedje/rustybuzz" } +serde = { version = "1", features = ["derive"] } ttf-parser = "0.12" unicode-bidi = "0.3.5" unicode-xid = "0.2" xi-unicode = "0.3" anyhow = { version = "1", optional = true } -serde = { version = "1", features = ["derive"] } +dirs = { version = "3", optional = true } +memmap2 = { version = "0.2", optional = true } +walkdir = { version = "2", optional = true } [dev-dependencies] walkdir = "2" diff --git a/bench/Cargo.toml b/bench/Cargo.toml index 36f40b8c9..4965c3ea4 100644 --- a/bench/Cargo.toml +++ b/bench/Cargo.toml @@ -7,7 +7,6 @@ publish = false [dev-dependencies] criterion = "0.3" -fontdock = { path = "../../fontdock" } typst = { path = ".." } [[bench]] diff --git a/bench/src/bench.rs b/bench/src/bench.rs index aac91dd4d..1b160bdcc 100644 --- a/bench/src/bench.rs +++ b/bench/src/bench.rs @@ -1,9 +1,8 @@ use std::path::Path; use criterion::{criterion_group, criterion_main, Criterion}; -use fontdock::FsIndex; -use typst::env::{Env, FsIndexExt, ResourceLoader}; +use typst::env::{Env, FsLoader}; use typst::eval::eval; use typst::exec::{exec, State}; use typst::layout::layout; @@ -17,13 +16,10 @@ const TYP_DIR: &str = "../tests/typ"; const CASES: &[&str] = &["full/coma.typ", "text/basic.typ"]; fn benchmarks(c: &mut Criterion) { - let mut index = FsIndex::new(); - index.search_dir(FONT_DIR); + let mut loader = FsLoader::new(); + loader.search_dir(FONT_DIR); - 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/env.rs b/src/env.rs deleted file mode 100644 index 3b8fd4cdd..000000000 --- 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 { - 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>>; - -/// 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, - entries: Vec>, -} - -/// 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(&mut self, path: P, parse: F) -> Option<(ResourceId, &R)> - where - P: AsRef, - F: FnOnce(Vec) -> Option, - 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(&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) -> Option { - 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 000000000..98378722a --- /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, + paths: Vec, + 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![], + 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) { + 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) -> 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 { + 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 { + load(&mut self.cache, &self.paths[idx]) + } + + fn load_file(&mut self, url: &str) -> Option { + load(&mut self.cache, Path::new(url)) + } +} + +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_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 000000000..4bdb54837 --- /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 { + 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 000000000..af3872dd7 --- /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, + /// Loaded resources indexed by [`ResourceId`]. + resources: Vec>, + /// Maps from URL to loaded resource. + urls: HashMap, + /// 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>, +} + +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), + 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 { + 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 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(&mut self, url: &str, parse: F) -> Option + where + F: FnOnce(Buffer) -> Option, + 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(&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; + + /// Load a file from a URL. + fn load_file(&mut self, url: &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); +} + +/// 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 82f653e92..f4bc6b7be 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 1ac8fea37..52912664d 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 { + // 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) -> EmLength { - EmLength(units.into() / self.units_per_em) - } -} - -impl FaceFromVec for FaceBuf { - fn from_vec(vec: Vec, index: u32) -> Option { - 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) -> 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, 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 { + 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 { + 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 { + 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 24ba65ceb..9890e33f1 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 47d19a62c..e6e28a4ad 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, ) -> 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, 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 6afc617b0..cf329567d 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 09b563360..134590bb0 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::(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 738348ee0..5018f0b44 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 809900352..058276813 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 365275444..82acbbaa0 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::(image.res); + let img = env.resource::(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::(resource); + let img = self.env.resource::(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 } diff --git a/tests/typeset.rs b/tests/typeset.rs index 7aaa017d2..afd0d0555 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -5,7 +5,6 @@ use std::fs; use std::path::Path; use std::rc::Rc; -use fontdock::FsIndex; use image::{GenericImageView, Rgba}; use tiny_skia::{ Color, ColorU8, FillRule, FilterQuality, Paint, Pattern, Pixmap, Rect, SpreadMode, @@ -16,7 +15,7 @@ use walkdir::WalkDir; use typst::color; use typst::diag::{Diag, DiagSet, Level, Pass}; -use typst::env::{Env, FsIndexExt, ImageResource, ResourceLoader}; +use typst::env::{Env, FsLoader, ImageResource}; use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value}; use typst::exec::State; use typst::geom::{self, Length, Point, Sides, Size}; @@ -63,13 +62,10 @@ fn main() { println!("Running {} tests", len); } - let mut index = FsIndex::new(); - index.search_dir(FONT_DIR); + let mut loader = FsLoader::new(); + loader.search_dir(FONT_DIR); - let mut env = Env { - fonts: index.into_dynamic_loader(), - resources: ResourceLoader::new(), - }; + let mut env = Env::new(loader); let mut ok = true; for src_path in filtered { @@ -414,7 +410,7 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap { } fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) { - let ttf = env.fonts.face(shaped.face_id).ttf(); + let ttf = env.face(shaped.face_id).ttf(); let mut x = 0.0; for glyph in &shaped.glyphs { @@ -484,7 +480,7 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, element: &Geometry) { } fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) { - let img = &env.resources.loaded::(element.res); + let img = &env.resource::(element.id); let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap(); for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) { @@ -513,10 +509,9 @@ fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) { fn convert_typst_fill(fill: Fill) -> Paint<'static> { let mut paint = Paint::default(); match fill { - Fill::Color(c) => match c { - color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a), - }, - Fill::Image(_) => todo!(), + Fill::Color(color::Color::Rgba(c)) => { + paint.set_color_rgba8(c.r, c.g, c.b, c.a); + } } paint }