diff --git a/bench/src/clock.rs b/bench/src/clock.rs index c3dcaeb73..e33c6c13a 100644 --- a/bench/src/clock.rs +++ b/bench/src/clock.rs @@ -4,37 +4,41 @@ use std::rc::Rc; use criterion::{criterion_group, criterion_main, Criterion}; -use typst::cache::Cache; -use typst::eval::{eval, Module, Scope}; -use typst::exec::{exec, State}; +use typst::eval::{eval, Module}; +use typst::exec::exec; use typst::export::pdf; use typst::layout::{layout, Frame, LayoutTree}; use typst::loading::{FileId, FsLoader}; use typst::parse::parse; use typst::syntax::SyntaxTree; -use typst::typeset; +use typst::Context; const FONT_DIR: &str = "../fonts"; const TYP_DIR: &str = "../tests/typ"; const CASES: &[&str] = &["coma.typ", "text/basic.typ"]; fn benchmarks(c: &mut Criterion) { - let ctx = Context::new(); + let loader = { + let mut loader = FsLoader::new(); + loader.search_path(FONT_DIR); + Rc::new(loader) + }; + + let ctx = Rc::new(RefCell::new(Context::new(loader.clone()))); for case in CASES { let path = Path::new(TYP_DIR).join(case); let name = path.file_stem().unwrap().to_string_lossy(); - let src_id = ctx.borrow_mut().loader.resolve_path(&path).unwrap(); + let src_id = loader.resolve_path(&path).unwrap(); let src = std::fs::read_to_string(&path).unwrap(); let case = Case::new(src_id, src, ctx.clone()); macro_rules! bench { - ($step:literal, setup = |$cache:ident| $setup:expr, code = $code:expr $(,)?) => { + ($step:literal, setup = |$ctx:ident| $setup:expr, code = $code:expr $(,)?) => { c.bench_function(&format!("{}-{}", $step, name), |b| { b.iter_batched( || { - let mut borrowed = ctx.borrow_mut(); - let $cache = &mut borrowed.cache; + let mut $ctx = ctx.borrow_mut(); $setup }, |_| $code, @@ -61,12 +65,12 @@ fn benchmarks(c: &mut Criterion) { { bench!( "layout", - setup = |cache| cache.layout.clear(), + setup = |ctx| ctx.layouts.clear(), code = case.layout(), ); bench!( "typeset", - setup = |cache| cache.layout.clear(), + setup = |ctx| ctx.layouts.clear(), code = case.typeset(), ); bench!("layout-cached", case.layout()); @@ -77,28 +81,11 @@ fn benchmarks(c: &mut Criterion) { } } -/// The context required for benchmarking a case. -struct Context { - loader: FsLoader, - cache: Cache, -} - -impl Context { - fn new() -> Rc> { - let mut loader = FsLoader::new(); - loader.search_path(FONT_DIR); - let cache = Cache::new(&loader); - Rc::new(RefCell::new(Self { loader, cache })) - } -} - /// A test case with prepared intermediate results. struct Case { ctx: Rc>, src_id: FileId, src: String, - scope: Scope, - state: State, ast: Rc, module: Module, tree: LayoutTree, @@ -108,20 +95,15 @@ struct Case { impl Case { fn new(src_id: FileId, src: String, ctx: Rc>) -> Self { let mut borrowed = ctx.borrow_mut(); - let Context { loader, cache } = &mut *borrowed; - let scope = typst::library::new(); - let state = typst::exec::State::default(); let ast = Rc::new(parse(&src).output); - let module = eval(loader, cache, src_id, Rc::clone(&ast), &scope).output; - let tree = exec(&module.template, state.clone()).output; - let frames = layout(loader, cache, &tree); + let module = eval(&mut borrowed, src_id, Rc::clone(&ast)).output; + let tree = exec(&mut borrowed, &module.template).output; + let frames = layout(&mut borrowed, &tree); drop(borrowed); Self { ctx, src_id, src, - scope, - state, ast, module, tree, @@ -135,31 +117,23 @@ impl Case { fn eval(&self) -> Module { let mut borrowed = self.ctx.borrow_mut(); - let Context { loader, cache } = &mut *borrowed; - let ast = Rc::clone(&self.ast); - eval(loader, cache, self.src_id, ast, &self.scope).output + eval(&mut borrowed, self.src_id, Rc::clone(&self.ast)).output } fn exec(&self) -> LayoutTree { - exec(&self.module.template, self.state.clone()).output + exec(&mut self.ctx.borrow_mut(), &self.module.template).output } fn layout(&self) -> Vec> { - let mut borrowed = self.ctx.borrow_mut(); - let Context { loader, cache } = &mut *borrowed; - layout(loader, cache, &self.tree) + layout(&mut self.ctx.borrow_mut(), &self.tree) } fn typeset(&self) -> Vec> { - let mut borrowed = self.ctx.borrow_mut(); - let Context { loader, cache } = &mut *borrowed; - let state = self.state.clone(); - typeset(loader, cache, self.src_id, &self.src, &self.scope, state).output + self.ctx.borrow_mut().typeset(self.src_id, &self.src).output } fn pdf(&self) -> Vec { - let ctx = self.ctx.borrow(); - pdf(&ctx.cache, &self.frames) + pdf(&self.ctx.borrow(), &self.frames) } } diff --git a/src/cache.rs b/src/cache.rs deleted file mode 100644 index 2aa276aa4..000000000 --- a/src/cache.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Caching of compilation artifacts. - -use crate::font::FontCache; -use crate::image::ImageCache; -#[cfg(feature = "layout-cache")] -use crate::layout::LayoutCache; -use crate::loading::Loader; - -/// Caches compilation artifacts. -pub struct Cache { - /// Caches parsed font faces. - pub font: FontCache, - /// Caches decoded images. - pub image: ImageCache, - /// Caches layouting artifacts. - #[cfg(feature = "layout-cache")] - pub layout: LayoutCache, -} - -impl Cache { - /// Create a new, empty cache. - pub fn new(loader: &dyn Loader) -> Self { - Self { - font: FontCache::new(loader), - image: ImageCache::new(), - #[cfg(feature = "layout-cache")] - layout: LayoutCache::new(), - } - } -} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index cbb61153c..0efaaa90d 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -25,24 +25,19 @@ use std::mem; use std::path::Path; use std::rc::Rc; -use crate::cache::Cache; use crate::diag::{Diag, DiagSet, Pass}; use crate::eco::EcoString; use crate::geom::{Angle, Fractional, Length, Relative}; +use crate::image::ImageCache; use crate::loading::{FileId, Loader}; use crate::parse::parse; use crate::syntax::visit::Visit; use crate::syntax::*; +use crate::Context; /// Evaluate a parsed source file into a module. -pub fn eval( - loader: &mut dyn Loader, - cache: &mut Cache, - location: FileId, - ast: Rc, - scope: &Scope, -) -> Pass { - let mut ctx = EvalContext::new(loader, cache, location, scope); +pub fn eval(ctx: &mut Context, location: FileId, ast: Rc) -> Pass { + let mut ctx = EvalContext::new(ctx, location); let template = ast.eval(&mut ctx); let module = Module { scope: ctx.scopes.top, template }; Pass::new(module, ctx.diags) @@ -60,9 +55,9 @@ pub struct Module { /// The context for evaluation. pub struct EvalContext<'a> { /// 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, + pub loader: &'a dyn Loader, + /// The cache for decoded images. + pub images: &'a mut ImageCache, /// The active scopes. pub scopes: Scopes<'a>, /// Evaluation diagnostics. @@ -74,17 +69,12 @@ pub struct EvalContext<'a> { } impl<'a> EvalContext<'a> { - /// Create a new evaluation context with a base scope. - pub fn new( - loader: &'a mut dyn Loader, - cache: &'a mut Cache, - location: FileId, - scope: &'a Scope, - ) -> Self { + /// Create a new evaluation context. + pub fn new(ctx: &'a mut Context, location: FileId) -> Self { Self { - loader, - cache, - scopes: Scopes::new(Some(scope)), + loader: ctx.loader.as_ref(), + images: &mut ctx.images, + scopes: Scopes::new(Some(&ctx.std)), diags: DiagSet::new(), route: vec![location], modules: HashMap::new(), diff --git a/src/exec/context.rs b/src/exec/context.rs index 04c0169d0..925fd7de5 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -10,6 +10,7 @@ use crate::layout::{ LayoutNode, LayoutTree, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, }; use crate::syntax::{Span, SyntaxTree}; +use crate::Context; /// The context for execution. pub struct ExecContext { @@ -28,13 +29,13 @@ pub struct ExecContext { impl ExecContext { /// Create a new execution context with a base state. - pub fn new(state: State) -> Self { + pub fn new(ctx: &mut Context) -> Self { Self { + state: ctx.state.clone(), diags: DiagSet::new(), tree: LayoutTree { runs: vec![] }, - page: Some(PageBuilder::new(&state, true)), - stack: StackBuilder::new(&state), - state, + page: Some(PageBuilder::new(&ctx.state, true)), + stack: StackBuilder::new(&ctx.state), } } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 752bdba56..d61e07937 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -16,10 +16,11 @@ use crate::geom::{Dir, Gen}; use crate::layout::{LayoutTree, StackChild, StackNode}; use crate::pretty::pretty; use crate::syntax::*; +use crate::Context; /// Execute a template to produce a layout tree. -pub fn exec(template: &Template, state: State) -> Pass { - let mut ctx = ExecContext::new(state); +pub fn exec(ctx: &mut Context, template: &Template) -> Pass { + let mut ctx = ExecContext::new(ctx); template.exec(&mut ctx); ctx.finish() } diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 7e55a6015..bfd364213 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -13,12 +13,12 @@ use pdf_writer::{ }; use ttf_parser::{name_id, GlyphId}; -use crate::cache::Cache; use crate::color::Color; -use crate::font::{Em, FaceId}; +use crate::font::{Em, FaceId, FontCache}; use crate::geom::{self, Length, Size}; -use crate::image::{Image, ImageId}; +use crate::image::{Image, ImageCache, ImageId}; use crate::layout::{Element, Frame, Geometry, Paint}; +use crate::Context; /// Export a collection of frames into a PDF document. /// @@ -27,53 +27,55 @@ use crate::layout::{Element, Frame, Geometry, Paint}; /// can be included in the PDF. /// /// Returns the raw bytes making up the PDF document. -pub fn pdf(cache: &Cache, frames: &[Rc]) -> Vec { - PdfExporter::new(cache, frames).write() +pub fn pdf(ctx: &Context, frames: &[Rc]) -> Vec { + PdfExporter::new(ctx, frames).write() } struct PdfExporter<'a> { writer: PdfWriter, frames: &'a [Rc], - cache: &'a Cache, + fonts: &'a FontCache, + font_map: Remapper, + images: &'a ImageCache, + image_map: Remapper, refs: Refs, - fonts: Remapper, - images: Remapper, } impl<'a> PdfExporter<'a> { - fn new(cache: &'a Cache, frames: &'a [Rc]) -> Self { + fn new(ctx: &'a Context, frames: &'a [Rc]) -> Self { let mut writer = PdfWriter::new(1, 7); writer.set_indent(2); - let mut fonts = Remapper::new(); - let mut images = Remapper::new(); + let mut font_map = Remapper::new(); + let mut image_map = Remapper::new(); let mut alpha_masks = 0; for frame in frames { for (_, element) in frame.elements() { match *element { - Element::Text(ref shaped) => fonts.insert(shaped.face_id), + Element::Text(ref shaped) => font_map.insert(shaped.face_id), Element::Geometry(_, _) => {} Element::Image(id, _) => { - let img = cache.image.get(id); + let img = ctx.images.get(id); if img.buf.color().has_alpha() { alpha_masks += 1; } - images.insert(id); + image_map.insert(id); } } } } - let refs = Refs::new(frames.len(), fonts.len(), images.len(), alpha_masks); + let refs = Refs::new(frames.len(), font_map.len(), image_map.len(), alpha_masks); Self { writer, frames, - cache, + fonts: &ctx.fonts, + images: &ctx.images, refs, - fonts, - images, + font_map, + image_map, } } @@ -95,7 +97,7 @@ impl<'a> PdfExporter<'a> { let mut resources = pages.resources(); let mut fonts = resources.fonts(); - for (refs, f) in self.refs.fonts().zip(self.fonts.pdf_indices()) { + for (refs, f) in self.refs.fonts().zip(self.font_map.pdf_indices()) { let name = format!("F{}", f); fonts.pair(Name(name.as_bytes()), refs.type0_font); } @@ -103,7 +105,7 @@ impl<'a> PdfExporter<'a> { drop(fonts); let mut images = resources.x_objects(); - for (id, im) in self.refs.images().zip(self.images.pdf_indices()) { + for (id, im) in self.refs.images().zip(self.image_map.pdf_indices()) { let name = format!("Im{}", im); images.pair(Name(name.as_bytes()), id); } @@ -163,7 +165,7 @@ impl<'a> PdfExporter<'a> { face = Some(shaped.face_id); size = shaped.size; - let name = format!("F{}", self.fonts.map(shaped.face_id)); + let name = format!("F{}", self.font_map.map(shaped.face_id)); text.font(Name(name.as_bytes()), size.to_pt() as f32); } @@ -206,7 +208,7 @@ impl<'a> PdfExporter<'a> { } Element::Image(id, Size { width, height }) => { - let name = format!("Im{}", self.images.map(id)); + let name = format!("Im{}", self.image_map.map(id)); let w = width.to_pt() as f32; let h = height.to_pt() as f32; @@ -222,8 +224,8 @@ 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.cache.font.get(face_id); + for (refs, face_id) in self.refs.fonts().zip(self.font_map.layout_indices()) { + let face = self.fonts.get(face_id); let ttf = face.ttf(); let name = ttf @@ -327,8 +329,8 @@ impl<'a> PdfExporter<'a> { fn write_images(&mut self) { let mut masks_seen = 0; - for (id, image_id) in self.refs.images().zip(self.images.layout_indices()) { - let img = self.cache.image.get(image_id); + for (id, image_id) in self.refs.images().zip(self.image_map.layout_indices()) { + let img = self.images.get(image_id); let (width, height) = img.buf.dimensions(); // Add the primary image. diff --git a/src/font.rs b/src/font.rs index 273c29144..0d6cd8809 100644 --- a/src/font.rs +++ b/src/font.rs @@ -203,6 +203,7 @@ impl Add for Em { /// Caches parsed font faces. pub struct FontCache { + loader: Rc, faces: Vec>, families: HashMap>, buffers: HashMap>>, @@ -211,7 +212,7 @@ pub struct FontCache { impl FontCache { /// Create a new, empty font cache. - pub fn new(loader: &dyn Loader) -> Self { + pub fn new(loader: Rc) -> Self { let mut faces = vec![]; let mut families = HashMap::>::new(); @@ -225,6 +226,7 @@ impl FontCache { } Self { + loader, faces, families, buffers: HashMap::new(), @@ -234,15 +236,10 @@ impl FontCache { /// 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 { + pub fn select(&mut self, family: &str, variant: FontVariant) -> Option { // Check whether a family with this name exists. let ids = self.families.get(family)?; - let infos = loader.faces(); + let infos = self.loader.faces(); let mut best = None; let mut best_key = None; @@ -284,7 +281,7 @@ impl FontCache { let buffer = match self.buffers.entry(file) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { - let buffer = loader.load_file(file)?; + let buffer = self.loader.load_file(file)?; entry.insert(Rc::new(buffer)) } }; diff --git a/src/image.rs b/src/image.rs index 282832593..d0719ac78 100644 --- a/src/image.rs +++ b/src/image.rs @@ -3,6 +3,7 @@ use std::collections::{hash_map::Entry, HashMap}; use std::fmt::{self, Debug, Formatter}; use std::io::Cursor; +use std::rc::Rc; use image::io::Reader as ImageReader; use image::{DynamicImage, GenericImageView, ImageFormat}; @@ -53,25 +54,27 @@ impl Debug for Image { } /// Caches decoded images. -#[derive(Default)] pub struct ImageCache { - /// Maps from file hashes to ids of decoded images. + loader: Rc, images: HashMap, - /// Callback for loaded images. on_load: Option>, } impl ImageCache { /// Create a new, empty image cache. - pub fn new() -> Self { - Self::default() + pub fn new(loader: Rc) -> Self { + Self { + loader, + images: HashMap::new(), + on_load: None, + } } /// Load and decode an image file from a path. - pub fn load(&mut self, loader: &mut dyn Loader, file: FileId) -> Option { + pub fn load(&mut self, file: FileId) -> Option { let id = ImageId(file.into_raw()); if let Entry::Vacant(entry) = self.images.entry(id) { - let buffer = loader.load_file(file)?; + let buffer = self.loader.load_file(file)?; let image = Image::parse(&buffer)?; if let Some(callback) = &self.on_load { callback(id, &image); diff --git a/src/layout/image.rs b/src/layout/image.rs index 07d7799c4..2c20642b8 100644 --- a/src/layout/image.rs +++ b/src/layout/image.rs @@ -28,7 +28,7 @@ impl Layout for ImageNode { let width = self.width.map(|w| w.resolve(base.width)); let height = self.height.map(|w| w.resolve(base.height)); - let dimensions = ctx.cache.image.get(self.id).buf.dimensions(); + let dimensions = ctx.images.get(self.id).buf.dimensions(); let pixel_width = dimensions.0 as f64; let pixel_height = dimensions.1 as f64; let pixel_ratio = pixel_width / pixel_height; diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 523d1a926..7f8ee4ff2 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -32,22 +32,16 @@ use std::rc::Rc; #[cfg(feature = "layout-cache")] use fxhash::FxHasher64; -use crate::cache::Cache; +use crate::font::FontCache; use crate::geom::*; +use crate::image::ImageCache; use crate::loading::Loader; +use crate::Context; /// Layout a tree into a collection of frames. -pub fn layout( - loader: &mut dyn Loader, - cache: &mut Cache, - tree: &LayoutTree, -) -> Vec> { - tree.layout(&mut LayoutContext { - loader, - cache, - #[cfg(feature = "layout-cache")] - level: 0, - }) +pub fn layout(ctx: &mut Context, tree: &LayoutTree) -> Vec> { + let mut ctx = LayoutContext::new(ctx); + tree.layout(&mut ctx) } /// A tree of layout nodes. @@ -129,15 +123,15 @@ impl Layout for LayoutNode { #[cfg(feature = "layout-cache")] { ctx.level += 1; - let frames = - ctx.cache.layout.get(self.hash, regions.clone()).unwrap_or_else(|| { - let frames = self.node.layout(ctx, regions); - ctx.cache.layout.insert(self.hash, frames.clone(), ctx.level - 1); - frames - }); + let frames = ctx.layouts.get(self.hash, regions.clone()).unwrap_or_else(|| { + let frames = self.node.layout(ctx, regions); + ctx.layouts.insert(self.hash, frames.clone(), ctx.level - 1); + frames + }); ctx.level -= 1; frames } + #[cfg(not(feature = "layout-cache"))] self.node.layout(ctx, regions) } @@ -214,14 +208,34 @@ pub trait Layout { /// The context for layouting. pub struct LayoutContext<'a> { /// 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, + pub loader: &'a dyn Loader, + /// The cache for parsed font faces. + pub fonts: &'a mut FontCache, + /// The cache for decoded imges. + pub images: &'a mut ImageCache, + /// The cache for layouting artifacts. + #[cfg(feature = "layout-cache")] + pub layouts: &'a mut LayoutCache, /// How deeply nested the current layout tree position is. #[cfg(feature = "layout-cache")] pub level: usize, } +impl<'a> LayoutContext<'a> { + /// Create a new layout context. + pub fn new(ctx: &'a mut Context) -> Self { + Self { + loader: ctx.loader.as_ref(), + fonts: &mut ctx.fonts, + images: &mut ctx.images, + #[cfg(feature = "layout-cache")] + layouts: &mut ctx.layouts, + #[cfg(feature = "layout-cache")] + level: 0, + } + } +} + /// A sequence of regions to layout into. #[derive(Debug, Clone, Eq, PartialEq)] pub struct Regions { diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index 9b8774ccd..5e1bc3277 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -235,7 +235,7 @@ fn shape_segment<'a>( } } - if let Some(id) = ctx.cache.font.select(ctx.loader, family, variant) { + if let Some(id) = ctx.fonts.select(family, variant) { break (id, true); } } @@ -262,7 +262,7 @@ fn shape_segment<'a>( }); // Shape! - let mut face = ctx.cache.font.get(face_id); + let mut face = ctx.fonts.get(face_id); let buffer = rustybuzz::shape(face.ttf(), &[], buffer); let infos = buffer.glyph_infos(); let pos = buffer.glyph_positions(); @@ -337,7 +337,7 @@ fn shape_segment<'a>( first_face, ); - face = ctx.cache.font.get(face_id); + face = ctx.fonts.get(face_id); } i += 1; @@ -351,8 +351,6 @@ fn measure( glyphs: &[ShapedGlyph], state: &FontState, ) -> (Size, Length) { - let cache = &mut ctx.cache.font; - let mut width = Length::zero(); let mut top = Length::zero(); let mut bottom = Length::zero(); @@ -365,14 +363,14 @@ fn measure( // When there are no glyphs, we just use the vertical metrics of the // first available font. for family in state.families.iter() { - if let Some(face_id) = cache.select(ctx.loader, family, state.variant) { - expand_vertical(cache.get(face_id)); + if let Some(face_id) = ctx.fonts.select(family, state.variant) { + expand_vertical(ctx.fonts.get(face_id)); break; } } } else { for (face_id, group) in glyphs.group_by_key(|g| g.face_id) { - let face = cache.get(face_id); + let face = ctx.fonts.get(face_id); expand_vertical(face); for glyph in group { @@ -394,7 +392,7 @@ fn decorate( state: &FontState, ) { let mut apply = |substate: &LineState, metrics: fn(&Face) -> &LineMetrics| { - let metrics = metrics(&ctx.cache.font.get(face_id)); + let metrics = metrics(ctx.fonts.get(face_id)); let stroke = substate.stroke.unwrap_or(state.fill); diff --git a/src/lib.rs b/src/lib.rs index d1f433808..90fd87748 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,6 @@ pub mod diag; #[macro_use] pub mod eval; -pub mod cache; pub mod color; pub mod eco; pub mod exec; @@ -51,46 +50,106 @@ pub mod util; use std::rc::Rc; -use crate::cache::Cache; use crate::diag::Pass; use crate::eval::Scope; use crate::exec::State; +use crate::font::FontCache; +use crate::image::ImageCache; use crate::layout::Frame; +#[cfg(feature = "layout-cache")] +use crate::layout::LayoutCache; use crate::loading::{FileId, Loader}; -/// Process source code directly into a collection of layouted frames. -/// -/// # Parameters -/// - The `loader` is used to load fonts, images and other source files. -/// - The `cache` stores things that are reusable across several compilations -/// like loaded fonts, decoded images and layouting artifacts. -/// - The `location` is the file id of the source file and is used to resolve -/// relative paths (for importing and image loading). -/// - The `src` is the _Typst_ source code to typeset. -/// - The `scope` contains definitions that are available everywhere, typically -/// the standard library. -/// - The `state` defines initial properties for page size, font selection and -/// so on. -/// -/// # Return value -/// Returns a vector of frames representing individual pages alongside -/// diagnostic information (errors and warnings). -pub fn typeset( - loader: &mut dyn Loader, - cache: &mut Cache, - location: FileId, - src: &str, - scope: &Scope, +/// The core context which holds the loader, configuration and cached artifacts. +pub struct Context { + /// The loader the context was created with. + pub loader: Rc, + /// Caches parsed font faces. + pub fonts: FontCache, + /// Caches decoded images. + pub images: ImageCache, + /// Caches layouting artifacts. + #[cfg(feature = "layout-cache")] + pub layouts: LayoutCache, + /// The standard library scope. + std: Scope, + /// The default state. state: State, -) -> Pass>> { - let ast = parse::parse(src); - let module = eval::eval(loader, cache, location, Rc::new(ast.output), scope); - let tree = exec::exec(&module.output.template, state); - let frames = layout::layout(loader, cache, &tree.output); - - let mut diags = ast.diags; - diags.extend(module.diags); - diags.extend(tree.diags); - - Pass::new(frames, diags) +} + +impl Context { + /// Create a new context with the default settings. + pub fn new(loader: Rc) -> Self { + Self::builder().build(loader) + } + + /// Create a new context with advanced settings. + pub fn builder() -> ContextBuilder { + ContextBuilder::default() + } + + /// Garbage-collect caches. + pub fn turnaround(&mut self) { + #[cfg(feature = "layout-cache")] + self.layouts.turnaround(); + } + + /// Typeset a source file into a collection of layouted frames. + /// + /// The `file` is the file id of the source file and is used to resolve + /// relative paths (for importing and image loading). + /// + /// Returns a vector of frames representing individual pages alongside + /// diagnostic information (errors and warnings). + pub fn typeset(&mut self, file: FileId, src: &str) -> Pass>> { + let ast = parse::parse(src); + let module = eval::eval(self, file, Rc::new(ast.output)); + let tree = exec::exec(self, &module.output.template); + let frames = layout::layout(self, &tree.output); + + let mut diags = ast.diags; + diags.extend(module.diags); + diags.extend(tree.diags); + + Pass::new(frames, diags) + } +} + +/// A builder for a [`Context`]. +/// +/// This struct is created by [`Context::builder`]. +#[derive(Default)] +pub struct ContextBuilder { + std: Option, + state: Option, +} + +impl ContextBuilder { + /// The scope containing definitions that are available everywhere, + /// (the standard library). + pub fn std(mut self, std: Scope) -> Self { + self.std = Some(std); + self + } + + /// The `state` defining initial properties for page size, font selection + /// and so on. + pub fn state(mut self, state: State) -> Self { + self.state = Some(state); + self + } + + /// Finish building the context by providing the `loader` used to load + /// fonts, images, source files and other resources. + pub fn build(self, loader: Rc) -> Context { + Context { + loader: Rc::clone(&loader), + fonts: FontCache::new(Rc::clone(&loader)), + images: ImageCache::new(loader), + #[cfg(feature = "layout-cache")] + layouts: LayoutCache::new(), + std: self.std.unwrap_or(library::new()), + state: self.state.unwrap_or_default(), + } + } } diff --git a/src/library/elements.rs b/src/library/elements.rs index 03fccfe7b..33e8069c7 100644 --- a/src/library/elements.rs +++ b/src/library/elements.rs @@ -16,7 +16,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let mut node = None; if let Some(path) = &path { if let Some(file) = ctx.resolve(&path.v, path.span) { - if let Some(id) = ctx.cache.image.load(ctx.loader, file) { + if let Some(id) = ctx.images.load(file) { node = Some(ImageNode { id, width, height }); } else { ctx.diag(error!(path.span, "failed to load image")); diff --git a/src/loading/fs.rs b/src/loading/fs.rs index ea33016cb..8785499e1 100644 --- a/src/loading/fs.rs +++ b/src/loading/fs.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::collections::HashMap; use std::fs::{self, File}; use std::io; @@ -18,23 +19,23 @@ use crate::util::PathExt; #[derive(Default, Debug, Clone)] pub struct FsLoader { faces: Vec, - paths: HashMap, + paths: RefCell>, } impl FsLoader { /// Create a new loader without any fonts. pub fn new() -> Self { - Self { faces: vec![], paths: HashMap::new() } + Self { faces: vec![], paths: RefCell::default() } } /// Resolve a file id for a path. - pub fn resolve_path(&mut self, path: &Path) -> io::Result { + pub fn resolve_path(&self, path: &Path) -> io::Result { let file = File::open(path)?; let meta = file.metadata()?; if meta.is_file() { let handle = Handle::from_file(file)?; let id = FileId(fxhash::hash64(&handle)); - self.paths.insert(id, path.normalize()); + self.paths.borrow_mut().insert(id, path.normalize()); Ok(id) } else { Err(io::Error::new(io::ErrorKind::Other, "not a file")) @@ -165,14 +166,13 @@ impl Loader for FsLoader { &self.faces } - fn resolve_from(&mut self, base: FileId, path: &Path) -> Option { - let dir = self.paths[&base].parent()?; - let full = dir.join(path); - self.resolve_path(&full).ok() + fn resolve_from(&self, base: FileId, path: &Path) -> Option { + let full = self.paths.borrow()[&base].parent()?.join(path); + self.resolve(&full).ok() } - fn load_file(&mut self, id: FileId) -> Option> { - fs::read(&self.paths[&id]).ok() + fn load_file(&self, id: FileId) -> Option> { + fs::read(&self.paths.borrow()[&id]).ok() } } @@ -185,7 +185,8 @@ mod tests { let mut loader = FsLoader::new(); loader.search_path("fonts"); - let mut paths: Vec<_> = loader.paths.values().collect(); + let map = loader.paths.borrow(); + let mut paths: Vec<_> = map.values().collect(); paths.sort(); assert_eq!(paths, [ diff --git a/src/loading/mod.rs b/src/loading/mod.rs index c2f7ca395..3be74428a 100644 --- a/src/loading/mod.rs +++ b/src/loading/mod.rs @@ -21,10 +21,10 @@ pub trait Loader { /// /// This should return the same id for all paths pointing to the same file /// and `None` if the file does not exist. - fn resolve_from(&mut self, base: FileId, path: &Path) -> Option; + fn resolve_from(&self, base: FileId, path: &Path) -> Option; /// Load a file by id. - fn load_file(&mut self, id: FileId) -> Option>; + fn load_file(&self, id: FileId) -> Option>; } /// A file id that can be [resolved](Loader::resolve_from) from a path. @@ -53,11 +53,11 @@ impl Loader for BlankLoader { &[] } - fn resolve_from(&mut self, _: FileId, _: &Path) -> Option { + fn resolve_from(&self, _: FileId, _: &Path) -> Option { None } - fn load_file(&mut self, _: FileId) -> Option> { + fn load_file(&self, _: FileId) -> Option> { None } } diff --git a/src/main.rs b/src/main.rs index 036802046..f9da37fad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use std::fs; use std::path::{Path, PathBuf}; +use std::rc::Rc; use anyhow::{anyhow, bail, Context}; use same_file::is_same_file; @@ -11,11 +12,6 @@ fn main() -> anyhow::Result<()> { return Ok(()); } - // Create a loader for fonts and files. - let mut loader = typst::loading::FsLoader::new(); - loader.search_path("fonts"); - loader.search_system(); - // Determine source and destination path. let src_path = Path::new(&args[1]); let dest_path = if let Some(arg) = args.get(2) { @@ -33,18 +29,19 @@ fn main() -> anyhow::Result<()> { bail!("source and destination files are the same"); } - // Resolve the file id of the source file. - let src_id = loader.resolve_path(src_path).context("source file not found")?; + // Create a loader for fonts and files. + let mut loader = typst::loading::FsLoader::new(); + loader.search_path("fonts"); + loader.search_system(); - // Read the source. + // Resolve the file id of the source file and read the file. + let src_id = loader.resolve_path(src_path).context("source file not found")?; let src = fs::read_to_string(&src_path) .map_err(|_| anyhow!("failed to read source file"))?; - // Compile. - 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_id, &src, &scope, state); + // Typeset. + let mut ctx = typst::Context::new(Rc::new(loader)); + let pass = ctx.typeset(src_id, &src); // Print diagnostics. let map = typst::parse::LineMap::new(&src); @@ -62,7 +59,7 @@ fn main() -> anyhow::Result<()> { } // Export the PDF. - let buffer = typst::export::pdf(&cache, &pass.output); + let buffer = typst::export::pdf(&ctx, &pass.output); fs::write(&dest_path, buffer).context("failed to write PDF file")?; Ok(()) diff --git a/tests/typeset.rs b/tests/typeset.rs index 512e5879c..c73c51f61 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -10,7 +10,6 @@ use tiny_skia as sk; use ttf_parser::{GlyphId, OutlineBuilder}; use walkdir::WalkDir; -use typst::cache::Cache; use typst::color::Color; use typst::diag::{Diag, DiagSet, Level}; use typst::eval::{eval, Scope, Value}; @@ -21,6 +20,7 @@ use typst::layout::{layout, Element, Frame, Geometry, Paint, Text}; use typst::loading::{FileId, FsLoader}; use typst::parse::{parse, LineMap, Scanner}; use typst::syntax::{Location, Pos}; +use typst::Context; const TYP_DIR: &str = "./typ"; const REF_DIR: &str = "./ref"; @@ -57,10 +57,20 @@ fn main() { println!("Running {} tests", len); } - let mut loader = typst::loading::FsLoader::new(); - loader.search_path(FONT_DIR); + // We want to have "unbounded" pages, so we allow them to be infinitely + // large and fit them to match their content. + let mut state = State::default(); + state.page.size = Size::new(Length::pt(120.0), Length::inf()); + state.page.margins = Sides::splat(Some(Length::pt(10.0).into())); - let mut cache = typst::cache::Cache::new(&loader); + // Create a file system loader. + let loader = { + let mut loader = typst::loading::FsLoader::new(); + loader.search_path(FONT_DIR); + Rc::new(loader) + }; + + let mut ctx = typst::Context::builder().state(state).build(loader.clone()); let mut ok = true; for src_path in filtered { @@ -71,8 +81,8 @@ fn main() { args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf")); ok &= test( - &mut loader, - &mut cache, + loader.as_ref(), + &mut ctx, &src_path, &png_path, &ref_path, @@ -125,8 +135,8 @@ struct Panic { } fn test( - loader: &mut FsLoader, - cache: &mut Cache, + loader: &FsLoader, + ctx: &mut Context, src_path: &Path, png_path: &Path, ref_path: &Path, @@ -160,7 +170,7 @@ fn test( } } else { let (part_ok, compare_here, part_frames) = - test_part(loader, cache, src_id, part, i, compare_ref, lines); + test_part(ctx, src_id, part, i, compare_ref, lines); ok &= part_ok; compare_ever |= compare_here; frames.extend(part_frames); @@ -171,12 +181,12 @@ fn test( if compare_ever { if let Some(pdf_path) = pdf_path { - let pdf_data = typst::export::pdf(cache, &frames); + let pdf_data = typst::export::pdf(ctx, &frames); fs::create_dir_all(&pdf_path.parent().unwrap()).unwrap(); fs::write(pdf_path, pdf_data).unwrap(); } - let canvas = draw(&cache, &frames, 2.0); + let canvas = draw(ctx, &frames, 2.0); fs::create_dir_all(&png_path.parent().unwrap()).unwrap(); canvas.save_png(png_path).unwrap(); @@ -199,8 +209,7 @@ fn test( } fn test_part( - loader: &mut FsLoader, - cache: &mut Cache, + ctx: &mut Context, src_id: FileId, src: &str, i: usize, @@ -216,16 +225,10 @@ fn test_part( let panics = Rc::new(RefCell::new(vec![])); register_helpers(&mut scope, Rc::clone(&panics)); - // We want to have "unbounded" pages, so we allow them to be infinitely - // large and fit them to match their content. - let mut state = State::default(); - state.page.size = Size::new(Length::pt(120.0), Length::inf()); - state.page.margins = Sides::splat(Some(Length::pt(10.0).into())); - let parsed = parse(src); - let evaluated = eval(loader, cache, src_id, Rc::new(parsed.output), &scope); - let executed = exec(&evaluated.output.template, state.clone()); - let mut layouted = layout(loader, cache, &executed.output); + let evaluated = eval(ctx, src_id, Rc::new(parsed.output)); + let executed = exec(ctx, &evaluated.output.template); + let mut layouted = layout(ctx, &executed.output); let mut diags = parsed.diags; diags.extend(evaluated.diags); @@ -266,20 +269,20 @@ fn test_part( #[cfg(feature = "layout-cache")] { - let reference_cache = cache.layout.clone(); - for level in 0 .. reference_cache.levels() { - cache.layout = reference_cache.clone(); - cache.layout.retain(|x| x == level); - if cache.layout.frames.is_empty() { + let reference = ctx.layouts.clone(); + for level in 0 .. reference.levels() { + ctx.layouts = reference.clone(); + ctx.layouts.retain(|x| x == level); + if ctx.layouts.frames.is_empty() { continue; } - cache.layout.turnaround(); + ctx.layouts.turnaround(); - let cached_result = layout(loader, cache, &executed.output); + let cached_result = layout(ctx, &executed.output); - let misses = cache - .layout + let misses = ctx + .layouts .frames .iter() .flat_map(|(_, e)| e) @@ -303,8 +306,8 @@ fn test_part( } } - cache.layout = reference_cache; - cache.layout.turnaround(); + ctx.layouts = reference; + ctx.layouts.turnaround(); } if !compare_ref { @@ -391,7 +394,7 @@ fn print_diag(diag: &Diag, map: &LineMap, lines: u32) { println!("{}: {}-{}: {}", diag.level, start, end, diag.message); } -fn draw(cache: &Cache, frames: &[Rc], dpi: f32) -> sk::Pixmap { +fn draw(ctx: &Context, frames: &[Rc], dpi: f32) -> sk::Pixmap { let pad = Length::pt(5.0); let height = pad + frames.iter().map(|l| l.size.height + pad).sum::(); @@ -434,13 +437,13 @@ fn draw(cache: &Cache, frames: &[Rc], dpi: f32) -> sk::Pixmap { let ts = ts.pre_translate(x, y); match *element { Element::Text(ref text) => { - draw_text(&mut canvas, ts, cache, text); + draw_text(&mut canvas, ts, ctx, text); } Element::Geometry(ref geometry, paint) => { draw_geometry(&mut canvas, ts, geometry, paint); } Element::Image(id, size) => { - draw_image(&mut canvas, ts, cache, id, size); + draw_image(&mut canvas, ts, ctx, id, size); } } } @@ -451,8 +454,8 @@ fn draw(cache: &Cache, frames: &[Rc], dpi: f32) -> sk::Pixmap { canvas } -fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, cache: &Cache, text: &Text) { - let ttf = cache.font.get(text.face_id).ttf(); +fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, ctx: &Context, text: &Text) { + let ttf = ctx.fonts.get(text.face_id).ttf(); let mut x = 0.0; for glyph in &text.glyphs { @@ -540,11 +543,11 @@ fn draw_geometry( fn draw_image( canvas: &mut sk::Pixmap, ts: sk::Transform, - cache: &Cache, + ctx: &Context, id: ImageId, size: Size, ) { - let img = cache.image.get(id); + let img = ctx.images.get(id); let mut pixmap = sk::Pixmap::new(img.buf.width(), img.buf.height()).unwrap(); for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {