From 0bfee5b7772338fd39bbf708d3e31ea7bcec859b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 28 May 2021 12:44:44 +0200 Subject: [PATCH] Refactored loading and cache architecture --- Cargo.toml | 2 +- bench/src/bench.rs | 50 +++--- src/cache.rs | 43 +++--- src/env/image.rs | 47 ------ src/env/mod.rs | 243 ------------------------------ src/eval/mod.rs | 55 ++++--- src/eval/value.rs | 14 +- src/exec/context.rs | 10 +- src/exec/mod.rs | 19 +-- src/export/mod.rs | 5 + src/{pdf/mod.rs => export/pdf.rs} | 32 ++-- src/font.rs | 126 +++++++++++++++- src/image.rs | 127 ++++++++++++++++ src/layout/frame.rs | 3 +- src/layout/mod.rs | 47 ++++-- src/layout/shaping.rs | 25 +-- src/lib.rs | 31 ++-- src/library/image.rs | 25 +-- src/{env => loading}/fs.rs | 0 src/loading/mod.rs | 43 ++++++ src/main.rs | 52 +++---- src/pretty.rs | 8 - tests/typeset.rs | 66 ++++---- 23 files changed, 558 insertions(+), 515 deletions(-) delete mode 100644 src/env/image.rs delete mode 100644 src/env/mod.rs create mode 100644 src/export/mod.rs rename src/{pdf/mod.rs => export/pdf.rs} (96%) create mode 100644 src/image.rs rename src/{env => loading}/fs.rs (100%) create mode 100644 src/loading/mod.rs diff --git a/Cargo.toml b/Cargo.toml index ed46ccd92..fa3668a27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ opt-level = 2 [dependencies] decorum = { version = "0.3.1", default-features = false, features = ["serialize-serde"] } fxhash = "0.2.1" -image = { version = "0.23", default-features = false, features = ["jpeg", "png"] } +image = { version = "0.23", default-features = false, features = ["png", "jpeg"] } miniz_oxide = "0.3" pdf-writer = { path = "../pdf-writer" } rustybuzz = { git = "https://github.com/laurmaedje/rustybuzz" } diff --git a/bench/src/bench.rs b/bench/src/bench.rs index f6ea9398d..8563f1154 100644 --- a/bench/src/bench.rs +++ b/bench/src/bench.rs @@ -1,15 +1,14 @@ use std::path::Path; +use std::rc::Rc; use criterion::{criterion_group, criterion_main, Criterion}; -use typst::cache::Cache; -use typst::env::{Env, FsLoader}; use typst::eval::eval; -use typst::exec::{exec, State}; +use typst::exec::exec; +use typst::export::pdf; use typst::layout::layout; -use typst::library; +use typst::loading::FsLoader; use typst::parse::parse; -use typst::pdf; use typst::typeset; const FONT_DIR: &str = "../fonts"; @@ -20,38 +19,39 @@ fn benchmarks(c: &mut Criterion) { let mut loader = FsLoader::new(); loader.search_path(FONT_DIR); - let mut env = Env::new(loader); - - let scope = library::new(); - let state = State::default(); + let mut cache = typst::cache::Cache::new(&loader); + let scope = typst::library::new(); + let state = typst::exec::State::default(); for case in CASES { let case = Path::new(case); let name = case.file_stem().unwrap().to_string_lossy(); - let src = std::fs::read_to_string(Path::new(TYP_DIR).join(case)).unwrap(); macro_rules! bench { - ($step:literal: $($tts:tt)*) => { - c.bench_function( - &format!("{}-{}", $step, name), - |b| b.iter(|| $($tts)*) - ); + ($step:literal: $code:expr) => { + c.bench_function(&format!("{}-{}", $step, name), |b| { + b.iter(|| { + cache.layout.clear(); + $code + }); + }); }; } - // Prepare intermediate results and run warm. - let syntax_tree = parse(&src).output; - let expr_map = eval(&mut env, &syntax_tree, &scope).output; - let layout_tree = exec(&mut env, &syntax_tree, &expr_map, state.clone()).output; - let frames = layout(&mut env, &mut Cache::new(), &layout_tree); + // Prepare intermediate results, run warm and fill caches. + let src = std::fs::read_to_string(Path::new(TYP_DIR).join(case)).unwrap(); + let parsed = Rc::new(parse(&src).output); + let evaluated = eval(&mut loader, &mut cache, parsed.clone(), &scope).output; + let executed = exec(&evaluated.template, state.clone()).output; + let layouted = layout(&mut loader, &mut cache, &executed); // Bench! bench!("parse": parse(&src)); - bench!("eval": eval(&mut env, &syntax_tree, &scope)); - bench!("exec": exec(&mut env, &syntax_tree, &expr_map, state.clone())); - bench!("layout": layout(&mut env, &mut Cache::new(), &layout_tree)); - bench!("typeset": typeset(&mut env, &mut Cache::new(), &src, &scope, state.clone())); - bench!("pdf": pdf::export(&env, &frames)); + bench!("eval": eval(&mut loader, &mut cache, parsed.clone(), &scope)); + bench!("exec": exec(&evaluated.template, state.clone())); + bench!("layout": layout(&mut loader, &mut cache, &executed)); + bench!("typeset": typeset(&mut loader, &mut cache, &src, &scope, state.clone())); + bench!("pdf": pdf(&cache, &layouted)); } } diff --git a/src/cache.rs b/src/cache.rs index 4cf97ba6f..aa9c10a01 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,34 +1,27 @@ -//! Caching for incremental compilation. +//! Caching of compilation artifacts. -use std::collections::HashMap; +use crate::font::FontCache; +use crate::image::ImageCache; +use crate::layout::LayoutCache; +use crate::loading::Loader; -use crate::layout::{Frame, Regions}; - -/// A cache for incremental compilation. -#[derive(Default, Debug, Clone)] +/// Caches compilation artifacts. pub struct Cache { - /// A map that holds the layouted nodes from past compilations. - pub frames: HashMap, + /// Caches parsed font faces. + pub font: FontCache, + /// Caches decoded images. + pub image: ImageCache, + /// Caches layouting artifacts. + pub layout: LayoutCache, } impl Cache { /// Create a new, empty cache. - pub fn new() -> Self { - Self::default() - } - - /// Clear the cache. - pub fn clear(&mut self) { - self.frames.clear(); + pub fn new(loader: &dyn Loader) -> Self { + Self { + font: FontCache::new(loader), + image: ImageCache::new(), + layout: LayoutCache::new(), + } } } - -/// Frames from past compilations and checks for their validity in future -/// compilations. -#[derive(Debug, Clone)] -pub struct FramesEntry { - /// The regions in which these frames are valid. - pub regions: Regions, - /// Cached frames for a node. - pub frames: Vec, -} diff --git a/src/env/image.rs b/src/env/image.rs deleted file mode 100644 index 365ff312c..000000000 --- a/src/env/image.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; -use std::io::Cursor; - -use image::io::Reader as ImageReader; -use image::{DynamicImage, GenericImageView, ImageFormat}; - -/// A loaded image. -pub struct Image { - /// The original format the image was encoded in. - pub format: ImageFormat, - /// The decoded image. - pub buf: DynamicImage, -} - -impl Image { - /// Parse an image from raw data in a supported format. - /// - /// The image format is determined automatically. - pub fn parse(data: &[u8]) -> Option { - let cursor = Cursor::new(data); - let reader = ImageReader::new(cursor).with_guessed_format().ok()?; - let format = reader.format()?; - let buf = reader.decode().ok()?; - Some(Self { format, buf }) - } - - /// The width of the image. - pub fn width(&self) -> u32 { - self.buf.width() - } - - /// The height of the image. - pub fn height(&self) -> u32 { - self.buf.height() - } -} - -impl Debug for Image { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Image") - .field("format", &self.format) - .field("color", &self.buf.color()) - .field("width", &self.width()) - .field("height", &self.height()) - .finish() - } -} diff --git a/src/env/mod.rs b/src/env/mod.rs deleted file mode 100644 index 84be3e813..000000000 --- a/src/env/mod.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Font and image loading. - -#[cfg(feature = "fs")] -mod fs; -mod image; - -pub use self::image::*; -#[cfg(feature = "fs")] -pub use fs::*; - -use std::collections::{hash_map::Entry, HashMap}; -use std::rc::Rc; - -use serde::{Deserialize, Serialize}; - -use crate::font::{Face, FaceInfo, FontVariant}; - -/// Handles font and image loading. -pub struct Env { - /// The loader that serves the font face and file buffers. - loader: Box, - /// 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>, - /// Loaded images indexed by [`ImageId`]. - images: Vec, - /// Maps from paths to loaded images. - paths: HashMap, - /// Callback for loaded font faces. - on_face_load: Option>, - /// Callback for loaded images. - on_image_load: Option>, -} - -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), - faces, - families, - images: vec![], - paths: HashMap::new(), - on_face_load: None, - on_image_load: None, - } - } - - /// 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 id = best?; - let idx = id.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)?; - if let Some(callback) = &self.on_face_load { - callback(id, &face); - } - *slot = Some(face); - } - - best - } - - /// Get a reference to a queried face. - /// - /// 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") - } - - /// Register a callback which is invoked when a font face was loaded. - pub fn on_face_load(&mut self, f: F) - where - F: Fn(FaceId, &Face) + 'static, - { - self.on_face_load = Some(Box::new(f)); - } - - /// Load and decode an image file from a path. - pub fn load_image(&mut self, path: &str) -> Option { - Some(match self.paths.entry(path.to_string()) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - let buffer = self.loader.load_file(path)?; - let image = Image::parse(&buffer)?; - let id = ImageId(self.images.len() as u32); - if let Some(callback) = &self.on_image_load { - callback(id, &image); - } - self.images.push(image); - *entry.insert(id) - } - }) - } - - /// Get a reference to a loaded image. - /// - /// This panics if no image with this id was loaded. This function should - /// only be called with ids returned by [`load_image()`](Self::load_image). - #[track_caller] - pub fn image(&self, id: ImageId) -> &Image { - &self.images[id.0 as usize] - } - - /// Register a callback which is invoked when an image was loaded. - pub fn on_image_load(&mut self, f: F) - where - F: Fn(ImageId, &Image) + 'static, - { - self.on_image_load = Some(Box::new(f)); - } -} - -/// Loads fonts and images 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 path. - fn load_file(&mut self, path: &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); - - /// Create a face id from the raw underlying value. - /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub fn from_raw(v: u32) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub fn into_raw(self) -> u32 { - self.0 - } -} - -/// A unique identifier for a loaded image. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct ImageId(u32); - -impl ImageId { - /// Create an image id from the raw underlying value. - /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub fn from_raw(v: u32) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub fn into_raw(self) -> u32 { - self.0 - } -} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index da7fca536..d841dbae5 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -10,38 +10,50 @@ pub use capture::*; pub use scope::*; pub use value::*; -use std::collections::HashMap; use std::rc::Rc; +use crate::cache::Cache; use crate::color::Color; use crate::diag::{Diag, DiagSet, Pass}; -use crate::env::Env; use crate::geom::{Angle, Length, Relative}; +use crate::loading::Loader; use crate::syntax::visit::Visit; use crate::syntax::*; -/// Evaluate all nodes in a syntax tree. +/// Evaluated a parsed source file into a module. /// /// The `scope` consists of the base definitions that are present from the /// beginning (typically, the standard library). -pub fn eval(env: &mut Env, tree: &Tree, scope: &Scope) -> Pass { - let mut ctx = EvalContext::new(env, scope); +pub fn eval( + loader: &mut dyn Loader, + cache: &mut Cache, + tree: Rc, + base: &Scope, +) -> Pass { + let mut ctx = EvalContext::new(loader, cache, base); let map = tree.eval(&mut ctx); - Pass::new(map, ctx.diags) + let module = Module { + scope: ctx.scopes.top, + template: vec![TemplateNode::Tree { tree, map }], + }; + Pass::new(module, ctx.diags) } -/// A map from nodes to the values they evaluated to. -/// -/// The raw pointers point into the nodes contained in some [`Tree`]. Since the -/// lifetime is erased, the tree could go out of scope while the hash map still -/// lives. Although this could lead to lookup panics, it is not unsafe since the -/// pointers are never dereferenced. -pub type NodeMap = HashMap<*const Node, Value>; +/// An evaluated module, ready for importing or execution. +#[derive(Debug, Clone, PartialEq)] +pub struct Module { + /// The top-level definitions that were bound in this module. + pub scope: Scope, + /// The template defined by this module. + pub template: TemplateValue, +} /// The context for evaluation. pub struct EvalContext<'a> { - /// The environment from which resources are gathered. - pub env: &'a mut Env, + /// The loader from which resources (files and images) are loaded. + pub loader: &'a mut dyn Loader, + /// A cache for loaded resources. + pub cache: &'a mut Cache, /// The active scopes. pub scopes: Scopes<'a>, /// Evaluation diagnostics. @@ -49,11 +61,16 @@ pub struct EvalContext<'a> { } impl<'a> EvalContext<'a> { - /// Create a new execution context with a base scope. - pub fn new(env: &'a mut Env, scope: &'a Scope) -> Self { + /// Create a new evaluation context with a base scope. + pub fn new( + loader: &'a mut dyn Loader, + cache: &'a mut Cache, + base: &'a Scope, + ) -> Self { Self { - env, - scopes: Scopes::with_base(scope), + loader, + cache, + scopes: Scopes::with_base(base), diags: DiagSet::new(), } } diff --git a/src/eval/value.rs b/src/eval/value.rs index 0d87c28f6..d10d734af 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -1,15 +1,15 @@ use std::any::Any; use std::cmp::Ordering; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::Deref; use std::rc::Rc; -use super::{EvalContext, NodeMap}; +use super::EvalContext; use crate::color::{Color, RgbaColor}; use crate::exec::ExecContext; use crate::geom::{Angle, Length, Linear, Relative}; -use crate::syntax::{Span, Spanned, Tree}; +use crate::syntax::{Node, Span, Spanned, Tree}; /// A computational value. #[derive(Debug, Clone, PartialEq)] @@ -163,6 +163,14 @@ impl PartialEq for TemplateNode { } } +/// A map from nodes to the values they evaluated to. +/// +/// The raw pointers point into the nodes contained in some [`Tree`]. Since the +/// lifetime is erased, the tree could go out of scope while the hash map still +/// lives. Although this could lead to lookup panics, it is not unsafe since the +/// pointers are never dereferenced. +pub type NodeMap = HashMap<*const Node, Value>; + /// A reference-counted dynamic template node that can implement custom /// behaviour. #[derive(Clone)] diff --git a/src/exec/context.rs b/src/exec/context.rs index 93ffaf966..016b092a9 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -2,7 +2,6 @@ use std::mem; use super::{Exec, FontFamily, State}; use crate::diag::{Diag, DiagSet, Pass}; -use crate::env::Env; use crate::eval::TemplateValue; use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size}; use crate::layout::{ @@ -11,9 +10,7 @@ use crate::layout::{ use crate::syntax::Span; /// The context for execution. -pub struct ExecContext<'a> { - /// The environment from which resources are gathered. - pub env: &'a mut Env, +pub struct ExecContext { /// The active execution state. pub state: State, /// Execution diagnostics. @@ -27,11 +24,10 @@ pub struct ExecContext<'a> { stack: StackBuilder, } -impl<'a> ExecContext<'a> { +impl ExecContext { /// Create a new execution context with a base state. - pub fn new(env: &'a mut Env, state: State) -> Self { + pub fn new(state: State) -> Self { Self { - env, diags: DiagSet::new(), tree: Tree { runs: vec![] }, page: Some(PageBuilder::new(&state, true)), diff --git a/src/exec/mod.rs b/src/exec/mod.rs index b6765d1e6..643d5b44a 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -9,29 +9,18 @@ pub use state::*; use std::rc::Rc; use crate::diag::Pass; -use crate::env::Env; use crate::eval::{NodeMap, TemplateFunc, TemplateNode, TemplateValue, Value}; use crate::layout; use crate::pretty::pretty; use crate::syntax::*; -/// Execute a syntax tree to produce a layout tree. -/// -/// The `map` shall be a node map computed for this tree with -/// [`eval`](crate::eval::eval). Note that `tree` must be the _exact_ same tree -/// as used for evaluation (no cloned version), because the node map depends on -/// the pointers being stable. +/// Execute a template to produce a layout tree. /// /// The `state` is the base state that may be updated over the course of /// execution. -pub fn exec( - env: &mut Env, - tree: &Tree, - map: &NodeMap, - state: State, -) -> Pass { - let mut ctx = ExecContext::new(env, state); - tree.exec_with_map(&mut ctx, &map); +pub fn exec(template: &TemplateValue, state: State) -> Pass { + let mut ctx = ExecContext::new(state); + template.exec(&mut ctx); ctx.finish() } diff --git a/src/export/mod.rs b/src/export/mod.rs new file mode 100644 index 000000000..5ed0abf96 --- /dev/null +++ b/src/export/mod.rs @@ -0,0 +1,5 @@ +//! Exporting into external formats. + +mod pdf; + +pub use pdf::*; diff --git a/src/pdf/mod.rs b/src/export/pdf.rs similarity index 96% rename from src/pdf/mod.rs rename to src/export/pdf.rs index 9b353fce0..0ca4df388 100644 --- a/src/pdf/mod.rs +++ b/src/export/pdf.rs @@ -12,34 +12,35 @@ use pdf_writer::{ }; use ttf_parser::{name_id, GlyphId}; +use crate::cache::Cache; use crate::color::Color; -use crate::env::{Env, FaceId, Image, ImageId}; -use crate::font::{Em, VerticalFontMetric}; +use crate::font::{Em, FaceId, VerticalFontMetric}; use crate::geom::{self, Length, Size}; +use crate::image::{Image, ImageId}; use crate::layout::{Element, Fill, Frame, Shape}; /// Export a collection of frames into a PDF document. /// /// This creates one page per frame. In addition to the frames, you need to pass -/// in the environment used for typesetting such that things like fonts and -/// images can be included in the PDF. +/// in the cache used during compilation such that things like fonts and images +/// can be included in the PDF. /// /// Returns the raw bytes making up the PDF document. -pub fn export(env: &Env, frames: &[Frame]) -> Vec { - PdfExporter::new(env, frames).write() +pub fn pdf(cache: &Cache, frames: &[Frame]) -> Vec { + PdfExporter::new(cache, frames).write() } struct PdfExporter<'a> { writer: PdfWriter, frames: &'a [Frame], - env: &'a Env, + cache: &'a Cache, refs: Refs, fonts: Remapper, images: Remapper, } impl<'a> PdfExporter<'a> { - fn new(env: &'a Env, frames: &'a [Frame]) -> Self { + fn new(cache: &'a Cache, frames: &'a [Frame]) -> Self { let mut writer = PdfWriter::new(1, 7); writer.set_indent(2); @@ -53,7 +54,7 @@ impl<'a> PdfExporter<'a> { Element::Text(ref shaped) => fonts.insert(shaped.face_id), Element::Geometry(_, _) => {} Element::Image(id, _) => { - let img = env.image(id); + let img = cache.image.get(id); if img.buf.color().has_alpha() { alpha_masks += 1; } @@ -65,7 +66,14 @@ impl<'a> PdfExporter<'a> { let refs = Refs::new(frames.len(), fonts.len(), images.len(), alpha_masks); - Self { writer, frames, env, refs, fonts, images } + Self { + writer, + frames, + cache, + refs, + fonts, + images, + } } fn write(mut self) -> Vec { @@ -207,7 +215,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.face(face_id); + let face = self.cache.font.get(face_id); let ttf = face.ttf(); let name = ttf @@ -312,7 +320,7 @@ impl<'a> PdfExporter<'a> { let mut masks_seen = 0; for (id, image_id) in self.refs.images().zip(self.images.layout_indices()) { - let img = self.env.image(image_id); + let img = self.cache.image.get(image_id); let (width, height) = img.buf.dimensions(); // Add the primary image. diff --git a/src/font.rs b/src/font.rs index dd81fa88e..69a309006 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,11 +1,13 @@ //! Font handling. +use std::collections::HashMap; use std::fmt::{self, Debug, Display, Formatter}; use serde::{Deserialize, Serialize}; -use crate::env::Buffer; use crate::geom::Length; +use crate::loading::Buffer; +use crate::loading::Loader; /// A font face. pub struct Face { @@ -155,6 +157,128 @@ impl Em { } } +/// Caches parsed font faces. +pub struct FontCache { + faces: Vec>, + families: HashMap>, + on_load: Option>, +} + +impl FontCache { + /// Create a new, empty font cache. + pub fn new(loader: &dyn Loader) -> Self { + let mut faces = vec![]; + let mut families = HashMap::>::new(); + + for (i, info) in loader.faces().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 { faces, families, on_load: None } + } + + /// Query for and load the font face from the given `family` that most + /// closely matches the given `variant`. + pub fn select( + &mut self, + loader: &mut dyn Loader, + family: &str, + variant: FontVariant, + ) -> Option { + // Check whether a family with this name exists. + let ids = self.families.get(family)?; + let infos = 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 id = best?; + let idx = id.0 as usize; + let slot = &mut self.faces[idx]; + if slot.is_none() { + let index = infos[idx].index; + let buffer = loader.load_face(idx)?; + let face = Face::new(buffer, index)?; + if let Some(callback) = &self.on_load { + callback(id, &face); + } + *slot = Some(face); + } + + best + } + + /// Get a reference to a loaded face. + /// + /// This panics if no face with this id was loaded. This function should + /// only be called with ids returned by [`select()`](Self::select). + #[track_caller] + pub fn get(&self, id: FaceId) -> &Face { + self.faces[id.0 as usize].as_ref().expect("font face was not loaded") + } + + /// Register a callback which is invoked each time a font face is loaded. + pub fn on_load(&mut self, f: F) + where + F: Fn(FaceId, &Face) + 'static, + { + self.on_load = Some(Box::new(f)); + } +} + +/// 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); + + /// Create a face id from the raw underlying value. + /// + /// This should only be called with values returned by + /// [`into_raw`](Self::into_raw). + pub fn from_raw(v: u32) -> Self { + Self(v) + } + + /// Convert into the raw underlying value. + pub fn into_raw(self) -> u32 { + self.0 + } +} + /// Properties of a single font face. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FaceInfo { diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 000000000..bdfc19a62 --- /dev/null +++ b/src/image.rs @@ -0,0 +1,127 @@ +//! Image handling. + +use std::collections::{hash_map::Entry, HashMap}; +use std::fmt::{self, Debug, Formatter}; +use std::io::Cursor; + +use image::io::Reader as ImageReader; +use image::{DynamicImage, GenericImageView, ImageFormat}; +use serde::{Deserialize, Serialize}; + +use crate::loading::Loader; + +/// A loaded image. +pub struct Image { + /// The original format the image was encoded in. + pub format: ImageFormat, + /// The decoded image. + pub buf: DynamicImage, +} + +impl Image { + /// Parse an image from raw data in a supported format (PNG or JPEG). + /// + /// The image format is determined automatically. + pub fn parse(data: &[u8]) -> Option { + let cursor = Cursor::new(data); + let reader = ImageReader::new(cursor).with_guessed_format().ok()?; + let format = reader.format()?; + let buf = reader.decode().ok()?; + Some(Self { format, buf }) + } + + /// The width of the image. + pub fn width(&self) -> u32 { + self.buf.width() + } + + /// The height of the image. + pub fn height(&self) -> u32 { + self.buf.height() + } +} + +impl Debug for Image { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("Image") + .field("format", &self.format) + .field("color", &self.buf.color()) + .field("width", &self.width()) + .field("height", &self.height()) + .finish() + } +} + +/// Caches decoded images. +pub struct ImageCache { + /// Loaded images indexed by [`ImageId`]. + images: Vec, + /// Maps from paths to loaded images. + paths: HashMap, + /// Callback for loaded images. + on_load: Option>, +} + +impl ImageCache { + /// Create a new, empty image cache. + pub fn new() -> Self { + Self { + images: vec![], + paths: HashMap::new(), + on_load: None, + } + } + + /// Load and decode an image file from a path. + pub fn load(&mut self, loader: &mut dyn Loader, path: &str) -> Option { + Some(match self.paths.entry(path.to_string()) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let buffer = loader.load_file(path)?; + let image = Image::parse(&buffer)?; + let id = ImageId(self.images.len() as u32); + if let Some(callback) = &self.on_load { + callback(id, &image); + } + self.images.push(image); + *entry.insert(id) + } + }) + } + + /// Get a reference to a loaded image. + /// + /// This panics if no image with this id was loaded. This function should + /// only be called with ids returned by [`load()`](Self::load). + #[track_caller] + pub fn get(&self, id: ImageId) -> &Image { + &self.images[id.0 as usize] + } + + /// Register a callback which is invoked each time an image is loaded. + pub fn on_load(&mut self, f: F) + where + F: Fn(ImageId, &Image) + 'static, + { + self.on_load = Some(Box::new(f)); + } +} + +/// A unique identifier for a loaded image. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct ImageId(u32); + +impl ImageId { + /// Create an image id from the raw underlying value. + /// + /// This should only be called with values returned by + /// [`into_raw`](Self::into_raw). + pub fn from_raw(v: u32) -> Self { + Self(v) + } + + /// Convert into the raw underlying value. + pub fn into_raw(self) -> u32 { + self.0 + } +} diff --git a/src/layout/frame.rs b/src/layout/frame.rs index 61a84d6d6..6cecc7a34 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -1,6 +1,7 @@ use crate::color::Color; -use crate::env::{FaceId, ImageId}; +use crate::font::FaceId; use crate::geom::{Length, Path, Point, Size}; +use crate::image::ImageId; use serde::{Deserialize, Serialize}; diff --git a/src/layout/mod.rs b/src/layout/mod.rs index bdcf5ec48..30776fa2b 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -17,19 +17,20 @@ pub use shaping::*; pub use stack::*; use std::any::Any; +use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use decorum::N64; use fxhash::FxHasher64; -use crate::cache::{Cache, FramesEntry}; -use crate::env::Env; +use crate::cache::Cache; use crate::geom::*; +use crate::loading::Loader; /// Layout a tree into a collection of frames. -pub fn layout(env: &mut Env, cache: &mut Cache, tree: &Tree) -> Vec { - tree.layout(&mut LayoutContext { env, cache }) +pub fn layout(loader: &mut dyn Loader, cache: &mut Cache, tree: &Tree) -> Vec { + tree.layout(&mut LayoutContext { loader, cache }) } /// A tree of layout nodes. @@ -92,14 +93,14 @@ impl AnyNode { impl Layout for AnyNode { fn layout(&self, ctx: &mut LayoutContext, regions: &Regions) -> Vec { - if let Some(hit) = ctx.cache.frames.get(&self.hash) { + if let Some(hit) = ctx.cache.layout.frames.get(&self.hash) { if &hit.regions == regions { return hit.frames.clone(); } } let frames = self.node.layout(ctx, regions); - ctx.cache.frames.insert(self.hash, FramesEntry { + ctx.cache.layout.frames.insert(self.hash, FramesEntry { regions: regions.clone(), frames: frames.clone(), }); @@ -170,13 +171,39 @@ pub trait Layout { /// The context for layouting. pub struct LayoutContext<'a> { - /// The environment from which fonts are gathered. - pub env: &'a mut Env, - /// A cache which enables reuse of layout artifacts from past compilation - /// cycles. + /// The loader from which fonts are loaded. + pub loader: &'a mut dyn Loader, + /// A cache for loaded fonts and artifacts from past layouting. pub cache: &'a mut Cache, } +/// Caches layouting artifacts. +pub struct LayoutCache { + /// Maps from node hashes to the resulting frames and regions in which the + /// frames are valid. + pub frames: HashMap, +} + +impl LayoutCache { + /// Create a new, empty layout cache. + pub fn new() -> Self { + Self { frames: HashMap::new() } + } + + /// Clear the cache. + pub fn clear(&mut self) { + self.frames.clear(); + } +} + +/// Cached frames from past layouting. +pub struct FramesEntry { + /// The regions in which these frames are valid. + pub regions: Regions, + /// The cached frames for a node. + pub frames: Vec, +} + /// A sequence of regions to layout into. #[derive(Debug, Clone, PartialEq)] pub struct Regions { diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index f8ab70379..14ea86117 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -5,9 +5,8 @@ use std::ops::Range; use rustybuzz::UnicodeBuffer; use super::{Element, Frame, Glyph, LayoutContext, Text}; -use crate::env::FaceId; use crate::exec::FontProps; -use crate::font::Face; +use crate::font::{Face, FaceId}; use crate::geom::{Dir, Length, Point, Size}; use crate::util::SliceExt; @@ -215,10 +214,12 @@ fn shape_segment<'a>( let (face_id, fallback) = loop { // Try to load the next available font family. match families.next() { - Some(family) => match ctx.env.query_face(family, props.variant) { - Some(id) => break (id, true), - None => {} - }, + Some(family) => { + match ctx.cache.font.select(ctx.loader, family, props.variant) { + Some(id) => break (id, true), + None => {} + } + } // We're out of families, so we don't do any more fallback and just // shape the tofus with the first face we originally used. None => match first_face { @@ -242,7 +243,7 @@ fn shape_segment<'a>( }); // Shape! - let mut face = ctx.env.face(face_id); + let mut face = ctx.cache.font.get(face_id); let buffer = rustybuzz::shape(face.ttf(), &[], buffer); let infos = buffer.glyph_infos(); let pos = buffer.glyph_positions(); @@ -317,7 +318,7 @@ fn shape_segment<'a>( first_face, ); - face = ctx.env.face(face_id); + face = ctx.cache.font.get(face_id); } i += 1; @@ -331,6 +332,8 @@ fn measure( glyphs: &[ShapedGlyph], props: &FontProps, ) -> (Size, Length) { + let cache = &mut ctx.cache.font; + let mut width = Length::zero(); let mut top = Length::zero(); let mut bottom = Length::zero(); @@ -343,14 +346,14 @@ fn measure( // 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) = ctx.env.query_face(family, props.variant) { - expand_vertical(ctx.env.face(face_id)); + if let Some(face_id) = cache.select(ctx.loader, family, props.variant) { + expand_vertical(cache.get(face_id)); break; } } } else { for (face_id, group) in glyphs.group_by_key(|g| g.face_id) { - let face = ctx.env.face(face_id); + let face = cache.get(face_id); expand_vertical(face); for glyph in group { diff --git a/src/lib.rs b/src/lib.rs index 8742aeb80..c435c2dd7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,8 @@ //! tree]. The structures describing the tree can be found in the [syntax] //! module. //! - **Evaluation:** The next step is to [evaluate] the syntax tree. This -//! computes the value of each node in document and stores them in a map from -//! node-pointers to values. -//! - **Execution:** Now, we can [execute] the parsed and evaluated "script". +//! computes the value of each node in the document and produces a [module]. +//! - **Execution:** Now, we can [execute] the parsed and evaluated module. //! This produces a [layout tree], a high-level, fully styled representation //! of the document. The nodes of this tree are self-contained and //! order-independent and thus much better suited for layouting than the @@ -23,10 +22,11 @@ //! [parsed]: parse::parse //! [syntax tree]: syntax::Tree //! [evaluate]: eval::eval +//! [module]: eval::Module //! [execute]: exec::exec //! [layout tree]: layout::Tree //! [layouted]: layout::layout -//! [PDF]: pdf +//! [PDF]: export::pdf #[macro_use] pub mod diag; @@ -34,42 +34,45 @@ pub mod diag; pub mod eval; pub mod cache; pub mod color; -pub mod env; pub mod exec; +pub mod export; pub mod font; pub mod geom; +pub mod image; pub mod layout; pub mod library; +pub mod loading; pub mod paper; pub mod parse; -pub mod pdf; pub mod pretty; pub mod syntax; pub mod util; +use std::rc::Rc; + use crate::cache::Cache; use crate::diag::Pass; -use crate::env::Env; use crate::eval::Scope; use crate::exec::State; use crate::layout::Frame; +use crate::loading::Loader; -/// Process source code directly into a collection of frames. +/// Process source code directly into a collection of layouted frames. pub fn typeset( - env: &mut Env, + loader: &mut dyn Loader, cache: &mut Cache, src: &str, - scope: &Scope, + base: &Scope, state: State, ) -> Pass> { let parsed = parse::parse(src); - let evaluated = eval::eval(env, &parsed.output, scope); - let executed = exec::exec(env, &parsed.output, &evaluated.output, state); - let frames = layout::layout(env, cache, &executed.output); + let evaluated = eval::eval(loader, cache, Rc::new(parsed.output), base); + let executed = exec::exec(&evaluated.output.template, state); + let layouted = layout::layout(loader, cache, &executed.output); let mut diags = parsed.diags; diags.extend(evaluated.diags); diags.extend(executed.diags); - Pass::new(frames, diags) + Pass::new(layouted, diags) } diff --git a/src/library/image.rs b/src/library/image.rs index b73c26a9b..cd6a97d11 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -1,7 +1,7 @@ use ::image::GenericImageView; use super::*; -use crate::env::ImageId; +use crate::image::ImageId; use crate::layout::{AnyNode, Element, Frame, Layout, LayoutContext, Regions}; /// `image`: An image. @@ -18,21 +18,26 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let width = args.eat_named(ctx, "width"); let height = args.eat_named(ctx, "height"); + let mut node = None; + if let Some(path) = &path { + if let Some(id) = ctx.cache.image.load(ctx.loader, &path.v) { + let img = ctx.cache.image.get(id); + let dimensions = img.buf.dimensions(); + node = Some(ImageNode { id, dimensions, width, height }); + } else { + ctx.diag(error!(path.span, "failed to load image")); + } + } + Value::template("image", move |ctx| { - if let Some(path) = &path { - if let Some(id) = ctx.env.load_image(&path.v) { - let img = ctx.env.image(id); - let dimensions = img.buf.dimensions(); - ctx.push(ImageNode { id, dimensions, width, height }); - } else { - ctx.diag(error!(path.span, "failed to load image")); - } + if let Some(node) = node { + ctx.push(node); } }) } /// An image node. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, Hash)] struct ImageNode { /// The id of the image file. id: ImageId, diff --git a/src/env/fs.rs b/src/loading/fs.rs similarity index 100% rename from src/env/fs.rs rename to src/loading/fs.rs diff --git a/src/loading/mod.rs b/src/loading/mod.rs new file mode 100644 index 000000000..818e7e3c2 --- /dev/null +++ b/src/loading/mod.rs @@ -0,0 +1,43 @@ +//! Resource loading. + +#[cfg(feature = "fs")] +mod fs; + +#[cfg(feature = "fs")] +pub use fs::*; + +use std::rc::Rc; + +use crate::font::FaceInfo; + +/// A shared byte buffer. +pub type Buffer = Rc>; + +/// Loads resources from a local or remote 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 path. + fn load_file(&mut self, path: &str) -> Option; +} + +/// A loader which serves nothing. +pub 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 + } +} diff --git a/src/main.rs b/src/main.rs index aef0f573d..5370f6a8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,15 +3,6 @@ use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail, Context}; -use typst::cache::Cache; -use typst::diag::Pass; -use typst::env::{Env, FsLoader}; -use typst::exec::State; -use typst::library; -use typst::parse::LineMap; -use typst::pdf; -use typst::typeset; - fn main() -> anyhow::Result<()> { let args: Vec<_> = std::env::args().collect(); if args.len() < 2 || args.len() > 3 { @@ -35,35 +26,30 @@ fn main() -> anyhow::Result<()> { let src = fs::read_to_string(src_path).context("Failed to read from source file.")?; - let mut loader = FsLoader::new(); + let mut loader = typst::loading::FsLoader::new(); loader.search_path("fonts"); loader.search_system(); - let mut env = Env::new(loader); - let mut cache = Cache::new(); - let scope = library::new(); - let state = State::default(); - - let Pass { output: frames, diags } = - typeset(&mut env, &mut cache, &src, &scope, state); - if !diags.is_empty() { - let map = LineMap::new(&src); - for diag in diags { - let start = map.location(diag.span.start).unwrap(); - let end = map.location(diag.span.end).unwrap(); - println!( - "{}: {}:{}-{}: {}", - diag.level, - src_path.display(), - start, - end, - diag.message, - ); - } + let mut cache = typst::cache::Cache::new(&loader); + let scope = typst::library::new(); + let state = typst::exec::State::default(); + let pass = typst::typeset(&mut loader, &mut cache, &src, &scope, state); + let map = typst::parse::LineMap::new(&src); + for diag in pass.diags { + let start = map.location(diag.span.start).unwrap(); + let end = map.location(diag.span.end).unwrap(); + println!( + "{}: {}:{}-{}: {}", + diag.level, + src_path.display(), + start, + end, + diag.message, + ); } - let pdf_data = pdf::export(&env, &frames); - fs::write(&dest_path, pdf_data).context("Failed to write PDF file.")?; + let buffer = typst::export::pdf(&cache, &pass.output); + fs::write(&dest_path, buffer).context("Failed to write PDF file.")?; Ok(()) } diff --git a/src/pretty.rs b/src/pretty.rs index acdb46a0d..bf475bf62 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -604,7 +604,6 @@ mod tests { use std::rc::Rc; use super::*; - use crate::env::Env; use crate::parse::parse; #[track_caller] @@ -726,13 +725,6 @@ mod tests { roundtrip("#for k, x in y {z}"); } - #[test] - fn test_pretty_print_with_map() { - let tree = parse("*[{1+2}[{4}]]*{2+3}").output; - let map = eval(&mut Env::blank(), &tree, &Default::default()).output; - assert_eq!(pretty_with_map(&tree, &map), "*[3[4]]*5"); - } - #[test] fn test_pretty_print_value() { // Simple values. diff --git a/tests/typeset.rs b/tests/typeset.rs index 57693cfde..faf76f7e9 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -15,18 +15,15 @@ use walkdir::WalkDir; use typst::cache::Cache; use typst::color; -use typst::diag::{Diag, DiagSet, Level, Pass}; -use typst::env::{Env, FsLoader, ImageId}; +use typst::diag::{Diag, DiagSet, Level}; use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value}; use typst::exec::State; use typst::geom::{self, Length, Point, Sides, Size}; +use typst::image::ImageId; use typst::layout::{Element, Fill, Frame, Shape, Text}; -use typst::library; +use typst::loading::FsLoader; use typst::parse::{LineMap, Scanner}; -use typst::pdf; -use typst::pretty::pretty; use typst::syntax::{Location, Pos}; -use typst::typeset; const TYP_DIR: &str = "./typ"; const REF_DIR: &str = "./ref"; @@ -63,10 +60,10 @@ fn main() { println!("Running {} tests", len); } - let mut loader = FsLoader::new(); + let mut loader = typst::loading::FsLoader::new(); loader.search_path(FONT_DIR); - let mut env = Env::new(loader); + let mut cache = typst::cache::Cache::new(&loader); let mut ok = true; for src_path in filtered { @@ -77,7 +74,8 @@ fn main() { args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf")); ok &= test( - &mut env, + &mut loader, + &mut cache, &src_path, &png_path, &ref_path, @@ -130,7 +128,8 @@ struct Panic { } fn test( - env: &mut Env, + loader: &mut FsLoader, + cache: &mut Cache, src_path: &Path, png_path: &Path, ref_path: &Path, @@ -163,7 +162,7 @@ fn test( } } else { let (part_ok, compare_here, part_frames) = - test_part(env, part, i, compare_ref, lines); + test_part(loader, cache, part, i, compare_ref, lines); ok &= part_ok; compare_ever |= compare_here; frames.extend(part_frames); @@ -174,12 +173,12 @@ fn test( if compare_ever { if let Some(pdf_path) = pdf_path { - let pdf_data = pdf::export(&env, &frames); + let pdf_data = typst::export::pdf(cache, &frames); fs::create_dir_all(&pdf_path.parent().unwrap()).unwrap(); fs::write(pdf_path, pdf_data).unwrap(); } - let canvas = draw(&env, &frames, 2.0); + let canvas = draw(&cache, &frames, 2.0); fs::create_dir_all(&png_path.parent().unwrap()).unwrap(); canvas.save_png(png_path).unwrap(); @@ -202,7 +201,8 @@ fn test( } fn test_part( - env: &mut Env, + loader: &mut FsLoader, + cache: &mut Cache, src: &str, i: usize, compare_ref: bool, @@ -212,7 +212,8 @@ fn test_part( let (local_compare_ref, ref_diags) = parse_metadata(src, &map); let compare_ref = local_compare_ref.unwrap_or(compare_ref); - let mut scope = library::new(); + // We hook up some extra test helpers into the global scope. + let mut scope = typst::library::new(); let panics = Rc::new(RefCell::new(vec![])); register_helpers(&mut scope, Rc::clone(&panics)); @@ -222,10 +223,9 @@ fn test_part( state.page.size = Size::new(Length::pt(120.0), Length::raw(f64::INFINITY)); state.page.margins = Sides::splat(Some(Length::pt(10.0).into())); - let Pass { output: mut frames, diags } = - typeset(env, &mut Cache::new(), &src, &scope, state); + let mut pass = typst::typeset(loader, cache, &src, &scope, state); if !compare_ref { - frames.clear(); + pass.output.clear(); } let mut ok = true; @@ -242,11 +242,11 @@ fn test_part( ok = false; } - if diags != ref_diags { + if pass.diags != ref_diags { println!(" Subtest {} does not match expected diagnostics. ❌", i); ok = false; - for diag in &diags { + for diag in &pass.diags { if !ref_diags.contains(diag) { print!(" Not annotated | "); print_diag(diag, &map, lines); @@ -254,14 +254,14 @@ fn test_part( } for diag in &ref_diags { - if !diags.contains(diag) { + if !pass.diags.contains(diag) { print!(" Not emitted | "); print_diag(diag, &map, lines); } } } - (ok, compare_ref, frames) + (ok, compare_ref, pass.output) } fn parse_metadata(src: &str, map: &LineMap) -> (Option, DiagSet) { @@ -311,7 +311,7 @@ fn parse_metadata(src: &str, map: &LineMap) -> (Option, DiagSet) { fn register_helpers(scope: &mut Scope, panics: Rc>>) { pub fn args(_: &mut EvalContext, args: &mut FuncArgs) -> Value { - let repr = pretty(args); + let repr = typst::pretty::pretty(args); args.items.clear(); Value::template("args", move |ctx| { let snapshot = ctx.state.clone(); @@ -345,7 +345,7 @@ fn print_diag(diag: &Diag, map: &LineMap, lines: u32) { println!("{}: {}-{}: {}", diag.level, start, end, diag.message); } -fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap { +fn draw(cache: &Cache, frames: &[Frame], dpi: f32) -> Pixmap { let pad = Length::pt(5.0); let height = pad + frames.iter().map(|l| l.size.height + pad).sum::(); @@ -393,13 +393,13 @@ fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap { let ts = ts.pre_translate(x, y); match *element { Element::Text(ref text) => { - draw_text(&mut canvas, env, ts, text); + draw_text(&mut canvas, cache, ts, text); } Element::Geometry(ref shape, fill) => { draw_geometry(&mut canvas, ts, shape, fill); } Element::Image(id, size) => { - draw_image(&mut canvas, env, ts, id, size); + draw_image(&mut canvas, cache, ts, id, size); } } } @@ -410,8 +410,8 @@ fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap { canvas } -fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, text: &Text) { - let ttf = env.face(text.face_id).ttf(); +fn draw_text(canvas: &mut Pixmap, cache: &Cache, ts: Transform, text: &Text) { + let ttf = cache.font.get(text.face_id).ttf(); let mut x = 0.0; for glyph in &text.glyphs { @@ -480,8 +480,14 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, shape: &Shape, fill: Fill) }; } -fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, id: ImageId, size: Size) { - let img = env.image(id); +fn draw_image( + canvas: &mut Pixmap, + cache: &Cache, + ts: Transform, + id: ImageId, + size: Size, +) { + let img = cache.image.get(id); let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap(); for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {