diff --git a/bench/src/bench.rs b/bench/src/bench.rs index 1b160bdcc..052ef5d63 100644 --- a/bench/src/bench.rs +++ b/bench/src/bench.rs @@ -17,7 +17,7 @@ const CASES: &[&str] = &["full/coma.typ", "text/basic.typ"]; fn benchmarks(c: &mut Criterion) { let mut loader = FsLoader::new(); - loader.search_dir(FONT_DIR); + loader.search_path(FONT_DIR); let mut env = Env::new(loader); diff --git a/src/env/fs.rs b/src/env/fs.rs index 98378722a..5f18191bb 100644 --- a/src/env/fs.rs +++ b/src/env/fs.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use memmap2::Mmap; +use serde::{Deserialize, Serialize}; use ttf_parser::{name_id, Face}; use walkdir::WalkDir; @@ -14,10 +15,11 @@ 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)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct FsLoader { faces: Vec, - paths: Vec, + files: Vec, + #[serde(skip)] cache: FileCache, } @@ -30,7 +32,7 @@ impl FsLoader { pub fn new() -> Self { Self { faces: vec![], - paths: vec![], + files: vec![], cache: HashMap::new(), } } @@ -64,19 +66,22 @@ impl FsLoader { let windir = std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string()); - self.search_dir(Path::new(&windir).join("Fonts")); + self.search_path(Path::new(&windir).join("Fonts")); if let Some(roaming) = dirs::config_dir() { - self.search_dir(roaming.join("Microsoft\\Windows\\Fonts")); + self.search_path(roaming.join("Microsoft\\Windows\\Fonts")); } if let Some(local) = dirs::cache_dir() { - self.search_dir(local.join("Microsoft\\Windows\\Fonts")); + self.search_path(local.join("Microsoft\\Windows\\Fonts")); } } - /// Search for all fonts in a directory. - pub fn search_dir(&mut self, dir: impl AsRef) { + /// Search for all fonts at a path. + /// + /// If the path is a directory, all contained fonts will be searched for + /// recursively. + pub fn search_path(&mut self, dir: impl AsRef) { let walk = WalkDir::new(dir) .follow_links(true) .sort_by(|a, b| a.file_name().cmp(b.file_name())) @@ -102,8 +107,9 @@ impl FsLoader { /// /// 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<()> { + fn search_file(&mut self, path: impl AsRef) -> io::Result<()> { let path = path.as_ref(); + let path = path.strip_prefix(".").unwrap_or(path); let file = File::open(path)?; let mmap = unsafe { Mmap::map(&file)? }; @@ -149,10 +155,15 @@ impl FsLoader { // Merge with an existing entry for the same family name. self.faces.push(FaceInfo { family, variant, index }); - self.paths.push(path.to_owned()); + self.files.push(path.to_owned()); Ok(()) } + + /// Paths to font files, parallel to [`faces()`](Self::faces). + pub fn files(&self) -> &[PathBuf] { + &self.files + } } impl Loader for FsLoader { @@ -161,7 +172,7 @@ impl Loader for FsLoader { } fn load_face(&mut self, idx: usize) -> Option { - load(&mut self.cache, &self.paths[idx]) + load(&mut self.cache, &self.files[idx]) } fn load_file(&mut self, url: &str) -> Option { @@ -169,6 +180,7 @@ impl Loader for FsLoader { } } +/// Load from the file system using a cache. fn load(cache: &mut FileCache, path: &Path) -> Option { match cache.entry(path.to_owned()) { Entry::Occupied(entry) => entry.get().clone(), @@ -186,10 +198,9 @@ mod tests { #[test] fn test_index_font_dir() { let mut loader = FsLoader::new(); - loader.search_dir("fonts"); - loader.paths.sort(); + loader.search_path("fonts"); - assert_eq!(loader.paths, &[ + assert_eq!(loader.files, &[ Path::new("fonts/EBGaramond-Bold.ttf"), Path::new("fonts/EBGaramond-BoldItalic.ttf"), Path::new("fonts/EBGaramond-Italic.ttf"), diff --git a/src/font.rs b/src/font.rs index 52912664d..5a83f6c4c 100644 --- a/src/font.rs +++ b/src/font.rs @@ -25,7 +25,7 @@ impl Face { // 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`. + // - Nobody else can move it since we have 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] = @@ -151,19 +151,20 @@ impl Em { } /// Properties of a single font face. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 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. + #[serde(flatten)] 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)] +#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub struct FontVariant { /// The style of the face (normal / italic / oblique). pub style: FontStyle, @@ -183,6 +184,7 @@ impl FontVariant { /// The style of a font face. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum FontStyle { /// The default style. Normal, @@ -347,6 +349,7 @@ impl Debug for FontWeight { /// The width of a font face. #[derive(Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] +#[serde(transparent)] pub struct FontStretch(f32); impl FontStretch { diff --git a/src/main.rs b/src/main.rs index 058276813..fa98d4c51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ fn main() -> anyhow::Result<()> { let src = fs::read_to_string(src_path).context("Failed to read from source file.")?; let mut loader = FsLoader::new(); - loader.search_dir("fonts"); + loader.search_path("fonts"); loader.search_system(); let mut env = Env::new(loader); diff --git a/tests/typeset.rs b/tests/typeset.rs index afd0d0555..5e48494d8 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -63,7 +63,7 @@ fn main() { } let mut loader = FsLoader::new(); - loader.search_dir(FONT_DIR); + loader.search_path(FONT_DIR); let mut env = Env::new(loader); @@ -122,6 +122,12 @@ impl Args { } } +struct Panic { + pos: Pos, + lhs: Option, + rhs: Option, +} + fn test( env: &mut Env, src_path: &Path, @@ -302,12 +308,6 @@ fn parse_metadata(src: &str, map: &LineMap) -> (Option, DiagSet) { (compare_ref, diags) } -struct Panic { - pos: Pos, - lhs: Option, - rhs: Option, -} - fn register_helpers(scope: &mut Scope, panics: Rc>>) { pub fn args(_: &mut EvalContext, args: &mut FuncArgs) -> Value { let repr = pretty(args); @@ -344,7 +344,7 @@ fn print_diag(diag: &Diag, map: &LineMap, lines: u32) { println!("{}: {}-{}: {}", diag.level, start, end, diag.message); } -fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap { +fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap { let pad = Length::pt(5.0); let height = pad + frames.iter().map(|l| l.size.height + pad).sum::(); @@ -355,8 +355,8 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap { .max_by(|a, b| a.partial_cmp(&b).unwrap()) .unwrap_or_default(); - let pixel_width = (pixel_per_pt * width.to_pt() as f32) as u32; - let pixel_height = (pixel_per_pt * height.to_pt() as f32) as u32; + let pixel_width = (dpi * width.to_pt() as f32) as u32; + let pixel_height = (dpi * height.to_pt() as f32) as u32; if pixel_width > 4000 || pixel_height > 4000 { panic!( "overlarge image: {} by {} ({} x {})", @@ -365,7 +365,7 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap { } let mut canvas = Pixmap::new(pixel_width, pixel_height).unwrap(); - let ts = Transform::from_scale(pixel_per_pt, pixel_per_pt); + let ts = Transform::from_scale(dpi, dpi); canvas.fill(Color::BLACK); let mut origin = Point::new(pad, pad); @@ -391,15 +391,9 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap { let y = pos.y.to_pt() as f32; let ts = ts.pre_translate(x, y); match element { - Element::Text(shaped) => { - draw_text(&mut canvas, env, ts, shaped); - } - Element::Image(image) => { - draw_image(&mut canvas, env, ts, image); - } - Element::Geometry(geom) => { - draw_geometry(&mut canvas, ts, geom); - } + Element::Text(shaped) => draw_text(&mut canvas, env, ts, shaped), + Element::Image(image) => draw_image(&mut canvas, env, ts, image), + Element::Geometry(geom) => draw_geometry(&mut canvas, ts, geom), } } @@ -517,32 +511,41 @@ fn convert_typst_fill(fill: Fill) -> Paint<'static> { } fn convert_typst_path(path: &geom::Path) -> tiny_skia::Path { - let f = |length: Length| length.to_pt() as f32; let mut builder = tiny_skia::PathBuilder::new(); + let f = |v: Length| v.to_pt() as f32; for elem in &path.0 { match elem { - geom::PathElement::MoveTo(p) => builder.move_to(f(p.x), f(p.y)), - geom::PathElement::LineTo(p) => builder.line_to(f(p.x), f(p.y)), - geom::PathElement::CubicTo(p1, p2, p3) => { - builder.cubic_to(f(p1.x), f(p1.y), f(p2.x), f(p2.y), f(p3.x), f(p3.y)) + geom::PathElement::MoveTo(p) => { + builder.move_to(f(p.x), f(p.y)); + } + geom::PathElement::LineTo(p) => { + builder.line_to(f(p.x), f(p.y)); + } + geom::PathElement::CubicTo(p1, p2, p3) => { + builder.cubic_to(f(p1.x), f(p1.y), f(p2.x), f(p2.y), f(p3.x), f(p3.y)); + } + geom::PathElement::ClosePath => { + builder.close(); } - geom::PathElement::ClosePath => builder.close(), }; } builder.finish().unwrap() } +fn convert_usvg_transform(transform: usvg::Transform) -> Transform { + let g = |v: f64| v as f32; + let usvg::Transform { a, b, c, d, e, f } = transform; + Transform::from_row(g(a), g(b), g(c), g(d), g(e), g(f)) +} + fn convert_usvg_fill(fill: &usvg::Fill) -> (Paint<'static>, FillRule) { let mut paint = Paint::default(); paint.anti_alias = true; match fill.paint { - usvg::Paint::Color(color) => paint.set_color_rgba8( - color.red, - color.green, - color.blue, - fill.opacity.to_u8(), - ), + usvg::Paint::Color(usvg::Color { red, green, blue }) => { + paint.set_color_rgba8(red, green, blue, fill.opacity.to_u8()) + } usvg::Paint::Link(_) => {} } @@ -555,29 +558,27 @@ fn convert_usvg_fill(fill: &usvg::Fill) -> (Paint<'static>, FillRule) { } fn convert_usvg_path(path: &usvg::PathData) -> tiny_skia::Path { - let f = |v: f64| v as f32; let mut builder = tiny_skia::PathBuilder::new(); + let f = |v: f64| v as f32; for seg in path.iter() { match *seg { - usvg::PathSegment::MoveTo { x, y } => builder.move_to(f(x), f(y)), + usvg::PathSegment::MoveTo { x, y } => { + builder.move_to(f(x), f(y)); + } usvg::PathSegment::LineTo { x, y } => { builder.line_to(f(x), f(y)); } usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => { - builder.cubic_to(f(x1), f(y1), f(x2), f(y2), f(x), f(y)) + builder.cubic_to(f(x1), f(y1), f(x2), f(y2), f(x), f(y)); + } + usvg::PathSegment::ClosePath => { + builder.close(); } - usvg::PathSegment::ClosePath => builder.close(), } } builder.finish().unwrap() } -fn convert_usvg_transform(transform: usvg::Transform) -> Transform { - let g = |v: f64| v as f32; - let usvg::Transform { a, b, c, d, e, f } = transform; - Transform::from_row(g(a), g(b), g(c), g(d), g(e), g(f)) -} - struct WrappedPathBuilder(tiny_skia::PathBuilder); impl OutlineBuilder for WrappedPathBuilder {