From e65c2b949c61fde471e03881359a2946845b554f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 14 May 2021 11:14:28 +0200 Subject: [PATCH] Remove resource abstraction and handle images natively --- src/env/fs.rs | 6 +- src/env/image.rs | 33 ++++++----- src/env/mod.rs | 122 ++++++++++++++++++++++++--------------- src/layout/background.rs | 2 +- src/layout/frame.rs | 30 ++-------- src/layout/shaping.rs | 2 +- src/library/image.rs | 15 +++-- src/pdf/mod.rs | 90 ++++++++++++++--------------- tests/typeset.rs | 50 +++++++++------- 9 files changed, 184 insertions(+), 166 deletions(-) diff --git a/src/env/fs.rs b/src/env/fs.rs index 5f18191bb..4b77f1095 100644 --- a/src/env/fs.rs +++ b/src/env/fs.rs @@ -12,7 +12,7 @@ use walkdir::WalkDir; use super::{Buffer, Loader}; use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight}; -/// Loads fonts and resources from the local file system. +/// Loads fonts and images from the local file system. /// /// _This is only available when the `fs` feature is enabled._ #[derive(Default, Debug, Clone, Serialize, Deserialize)] @@ -175,8 +175,8 @@ impl Loader for FsLoader { load(&mut self.cache, &self.files[idx]) } - fn load_file(&mut self, url: &str) -> Option { - load(&mut self.cache, Path::new(url)) + fn load_file(&mut self, path: &str) -> Option { + load(&mut self.cache, Path::new(path)) } } diff --git a/src/env/image.rs b/src/env/image.rs index 4bdb54837..365ff312c 100644 --- a/src/env/image.rs +++ b/src/env/image.rs @@ -4,37 +4,44 @@ use std::io::Cursor; use image::io::Reader as ImageReader; use image::{DynamicImage, GenericImageView, ImageFormat}; -use super::Buffer; - -/// A loaded image resource. -pub struct ImageResource { +/// A loaded image. +pub struct Image { /// The original format the image was encoded in. pub format: ImageFormat, /// The decoded image. pub buf: DynamicImage, } -impl ImageResource { - /// Parse an image resource from raw data in a supported format. +impl Image { + /// Parse an image from raw data in a supported format. /// /// The image format is determined automatically. - pub fn parse(data: Buffer) -> Option { - let cursor = Cursor::new(data.as_ref()); + 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 ImageResource { +impl Debug for Image { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let (width, height) = self.buf.dimensions(); - f.debug_struct("ImageResource") + f.debug_struct("Image") .field("format", &self.format) .field("color", &self.buf.color()) - .field("width", &width) - .field("height", &height) + .field("width", &self.width()) + .field("height", &self.height()) .finish() } } diff --git a/src/env/mod.rs b/src/env/mod.rs index c8ba46ec3..84be3e813 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -1,4 +1,4 @@ -//! Font and resource loading. +//! Font and image loading. #[cfg(feature = "fs")] mod fs; @@ -8,7 +8,6 @@ pub use self::image::*; #[cfg(feature = "fs")] pub use fs::*; -use std::any::Any; use std::collections::{hash_map::Entry, HashMap}; use std::rc::Rc; @@ -16,18 +15,22 @@ use serde::{Deserialize, Serialize}; use crate::font::{Face, FaceInfo, FontVariant}; -/// Handles font and resource loading. +/// Handles font and image loading. pub struct Env { /// The loader that serves the font face and file buffers. loader: Box, - /// Loaded resources indexed by [`ResourceId`]. - resources: Vec>, - /// Maps from URL to loaded resource. - urls: HashMap, /// Faces indexed by [`FaceId`]. `None` if not yet loaded. faces: Vec>, /// Maps a family name to the ids of all faces that are part of the family. families: HashMap>, + /// 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 { @@ -49,10 +52,12 @@ impl Env { Self { loader: Box::new(loader), - resources: vec![], - urls: HashMap::new(), faces, families, + images: vec![], + paths: HashMap::new(), + on_face_load: None, + on_image_load: None, } } @@ -113,41 +118,24 @@ impl Env { } // Load the face if it's not already loaded. - let idx = best?.0 as usize; + 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 } - /// Load a file from a local or remote URL, parse it into a cached resource - /// and return a unique identifier that allows to retrieve the parsed - /// resource through [`resource()`](Self::resource). - pub fn load_resource(&mut self, url: &str, parse: F) -> Option - where - F: FnOnce(Buffer) -> Option, - R: 'static, - { - Some(match self.urls.entry(url.to_string()) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - let buffer = self.loader.load_file(url)?; - let resource = parse(buffer)?; - let len = self.resources.len(); - self.resources.push(Box::new(resource)); - *entry.insert(ResourceId(len as u32)) - } - }) - } - /// Get a reference to a queried face. /// - /// # Panics /// This panics if no face with this id was loaded. This function should /// only be called with ids returned by [`query_face()`](Self::query_face). #[track_caller] @@ -155,20 +143,50 @@ impl Env { self.faces[id.0 as usize].as_ref().expect("font face was not loaded") } - /// Get a reference to a loaded resource. + /// 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 resource with this id was loaded. This function should - /// only be called with ids returned by - /// [`load_resource()`](Self::load_resource). + /// 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 resource(&self, id: ResourceId) -> &R { - self.resources[id.0 as usize] - .downcast_ref() - .expect("bad resource type") + 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 resources from a remote or local source. +/// 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]; @@ -176,8 +194,8 @@ pub trait Loader { /// Load the font face with the given index in [`faces()`](Self::faces). fn load_face(&mut self, idx: usize) -> Option; - /// Load a file from a URL. - fn load_file(&mut self, url: &str) -> Option; + /// Load a file from a path. + fn load_file(&mut self, path: &str) -> Option; } /// A shared byte buffer. @@ -205,11 +223,21 @@ impl FaceId { } } -/// A unique identifier for a loaded resource. +/// A unique identifier for a loaded image. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct ResourceId(u32); +pub struct ImageId(u32); -impl ResourceId { - /// A blank initialization value. - pub const MAX: Self = Self(u32::MAX); +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/background.rs b/src/layout/background.rs index 515eef718..33a9ce755 100644 --- a/src/layout/background.rs +++ b/src/layout/background.rs @@ -30,7 +30,7 @@ impl Layout for BackgroundNode { } }; - let element = Element::Geometry(Geometry { shape, fill: self.fill }); + let element = Element::Geometry(shape, self.fill); frame.elements.insert(0, (point, element)); } diff --git a/src/layout/frame.rs b/src/layout/frame.rs index 9890e33f1..ea0f6aa89 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -1,5 +1,5 @@ use crate::color::Color; -use crate::env::{FaceId, ResourceId}; +use crate::env::{FaceId, ImageId}; use crate::geom::{Length, Path, Point, Size}; use serde::{Deserialize, Serialize}; @@ -41,9 +41,9 @@ pub enum Element { /// Shaped text. Text(Text), /// A geometric shape. - Geometry(Geometry), + Geometry(Shape, Fill), /// A raster image. - Image(Image), + Image(ImageId, Size), } /// A run of shaped text. @@ -54,7 +54,7 @@ pub struct Text { /// The font size. pub size: Length, /// The glyph fill color / texture. - pub color: Fill, + pub fill: Fill, /// The glyphs. pub glyphs: Vec, } @@ -83,19 +83,6 @@ impl Text { } } -/// A shape with some kind of fill. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Geometry { - /// The shape to draw. - pub shape: Shape, - /// How the shape looks on the inside. - // - // TODO: This could be made into a Vec or something such that - // the user can compose multiple fills with alpha values less - // than one to achieve cool effects. - pub fill: Fill, -} - /// Some shape. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Shape { @@ -113,12 +100,3 @@ pub enum Fill { /// The fill is a color. Color(Color), } - -/// An image element. -#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] -pub struct Image { - /// The image resource. - pub id: ResourceId, - /// The size of the image in the document. - pub size: Size, -} diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index e6e28a4ad..8a4072609 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -70,7 +70,7 @@ impl<'a> ShapedText<'a> { let mut text = Text { face_id, size: self.props.size, - color: self.props.color, + fill: self.props.color, glyphs: vec![], }; diff --git a/src/library/image.rs b/src/library/image.rs index 134590bb0..a7388abb6 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -1,8 +1,8 @@ use ::image::GenericImageView; use super::*; -use crate::env::{ImageResource, ResourceId}; -use crate::layout::{AnyNode, Areas, Element, Frame, Image, Layout, LayoutContext}; +use crate::env::ImageId; +use crate::layout::{AnyNode, Areas, Element, Frame, Layout, LayoutContext}; /// `image`: An image. /// @@ -20,9 +20,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { Value::template("image", move |ctx| { if let Some(path) = &path { - let loaded = ctx.env.load_resource(&path.v, ImageResource::parse); - if let Some(id) = loaded { - let img = ctx.env.resource::(id); + 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 { @@ -35,8 +34,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { /// An image node. #[derive(Debug, Clone, PartialEq)] struct ImageNode { - /// The resource id of the image file. - id: ResourceId, + /// The id of the image file. + id: ImageId, /// The pixel dimensions of the image. dimensions: (u32, u32), /// The fixed width, if any. @@ -75,7 +74,7 @@ impl Layout for ImageNode { }; let mut frame = Frame::new(size, size.height); - frame.push(Point::ZERO, Element::Image(Image { id: self.id, size })); + frame.push(Point::ZERO, Element::Image(self.id, size)); vec![frame] } diff --git a/src/pdf/mod.rs b/src/pdf/mod.rs index 82acbbaa0..2b32f1011 100644 --- a/src/pdf/mod.rs +++ b/src/pdf/mod.rs @@ -13,10 +13,10 @@ use pdf_writer::{ use ttf_parser::{name_id, GlyphId}; use crate::color::Color; -use crate::env::{Env, FaceId, ImageResource, ResourceId}; +use crate::env::{Env, FaceId, Image, ImageId}; use crate::font::{Em, VerticalFontMetric}; use crate::geom::{self, Length, Size}; -use crate::layout::{Element, Fill, Frame, Image, Shape}; +use crate::layout::{Element, Fill, Frame, Shape}; /// Export a collection of frames into a PDF document. /// @@ -35,7 +35,7 @@ struct PdfExporter<'a> { env: &'a Env, refs: Refs, fonts: Remapper, - images: Remapper, + images: Remapper, } impl<'a> PdfExporter<'a> { @@ -49,16 +49,16 @@ impl<'a> PdfExporter<'a> { for frame in frames { for (_, element) in &frame.elements { - match element { - Element::Text(shaped) => fonts.insert(shaped.face_id), - Element::Image(image) => { - let img = env.resource::(image.id); + match *element { + Element::Text(ref shaped) => fonts.insert(shaped.face_id), + Element::Geometry(_, _) => {} + Element::Image(id, _) => { + let img = env.image(id); if img.buf.color().has_alpha() { alpha_masks += 1; } - images.insert(image.id); + images.insert(id); } - Element::Geometry(_) => {} } } } @@ -139,23 +139,35 @@ impl<'a> PdfExporter<'a> { let x = pos.x.to_pt() as f32; let y = (page.size.height - pos.y).to_pt() as f32; - match element { - &Element::Image(Image { id, size: Size { width, height } }) => { - let name = format!("Im{}", self.images.map(id)); - let w = width.to_pt() as f32; - let h = height.to_pt() as f32; + match *element { + Element::Text(ref shaped) => { + if fill != Some(shaped.fill) { + write_fill(&mut content, shaped.fill); + fill = Some(shaped.fill); + } - content.save_state(); - content.matrix(w, 0.0, 0.0, h, x, y - h); - content.x_object(Name(name.as_bytes())); - content.restore_state(); + let mut text = content.text(); + + // Then, also check if we need to issue a font switching + // action. + if shaped.face_id != face || shaped.size != size { + face = shaped.face_id; + size = shaped.size; + + let name = format!("F{}", self.fonts.map(shaped.face_id)); + text.font(Name(name.as_bytes()), size.to_pt() as f32); + } + + // TODO: Respect individual glyph offsets. + text.matrix(1.0, 0.0, 0.0, 1.0, x, y); + text.show(&shaped.encode_glyphs_be()); } - Element::Geometry(geometry) => { + Element::Geometry(ref shape, fill) => { content.save_state(); - write_fill(&mut content, geometry.fill); + write_fill(&mut content, fill); - match geometry.shape { + match *shape { Shape::Rect(Size { width, height }) => { let w = width.to_pt() as f32; let h = height.to_pt() as f32; @@ -177,27 +189,15 @@ impl<'a> PdfExporter<'a> { content.restore_state(); } - Element::Text(shaped) => { - if fill != Some(shaped.color) { - write_fill(&mut content, shaped.color); - fill = Some(shaped.color); - } + Element::Image(id, Size { width, height }) => { + let name = format!("Im{}", self.images.map(id)); + let w = width.to_pt() as f32; + let h = height.to_pt() as f32; - let mut text = content.text(); - - // Then, also check if we need to issue a font switching - // action. - if shaped.face_id != face || shaped.size != size { - face = shaped.face_id; - size = shaped.size; - - let name = format!("F{}", self.fonts.map(shaped.face_id)); - text.font(Name(name.as_bytes()), size.to_pt() as f32); - } - - // TODO: Respect individual glyph offsets. - text.matrix(1.0, 0.0, 0.0, 1.0, x, y); - text.show(&shaped.encode_glyphs_be()); + content.save_state(); + content.matrix(w, 0.0, 0.0, h, x, y - h); + content.x_object(Name(name.as_bytes())); + content.restore_state(); } } } @@ -311,8 +311,8 @@ impl<'a> PdfExporter<'a> { fn write_images(&mut self) { let mut masks_seen = 0; - for (id, resource) in self.refs.images().zip(self.images.layout_indices()) { - let img = self.env.resource::(resource); + for (id, image_id) in self.refs.images().zip(self.images.layout_indices()) { + let img = self.env.image(image_id); let (width, height) = img.buf.dimensions(); // Add the primary image. @@ -397,7 +397,7 @@ const DEFLATE_LEVEL: u8 = 6; /// Encode an image with a suitable filter. /// /// Skips the alpha channel as that's encoded separately. -fn encode_image(img: &ImageResource) -> ImageResult<(Vec, Filter, ColorSpace)> { +fn encode_image(img: &Image) -> ImageResult<(Vec, Filter, ColorSpace)> { let mut data = vec![]; let (filter, space) = match (img.format, &img.buf) { // 8-bit gray JPEG. @@ -438,7 +438,7 @@ fn encode_image(img: &ImageResource) -> ImageResult<(Vec, Filter, ColorSpace } /// Encode an image's alpha channel if present. -fn encode_alpha(img: &ImageResource) -> (Vec, Filter) { +fn encode_alpha(img: &Image) -> (Vec, Filter) { let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL); (data, Filter::FlateDecode) diff --git a/tests/typeset.rs b/tests/typeset.rs index 5e48494d8..4572e81b2 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -15,11 +15,11 @@ use walkdir::WalkDir; use typst::color; use typst::diag::{Diag, DiagSet, Level, Pass}; -use typst::env::{Env, FsLoader, ImageResource}; +use typst::env::{Env, FsLoader, ImageId}; use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value}; use typst::exec::State; use typst::geom::{self, Length, Point, Sides, Size}; -use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Text}; +use typst::layout::{Element, Fill, Frame, Shape, Text}; use typst::library; use typst::parse::{LineMap, Scanner}; use typst::pdf; @@ -385,15 +385,21 @@ fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap { None, ); - for &(pos, ref element) in &frame.elements { - let pos = origin + pos; - let x = pos.x.to_pt() as f32; - let y = pos.y.to_pt() as f32; + for (pos, element) in &frame.elements { + let global = origin + *pos; + let x = global.x.to_pt() as f32; + let y = global.y.to_pt() as f32; let ts = ts.pre_translate(x, y); - match element { - Element::Text(shaped) => draw_text(&mut canvas, env, ts, shaped), - Element::Image(image) => draw_image(&mut canvas, env, ts, image), - Element::Geometry(geom) => draw_geometry(&mut canvas, ts, geom), + match *element { + Element::Text(ref text) => { + draw_text(&mut canvas, env, 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); + } } } @@ -403,13 +409,13 @@ fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap { canvas } -fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) { - let ttf = env.face(shaped.face_id).ttf(); +fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, text: &Text) { + let ttf = env.face(text.face_id).ttf(); let mut x = 0.0; - for glyph in &shaped.glyphs { + for glyph in &text.glyphs { let units_per_em = ttf.units_per_em(); - let s = shaped.size.to_pt() as f32 / units_per_em as f32; + let s = text.size.to_pt() as f32 / units_per_em as f32; let dx = glyph.x_offset.to_pt() as f32; let ts = ts.pre_translate(x + dx, 0.0); @@ -441,7 +447,7 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) { if ttf.outline_glyph(GlyphId(glyph.id), &mut builder).is_some() { let path = builder.0.finish().unwrap(); let ts = ts.pre_scale(s, -s); - let mut paint = convert_typst_fill(shaped.color); + let mut paint = convert_typst_fill(text.fill); paint.anti_alias = true; canvas.fill_path(&path, &paint, FillRule::default(), ts, None); } @@ -451,11 +457,11 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) { } } -fn draw_geometry(canvas: &mut Pixmap, ts: Transform, element: &Geometry) { - let paint = convert_typst_fill(element.fill); +fn draw_geometry(canvas: &mut Pixmap, ts: Transform, shape: &Shape, fill: Fill) { + let paint = convert_typst_fill(fill); let rule = FillRule::default(); - match element.shape { + match *shape { Shape::Rect(Size { width, height }) => { let w = width.to_pt() as f32; let h = height.to_pt() as f32; @@ -473,8 +479,8 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, element: &Geometry) { }; } -fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) { - let img = &env.resource::(element.id); +fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, id: ImageId, size: Size) { + let img = env.image(id); let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap(); for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) { @@ -482,8 +488,8 @@ fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) { *dest = ColorU8::from_rgba(r, g, b, a).premultiply(); } - let view_width = element.size.width.to_pt() as f32; - let view_height = element.size.height.to_pt() as f32; + let view_width = size.width.to_pt() as f32; + let view_height = size.height.to_pt() as f32; let scale_x = view_width as f32 / pixmap.width() as f32; let scale_y = view_height as f32 / pixmap.height() as f32;