From 475ca7a62ec99f0b4d8319410b7ee3134a5fcfec Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 27 Nov 2020 22:35:42 +0100 Subject: [PATCH] =?UTF-8?q?Basic=20environment=20and=20resource=20loader?= =?UTF-8?q?=20=F0=9F=8F=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bench/src/bench.rs | 23 +++++++------ src/env.rs | 75 +++++++++++++++++++++++++++++++++++++++++++ src/eval/mod.rs | 12 ++++--- src/export/pdf.rs | 26 ++++++++------- src/font.rs | 6 ---- src/layout/mod.rs | 14 ++++---- src/layout/text.rs | 4 +-- src/lib.rs | 16 ++++----- src/library/insert.rs | 66 +++++++++++++++++-------------------- src/main.rs | 13 ++++---- tests/typeset.rs | 49 +++++++++++++++------------- 11 files changed, 189 insertions(+), 115 deletions(-) create mode 100644 src/env.rs diff --git a/bench/src/bench.rs b/bench/src/bench.rs index bc13ed01c..c232d4bfa 100644 --- a/bench/src/bench.rs +++ b/bench/src/bench.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use criterion::{criterion_group, criterion_main, Criterion}; use fontdock::fs::{FsIndex, FsSource}; +use typst::env::{Env, ResourceLoader}; use typst::eval::{eval, State}; use typst::export::pdf; use typst::font::FontLoader; @@ -25,23 +26,25 @@ fn benchmarks(c: &mut Criterion) { index.search_dir(FONT_DIR); let (files, descriptors) = index.into_vecs(); - let loader = Rc::new(RefCell::new(FontLoader::new( - Box::new(FsSource::new(files)), - descriptors, - ))); + let env = Rc::new(RefCell::new(Env { + fonts: FontLoader::new(Box::new(FsSource::new(files)), descriptors), + resources: ResourceLoader::new(), + })); // Prepare intermediate results and run warm. let state = State::default(); let tree = parse(COMA).output; - let document = eval(&tree, state.clone()).output; - let layouts = layout(&document, Rc::clone(&loader)); + let document = eval(&tree, Rc::clone(&env), state.clone()).output; + let layouts = layout(&document, Rc::clone(&env)); // Bench! bench!("parse-coma": parse(COMA)); - bench!("eval-coma": eval(&tree, state.clone())); - bench!("layout-coma": layout(&document, Rc::clone(&loader))); - bench!("typeset-coma": typeset(COMA, state.clone(), Rc::clone(&loader))); - bench!("export-pdf-coma": pdf::export(&layouts, &loader.borrow())); + bench!("eval-coma": eval(&tree, Rc::clone(&env), state.clone())); + bench!("layout-coma": layout(&document, Rc::clone(&env))); + bench!("typeset-coma": typeset(COMA, Rc::clone(&env), state.clone())); + + let env = env.borrow(); + bench!("export-pdf-coma": pdf::export(&layouts, &env)); } criterion_group!(benches, benchmarks); diff --git a/src/env.rs b/src/env.rs new file mode 100644 index 000000000..eba0e59b9 --- /dev/null +++ b/src/env.rs @@ -0,0 +1,75 @@ +//! Environment interactions. + +use std::any::Any; +use std::cell::RefCell; +use std::collections::{hash_map::Entry, HashMap}; +use std::fmt::{self, Debug, Formatter}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +use crate::font::FontLoader; + +/// A reference-counted shared environment. +pub type SharedEnv = Rc>; + +/// Encapsulates all environment dependencies (fonts, resources). +#[derive(Debug)] +pub struct Env { + /// Loads fonts from a dynamic font source. + pub fonts: FontLoader, + /// Loads resource from the file system. + pub resources: ResourceLoader, +} + +/// Loads resource from the file system. +pub struct ResourceLoader { + paths: HashMap, + entries: Vec>, +} + +/// A unique identifier for a resource. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct ResourceId(usize); + +impl ResourceLoader { + /// Create a new resource loader. + pub fn new() -> Self { + Self { paths: HashMap::new(), entries: vec![] } + } + + /// Load a resource from a path. + pub fn load( + &mut self, + path: impl AsRef, + parse: impl FnOnce(Vec) -> Option, + ) -> Option<(ResourceId, &R)> { + let path = path.as_ref(); + let id = match self.paths.entry(path.to_owned()) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let id = *entry.insert(ResourceId(self.entries.len())); + let data = fs::read(path).ok()?; + let resource = parse(data)?; + self.entries.push(Box::new(resource)); + id + } + }; + + Some((id, self.get_loaded(id))) + } + + /// Retrieve a previously loaded resource by its id. + /// + /// # Panics + /// This panics if no resource with this id was loaded. + pub fn get_loaded(&self, id: ResourceId) -> &R { + self.entries[id.0].downcast_ref().expect("bad resource type") + } +} + +impl Debug for ResourceLoader { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_set().entries(self.paths.keys()).finish() + } +} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index c45e46aeb..4cfebd3ee 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -22,6 +22,7 @@ use fontdock::FontStyle; use crate::diag::Diag; use crate::diag::{Deco, Feedback, Pass}; +use crate::env::SharedEnv; use crate::geom::{BoxAlign, Dir, Flow, Gen, Length, Linear, Relative, Sides, Size}; use crate::layout::{ Document, Expansion, LayoutNode, Pad, Pages, Par, Softness, Spacing, Stack, Text, @@ -30,10 +31,10 @@ use crate::syntax::*; /// Evaluate a syntax tree into a document. /// -/// The given `state` the base state that may be updated over the course of +/// The given `state` is the base state that may be updated over the course of /// evaluation. -pub fn eval(tree: &SynTree, state: State) -> Pass { - let mut ctx = EvalContext::new(state); +pub fn eval(tree: &SynTree, env: SharedEnv, state: State) -> Pass { + let mut ctx = EvalContext::new(env, state); ctx.start_page_group(false); tree.eval(&mut ctx); ctx.end_page_group(); @@ -43,6 +44,8 @@ pub fn eval(tree: &SynTree, state: State) -> Pass { /// The context for evaluation. #[derive(Debug)] pub struct EvalContext { + /// The environment from which resources are gathered. + pub env: SharedEnv, /// The active evaluation state. pub state: State, /// The accumulated feedback. @@ -62,8 +65,9 @@ pub struct EvalContext { impl EvalContext { /// Create a new evaluation context with a base state. - pub fn new(state: State) -> Self { + pub fn new(env: SharedEnv, state: State) -> Self { Self { + env, state, groups: vec![], inner: vec![], diff --git a/src/export/pdf.rs b/src/export/pdf.rs index af3ca0a68..43237dc7a 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -3,14 +3,14 @@ use std::collections::HashMap; use fontdock::FaceId; -use image::RgbImage; +use image::{DynamicImage, GenericImageView}; use pdf_writer::{ CidFontType, ColorSpace, Content, FontFlags, Name, PdfWriter, Rect, Ref, Str, SystemInfo, UnicodeCmap, }; use ttf_parser::{name_id, GlyphId}; -use crate::font::FontLoader; +use crate::env::{Env, ResourceId}; use crate::geom::Length; use crate::layout::{BoxLayout, LayoutElement}; @@ -21,14 +21,14 @@ use crate::layout::{BoxLayout, LayoutElement}; /// included in the _PDF_. /// /// Returns the raw bytes making up the _PDF_ document. -pub fn export(layouts: &[BoxLayout], loader: &FontLoader) -> Vec { - PdfExporter::new(layouts, loader).write() +pub fn export(layouts: &[BoxLayout], env: &Env) -> Vec { + PdfExporter::new(layouts, env).write() } struct PdfExporter<'a> { writer: PdfWriter, layouts: &'a [BoxLayout], - loader: &'a FontLoader, + env: &'a Env, /// We need to know exactly which indirect reference id will be used for /// which objects up-front to correctly declare the document catalogue, page /// tree and so on. These offsets are computed in the beginning and stored @@ -41,13 +41,13 @@ struct PdfExporter<'a> { /// Backwards from the pdf indices to the old face ids. fonts_to_layout: Vec, /// The already visited images. - images: Vec<&'a RgbImage>, + images: Vec, /// The total number of images. image_count: usize, } impl<'a> PdfExporter<'a> { - fn new(layouts: &'a [BoxLayout], loader: &'a FontLoader) -> Self { + fn new(layouts: &'a [BoxLayout], env: &'a Env) -> Self { let mut writer = PdfWriter::new(1, 7); writer.set_indent(2); @@ -75,7 +75,7 @@ impl<'a> PdfExporter<'a> { Self { writer, layouts, - loader, + env, refs, fonts_to_pdf, fonts_to_layout, @@ -185,7 +185,7 @@ impl<'a> PdfExporter<'a> { content.x_object(Name(name.as_bytes())); content.restore_state(); - self.images.push(&image.buf); + self.images.push(image.resource); } } @@ -194,7 +194,7 @@ impl<'a> PdfExporter<'a> { fn write_fonts(&mut self) { for (refs, &face_id) in self.refs.fonts().zip(&self.fonts_to_layout) { - let owned_face = self.loader.get_loaded(face_id); + let owned_face = self.env.fonts.get_loaded(face_id); let face = owned_face.get(); let name = face @@ -302,9 +302,11 @@ impl<'a> PdfExporter<'a> { } fn write_images(&mut self) { - for (id, image) in self.refs.images().zip(&self.images) { + for (id, &resource) in self.refs.images().zip(&self.images) { + let image = self.env.resources.get_loaded::(resource); + let data = image.to_rgb8().into_raw(); self.writer - .image_stream(id, &image.as_raw()) + .image_stream(id, &data) .width(image.width() as i32) .height(image.height() as i32) .color_space(ColorSpace::DeviceRGB) diff --git a/src/font.rs b/src/font.rs index 513c31f15..68a2db677 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,14 +1,8 @@ //! Font handling. -use std::cell::RefCell; -use std::rc::Rc; - use fontdock::{ContainsChar, FaceFromVec, FontSource}; use ttf_parser::Face; -/// A reference-counted shared font loader backed by a dynamic font source. -pub type SharedFontLoader = Rc>; - /// A font loader backed by a dynamic source. pub type FontLoader = fontdock::FontLoader>; diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 28b278992..1f7d6c9f6 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -8,9 +8,7 @@ mod spacing; mod stack; mod text; -use image::RgbImage; - -use crate::font::SharedFontLoader; +use crate::env::{ResourceId, SharedEnv}; use crate::geom::*; use crate::shaping::Shaped; @@ -23,16 +21,16 @@ pub use stack::*; pub use text::*; /// Layout a document and return the produced layouts. -pub fn layout(document: &Document, loader: SharedFontLoader) -> Vec { - let mut ctx = LayoutContext { loader }; +pub fn layout(document: &Document, env: SharedEnv) -> Vec { + let mut ctx = LayoutContext { env }; document.layout(&mut ctx) } /// The context for layouting. #[derive(Debug, Clone)] pub struct LayoutContext { - /// The font loader to query fonts from when typesetting text. - pub loader: SharedFontLoader, + /// The environment from which fonts are gathered. + pub env: SharedEnv, } /// Layout a node. @@ -185,7 +183,7 @@ pub enum LayoutElement { #[derive(Debug, Clone, PartialEq)] pub struct ImageElement { /// The image. - pub buf: RgbImage, + pub resource: ResourceId, /// The document size of the image. pub size: Size, } diff --git a/src/layout/text.rs b/src/layout/text.rs index fc319fa56..56b2328e0 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -25,10 +25,10 @@ pub struct Text { impl Layout for Text { fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Layouted { - let mut loader = ctx.loader.borrow_mut(); + let mut env = ctx.env.borrow_mut(); Layouted::Layout( shaping::shape( - &mut loader, + &mut env.fonts, &self.text, self.dir, self.font_size, diff --git a/src/lib.rs b/src/lib.rs index 7d54da4cd..d471b09d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ pub mod diag; #[macro_use] pub mod eval; pub mod color; +pub mod env; pub mod export; pub mod font; pub mod geom; @@ -40,19 +41,18 @@ pub mod prelude; pub mod shaping; pub mod syntax; +use std::rc::Rc; + use crate::diag::{Feedback, Pass}; +use crate::env::SharedEnv; use crate::eval::State; -use crate::font::SharedFontLoader; use crate::layout::BoxLayout; /// Process _Typst_ source code directly into a collection of layouts. -pub fn typeset( - src: &str, - state: State, - loader: SharedFontLoader, -) -> Pass> { +pub fn typeset(src: &str, env: SharedEnv, state: State) -> Pass> { let Pass { output: tree, feedback: f1 } = parse::parse(src); - let Pass { output: document, feedback: f2 } = eval::eval(&tree, state); - let layouts = layout::layout(&document, loader); + let Pass { output: document, feedback: f2 } = + eval::eval(&tree, Rc::clone(&env), state); + let layouts = layout::layout(&document, env); Pass::new(layouts, Feedback::join(f1, f2)) } diff --git a/src/library/insert.rs b/src/library/insert.rs index 2904c9584..b2cdc255d 100644 --- a/src/library/insert.rs +++ b/src/library/insert.rs @@ -1,10 +1,9 @@ -use std::fmt::{self, Debug, Formatter}; -use std::fs::File; -use std::io::BufReader; +use std::io::Cursor; use image::io::Reader; -use image::RgbImage; +use image::GenericImageView; +use crate::env::ResourceId; use crate::layout::*; use crate::prelude::*; @@ -20,25 +19,27 @@ pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value { let height = args.get::<_, Linear>(ctx, "height"); if let Some(path) = path { - if let Ok(file) = File::open(path.v) { - match Reader::new(BufReader::new(file)) + let mut env = ctx.env.borrow_mut(); + let loaded = env.resources.load(path.v, |data| { + Reader::new(Cursor::new(data)) .with_guessed_format() - .map_err(|err| err.into()) - .and_then(|reader| reader.decode()) - .map(|img| img.into_rgb8()) - { - Ok(buf) => { - ctx.push(Image { - buf, - width, - height, - align: ctx.state.align, - }); - } - Err(err) => ctx.diag(error!(path.span, "invalid image: {}", err)), - } + .ok() + .and_then(|reader| reader.decode().ok()) + }); + + if let Some((resource, buf)) = loaded { + let dimensions = buf.dimensions(); + drop(env); + ctx.push(Image { + resource, + dimensions, + width, + height, + align: ctx.state.align, + }); } else { - ctx.diag(error!(path.span, "failed to open image file")); + drop(env); + ctx.diag(error!(path.span, "failed to load image")); } } @@ -46,10 +47,12 @@ pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value { } /// An image node. -#[derive(Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq)] struct Image { - /// The image. - buf: RgbImage, + /// The resource id of the image file. + resource: ResourceId, + /// The pixel dimensions of the image. + dimensions: (u32, u32), /// The fixed width, if any. width: Option, /// The fixed height, if any. @@ -61,8 +64,7 @@ struct Image { impl Layout for Image { fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Layouted { let Area { rem, full } = areas.current; - let (pixel_width, pixel_height) = self.buf.dimensions(); - let pixel_ratio = (pixel_width as f64) / (pixel_height as f64); + let pixel_ratio = (self.dimensions.0 as f64) / (self.dimensions.1 as f64); let width = self.width.map(|w| w.resolve(full.width)); let height = self.height.map(|w| w.resolve(full.height)); @@ -85,23 +87,13 @@ impl Layout for Image { let mut boxed = BoxLayout::new(size); boxed.push( Point::ZERO, - LayoutElement::Image(ImageElement { buf: self.buf.clone(), size }), + LayoutElement::Image(ImageElement { resource: self.resource, size }), ); Layouted::Layout(boxed, self.align) } } -impl Debug for Image { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Image") - .field("width", &self.width) - .field("height", &self.height) - .field("align", &self.align) - .finish() - } -} - impl From for LayoutNode { fn from(image: Image) -> Self { Self::dynamic(image) diff --git a/src/main.rs b/src/main.rs index 715ee59fa..3f12655bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use anyhow::{anyhow, bail, Context}; use fontdock::fs::{FsIndex, FsSource}; use typst::diag::{Feedback, Pass}; +use typst::env::{Env, ResourceLoader}; use typst::eval::State; use typst::export::pdf; use typst::font::FontLoader; @@ -41,16 +42,16 @@ fn main() -> anyhow::Result<()> { index.search_os(); let (files, descriptors) = index.into_vecs(); - let loader = Rc::new(RefCell::new(FontLoader::new( - Box::new(FsSource::new(files)), - descriptors, - ))); + let env = Rc::new(RefCell::new(Env { + fonts: FontLoader::new(Box::new(FsSource::new(files)), descriptors), + resources: ResourceLoader::new(), + })); let state = State::default(); let Pass { output: layouts, feedback: Feedback { mut diags, .. }, - } = typeset(&src, state, Rc::clone(&loader)); + } = typeset(&src, Rc::clone(&env), state); if !diags.is_empty() { diags.sort(); @@ -71,7 +72,7 @@ fn main() -> anyhow::Result<()> { } } - let pdf_data = pdf::export(&layouts, &loader.borrow()); + let pdf_data = pdf::export(&layouts, &env.borrow()); fs::write(&dest_path, pdf_data).context("Failed to write PDF file.")?; Ok(()) diff --git a/tests/typeset.rs b/tests/typeset.rs index d9a1056a0..82d801a24 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -6,6 +6,7 @@ use std::path::Path; use std::rc::Rc; use fontdock::fs::{FsIndex, FsSource}; +use image::{DynamicImage, GenericImageView, Rgba}; use memmap::Mmap; use tiny_skia::{ Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, PathBuilder, Pattern, Pixmap, @@ -14,9 +15,10 @@ use tiny_skia::{ use ttf_parser::OutlineBuilder; use typst::diag::{Feedback, Pass}; +use typst::env::{Env, ResourceLoader, SharedEnv}; use typst::eval::State; use typst::export::pdf; -use typst::font::{FontLoader, SharedFontLoader}; +use typst::font::FontLoader; use typst::geom::{Length, Point}; use typst::layout::{BoxLayout, ImageElement, LayoutElement}; use typst::parse::LineMap; @@ -67,16 +69,16 @@ fn main() { index.search_dir(FONT_DIR); let (files, descriptors) = index.into_vecs(); - let loader = Rc::new(RefCell::new(FontLoader::new( - Box::new(FsSource::new(files)), - descriptors, - ))); + let env = Rc::new(RefCell::new(Env { + fonts: FontLoader::new(Box::new(FsSource::new(files)), descriptors), + resources: ResourceLoader::new(), + })); let mut ok = true; for (name, src_path, pdf_path, png_path, ref_path) in filtered { print!("Testing {}.", name); - test(&src_path, &pdf_path, &png_path, &loader); + test(&src_path, &pdf_path, &png_path, &env); let png_file = File::open(&png_path).unwrap(); let ref_file = match File::open(&ref_path) { @@ -104,13 +106,13 @@ fn main() { } } -fn test(src_path: &Path, pdf_path: &Path, png_path: &Path, loader: &SharedFontLoader) { +fn test(src_path: &Path, pdf_path: &Path, png_path: &Path, env: &SharedEnv) { let src = fs::read_to_string(src_path).unwrap(); let state = State::default(); let Pass { output: layouts, feedback: Feedback { mut diags, .. }, - } = typeset(&src, state, Rc::clone(loader)); + } = typeset(&src, Rc::clone(env), state); if !diags.is_empty() { diags.sort(); @@ -131,12 +133,11 @@ fn test(src_path: &Path, pdf_path: &Path, png_path: &Path, loader: &SharedFontLo } } - let loader = loader.borrow(); - - let canvas = draw(&layouts, &loader, 2.0); + let env = env.borrow(); + let canvas = draw(&layouts, &env, 2.0); canvas.pixmap.save_png(png_path).unwrap(); - let pdf_data = pdf::export(&layouts, &loader); + let pdf_data = pdf::export(&layouts, &env); fs::write(pdf_path, pdf_data).unwrap(); } @@ -170,7 +171,7 @@ impl TestFilter { } } -fn draw(layouts: &[BoxLayout], loader: &FontLoader, pixel_per_pt: f32) -> Canvas { +fn draw(layouts: &[BoxLayout], env: &Env, pixel_per_pt: f32) -> Canvas { let pad = Length::pt(5.0); let height = pad + layouts.iter().map(|l| l.size.height + pad).sum::(); @@ -207,9 +208,11 @@ fn draw(layouts: &[BoxLayout], loader: &FontLoader, pixel_per_pt: f32) -> Canvas let pos = origin + pos; match element { LayoutElement::Text(shaped) => { - draw_text(&mut canvas, loader, shaped, pos) + draw_text(&mut canvas, pos, env, shaped); + } + LayoutElement::Image(image) => { + draw_image(&mut canvas, pos, env, image); } - LayoutElement::Image(image) => draw_image(&mut canvas, image, pos), } } @@ -219,8 +222,8 @@ fn draw(layouts: &[BoxLayout], loader: &FontLoader, pixel_per_pt: f32) -> Canvas canvas } -fn draw_text(canvas: &mut Canvas, loader: &FontLoader, shaped: &Shaped, pos: Point) { - let face = loader.get_loaded(shaped.face).get(); +fn draw_text(canvas: &mut Canvas, pos: Point, env: &Env, shaped: &Shaped) { + let face = env.fonts.get_loaded(shaped.face).get(); for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) { let units_per_em = face.units_per_em().unwrap_or(1000); @@ -244,11 +247,13 @@ fn draw_text(canvas: &mut Canvas, loader: &FontLoader, shaped: &Shaped, pos: Poi } } -fn draw_image(canvas: &mut Canvas, image: &ImageElement, pos: Point) { - let mut pixmap = Pixmap::new(image.buf.width(), image.buf.height()).unwrap(); - for (src, dest) in image.buf.pixels().zip(pixmap.pixels_mut()) { - let [r, g, b] = src.0; - *dest = ColorU8::from_rgba(r, g, b, 255).premultiply(); +fn draw_image(canvas: &mut Canvas, pos: Point, env: &Env, image: &ImageElement) { + let buf = env.resources.get_loaded::(image.resource); + + let mut pixmap = Pixmap::new(buf.width(), buf.height()).unwrap(); + for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) { + let Rgba([r, g, b, a]) = src; + *dest = ColorU8::from_rgba(r, g, b, a).premultiply(); } let view_width = image.size.width.to_pt() as f32;