diff --git a/src/export/pdf/font.rs b/src/export/pdf/font.rs index 99268f9cd..b3481c437 100644 --- a/src/export/pdf/font.rs +++ b/src/export/pdf/font.rs @@ -9,7 +9,7 @@ use crate::util::SliceExt; /// Embed all used fonts into the PDF. pub fn write_fonts(ctx: &mut PdfContext) { - for font_id in ctx.font_map.layout_indices() { + for &font_id in ctx.font_map.items() { let type0_ref = ctx.alloc.bump(); let cid_ref = ctx.alloc.bump(); let descriptor_ref = ctx.alloc.bump(); diff --git a/src/export/pdf/image.rs b/src/export/pdf/image.rs index 90ab228f3..7886524c3 100644 --- a/src/export/pdf/image.rs +++ b/src/export/pdf/image.rs @@ -1,25 +1,26 @@ use std::io::Cursor; -use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba}; +use image::{DynamicImage, GenericImageView, ImageResult, Rgba}; use pdf_writer::{Filter, Finish}; use super::{deflate, PdfContext, RefExt}; -use crate::image::{Image, RasterImage}; +use crate::image::{DecodedImage, ImageFormat}; /// Embed all used images into the PDF. pub fn write_images(ctx: &mut PdfContext) { - for image_id in ctx.image_map.layout_indices() { + for image in ctx.image_map.items() { let image_ref = ctx.alloc.bump(); ctx.image_refs.push(image_ref); - let img = ctx.images.get(image_id); - let width = img.width(); - let height = img.height(); + let width = image.width(); + let height = image.height(); // Add the primary image. - match img { - Image::Raster(img) => { - if let Ok((data, filter, has_color)) = encode_image(img) { + match image.decode().unwrap() { + DecodedImage::Raster(dynamic) => { + if let Ok((data, filter, has_color)) = + encode_image(image.format(), &dynamic) + { let mut image = ctx.writer.image_xobject(image_ref, &data); image.filter(filter); image.width(width as i32); @@ -35,8 +36,8 @@ pub fn write_images(ctx: &mut PdfContext) { // Add a second gray-scale image containing the alpha values if // this image has an alpha channel. - if img.buf.color().has_alpha() { - let (alpha_data, alpha_filter) = encode_alpha(img); + if dynamic.color().has_alpha() { + let (alpha_data, alpha_filter) = encode_alpha(&dynamic); let mask_ref = ctx.alloc.bump(); image.s_mask(mask_ref); image.finish(); @@ -59,9 +60,9 @@ pub fn write_images(ctx: &mut PdfContext) { .device_gray(); } } - Image::Svg(img) => { + DecodedImage::Svg(svg) => { let next_ref = svg2pdf::convert_tree_into( - &img.0, + &svg, svg2pdf::Options::default(), &mut ctx.writer, image_ref, @@ -76,19 +77,22 @@ pub fn write_images(ctx: &mut PdfContext) { /// whether the image has color. /// /// Skips the alpha channel as that's encoded separately. -fn encode_image(img: &RasterImage) -> ImageResult<(Vec, Filter, bool)> { - Ok(match (img.format, &img.buf) { +fn encode_image( + format: ImageFormat, + dynamic: &DynamicImage, +) -> ImageResult<(Vec, Filter, bool)> { + Ok(match (format, dynamic) { // 8-bit gray JPEG. - (ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => { + (ImageFormat::Jpg, DynamicImage::ImageLuma8(_)) => { let mut data = Cursor::new(vec![]); - img.buf.write_to(&mut data, img.format)?; + dynamic.write_to(&mut data, image::ImageFormat::Jpeg)?; (data.into_inner(), Filter::DctDecode, false) } // 8-bit RGB JPEG (CMYK JPEGs get converted to RGB earlier). - (ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => { + (ImageFormat::Jpg, DynamicImage::ImageRgb8(_)) => { let mut data = Cursor::new(vec![]); - img.buf.write_to(&mut data, img.format)?; + dynamic.write_to(&mut data, image::ImageFormat::Jpeg)?; (data.into_inner(), Filter::DctDecode, true) } @@ -117,7 +121,7 @@ fn encode_image(img: &RasterImage) -> ImageResult<(Vec, Filter, bool)> { } /// Encode an image's alpha channel if present. -fn encode_alpha(img: &RasterImage) -> (Vec, Filter) { - let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); +fn encode_alpha(dynamic: &DynamicImage) -> (Vec, Filter) { + let pixels: Vec<_> = dynamic.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); (deflate(&pixels), Filter::FlateDecode) } diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs index 9ab3df24d..7468f7d74 100644 --- a/src/export/pdf/mod.rs +++ b/src/export/pdf/mod.rs @@ -17,7 +17,7 @@ use self::page::Page; use crate::font::{FontId, FontStore}; use crate::frame::Frame; use crate::geom::{Dir, Em, Length}; -use crate::image::{ImageId, ImageStore}; +use crate::image::Image; use crate::library::text::Lang; use crate::Context; @@ -46,7 +46,6 @@ const D65_GRAY: Name<'static> = Name(b"d65gray"); pub struct PdfContext<'a> { writer: PdfWriter, fonts: &'a FontStore, - images: &'a ImageStore, pages: Vec, page_heights: Vec, alloc: Ref, @@ -55,7 +54,7 @@ pub struct PdfContext<'a> { image_refs: Vec, page_refs: Vec, font_map: Remapper, - image_map: Remapper, + image_map: Remapper, glyph_sets: HashMap>, languages: HashMap, heading_tree: Vec, @@ -68,7 +67,6 @@ impl<'a> PdfContext<'a> { Self { writer: PdfWriter::new(), fonts: &ctx.fonts, - images: &ctx.images, pages: vec![], page_heights: vec![], alloc, @@ -147,36 +145,33 @@ fn deflate(data: &[u8]) -> Vec { miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) } -/// Assigns new, consecutive PDF-internal indices to things. -struct Remapper { - /// Forwards from the old indices to the new pdf indices. - to_pdf: HashMap, - /// Backwards from the pdf indices to the old indices. - to_layout: Vec, +/// Assigns new, consecutive PDF-internal indices to items. +struct Remapper { + /// Forwards from the items to the pdf indices. + to_pdf: HashMap, + /// Backwards from the pdf indices to the items. + to_items: Vec, } -impl Remapper +impl Remapper where - Index: Copy + Eq + Hash, + T: Eq + Hash + Clone, { fn new() -> Self { - Self { - to_pdf: HashMap::new(), - to_layout: vec![], - } + Self { to_pdf: HashMap::new(), to_items: vec![] } } - fn insert(&mut self, index: Index) { - let to_layout = &mut self.to_layout; - self.to_pdf.entry(index).or_insert_with(|| { + fn insert(&mut self, item: T) { + let to_layout = &mut self.to_items; + self.to_pdf.entry(item.clone()).or_insert_with(|| { let pdf_index = to_layout.len(); - to_layout.push(index); + to_layout.push(item); pdf_index }); } - fn map(&self, index: Index) -> usize { - self.to_pdf[&index] + fn map(&self, item: T) -> usize { + self.to_pdf[&item] } fn pdf_indices<'a>( @@ -186,8 +181,8 @@ where refs.iter().copied().zip(0 .. self.to_pdf.len()) } - fn layout_indices(&self) -> impl Iterator + '_ { - self.to_layout.iter().copied() + fn items(&self) -> impl Iterator + '_ { + self.to_items.iter() } } diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs index e5739a82b..6df7531bd 100644 --- a/src/export/pdf/page.rs +++ b/src/export/pdf/page.rs @@ -11,7 +11,7 @@ use crate::geom::{ self, Color, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size, Stroke, Transform, }; -use crate::image::ImageId; +use crate::image::Image; /// Construct page objects. pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) { @@ -290,13 +290,12 @@ fn write_frame(ctx: &mut PageContext, frame: &Frame) { for &(pos, ref element) in frame.elements() { let x = pos.x.to_f32(); let y = pos.y.to_f32(); - match *element { - Element::Group(ref group) => write_group(ctx, pos, group), - Element::Text(ref text) => write_text(ctx, x, y, text), - Element::Shape(ref shape) => write_shape(ctx, x, y, shape), - Element::Image(id, size) => write_image(ctx, x, y, id, size), - Element::Link(ref dest, size) => write_link(ctx, pos, dest, size), - Element::Pin(_) => {} + match element { + Element::Group(group) => write_group(ctx, pos, group), + Element::Text(text) => write_text(ctx, x, y, text), + Element::Shape(shape) => write_shape(ctx, x, y, shape), + Element::Image(image, size) => write_image(ctx, x, y, image, *size), + Element::Link(dest, size) => write_link(ctx, pos, dest, *size), } } } @@ -449,9 +448,9 @@ fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) { } /// Encode a vector or raster image into the content stream. -fn write_image(ctx: &mut PageContext, x: f32, y: f32, id: ImageId, size: Size) { - ctx.parent.image_map.insert(id); - let name = format_eco!("Im{}", ctx.parent.image_map.map(id)); +fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) { + ctx.parent.image_map.insert(image.clone()); + let name = format_eco!("Im{}", ctx.parent.image_map.map(image.clone())); let w = size.x.to_f32(); let h = size.y.to_f32(); ctx.content.save_state(); diff --git a/src/export/render.rs b/src/export/render.rs index 525d764d8..688cf9799 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -12,7 +12,7 @@ use crate::frame::{Element, Frame, Group, Text}; use crate::geom::{ self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform, }; -use crate::image::{Image, RasterImage, Svg}; +use crate::image::{DecodedImage, Image}; use crate::Context; /// Export a frame into a rendered image. @@ -49,21 +49,20 @@ fn render_frame( let y = pos.y.to_f32(); let ts = ts.pre_translate(x, y); - match *element { - Element::Group(ref group) => { + match element { + Element::Group(group) => { render_group(canvas, ts, mask, ctx, group); } - Element::Text(ref text) => { + Element::Text(text) => { render_text(canvas, ts, mask, ctx, text); } - Element::Shape(ref shape) => { + Element::Shape(shape) => { render_shape(canvas, ts, mask, shape); } - Element::Image(id, size) => { - render_image(canvas, ts, mask, ctx.images.get(id), size); + Element::Image(image, size) => { + render_image(canvas, ts, mask, image, *size); } Element::Link(_, _) => {} - Element::Pin(_) => {} } } } @@ -197,17 +196,20 @@ fn render_bitmap_glyph( let ppem = size * ts.sy; let font = ctx.fonts.get(text.font_id); let raster = font.ttf().glyph_raster_image(id, ppem as u16)?; - let img = RasterImage::parse(&raster.data).ok()?; + let ext = match raster.format { + ttf_parser::RasterImageFormat::PNG => "png", + }; + let image = Image::new(raster.data.into(), ext).ok()?; // FIXME: Vertical alignment isn't quite right for Apple Color Emoji, // and maybe also for Noto Color Emoji. And: Is the size calculation // correct? let h = text.size; - let w = (img.width() as f64 / img.height() as f64) * h; - let dx = (raster.x as f32) / (img.width() as f32) * size; - let dy = (raster.y as f32) / (img.height() as f32) * size; + let w = (image.width() as f64 / image.height() as f64) * h; + let dx = (raster.x as f32) / (image.width() as f32) * size; + let dy = (raster.y as f32) / (image.height() as f32) * size; let ts = ts.pre_translate(dx, -size - dy); - render_image(canvas, ts, mask, &Image::Raster(img), Size::new(w, h)) + render_image(canvas, ts, mask, &image, Size::new(w, h)) } /// Render an outline glyph into the canvas. This is the "normal" case. @@ -338,33 +340,33 @@ fn render_image( canvas: &mut sk::Pixmap, ts: sk::Transform, mask: Option<&sk::ClipMask>, - img: &Image, + image: &Image, size: Size, ) -> Option<()> { let view_width = size.x.to_f32(); let view_height = size.y.to_f32(); - let aspect = (img.width() as f32) / (img.height() as f32); + let aspect = (image.width() as f32) / (image.height() as f32); let scale = ts.sx.max(ts.sy); let w = (scale * view_width.max(aspect * view_height)).ceil() as u32; let h = ((w as f32) / aspect).ceil() as u32; let mut pixmap = sk::Pixmap::new(w, h)?; - match img { - Image::Raster(img) => { - let downscale = w < img.width(); + match image.decode().unwrap() { + DecodedImage::Raster(dynamic) => { + let downscale = w < image.width(); let filter = if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom }; - let buf = img.buf.resize(w, h, filter); + let buf = dynamic.resize(w, h, filter); for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) { let Rgba([r, g, b, a]) = src; *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); } } - Image::Svg(Svg(tree)) => { + DecodedImage::Svg(tree) => { resvg::render( &tree, FitTo::Size(w, h), diff --git a/src/font.rs b/src/font.rs index 8f440c52c..1fa86ba62 100644 --- a/src/font.rs +++ b/src/font.rs @@ -13,7 +13,7 @@ use ttf_parser::{name_id, GlyphId, PlatformId, Tag}; use unicode_segmentation::UnicodeSegmentation; use crate::geom::Em; -use crate::loading::{FileHash, Loader}; +use crate::loading::{Buffer, FileHash, Loader}; /// A unique identifier for a loaded font. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -40,7 +40,7 @@ pub struct FontStore { failed: Vec, fonts: Vec>, families: BTreeMap>, - buffers: HashMap>>, + buffers: HashMap, } impl FontStore { @@ -214,11 +214,11 @@ impl FontStore { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { let buffer = self.loader.load(path).ok()?; - entry.insert(Arc::new(buffer)) + entry.insert(buffer) } }; - let font = Font::new(Arc::clone(buffer), index)?; + let font = Font::new(buffer.clone(), index)?; *slot = Some(font); self.failed[idx] = false; @@ -239,7 +239,7 @@ pub struct Font { /// The raw font data, possibly shared with other fonts from the same /// collection. The vector's allocation must not move, because `ttf` points /// into it using unsafe code. - buffer: Arc>, + data: Buffer, /// The font's index in the collection (zero if not a collection). index: u32, /// The underlying ttf-parser/rustybuzz face. @@ -251,8 +251,8 @@ pub struct Font { } impl Font { - /// Parse a font from a buffer and collection index. - pub fn new(buffer: Arc>, index: u32) -> Option { + /// Parse a font from data and collection index. + pub fn new(data: Buffer, index: u32) -> Option { // Safety: // - The slices's location is stable in memory: // - We don't move the underlying vector @@ -260,13 +260,13 @@ impl Font { // - The internal 'static lifetime is not leaked because its rewritten // to the self-lifetime in `ttf()`. let slice: &'static [u8] = - unsafe { std::slice::from_raw_parts(buffer.as_ptr(), buffer.len()) }; + unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) }; let ttf = rustybuzz::Face::from_slice(slice, index)?; let metrics = FontMetrics::from_ttf(&ttf); Some(Self { - buffer, + data, index, ttf, metrics, @@ -275,8 +275,8 @@ impl Font { } /// The underlying buffer. - pub fn buffer(&self) -> &Arc> { - &self.buffer + pub fn buffer(&self) -> &Buffer { + &self.data } /// The collection index. diff --git a/src/frame.rs b/src/frame.rs index 8e0cb0dca..7a5fb9e4c 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -9,7 +9,7 @@ use crate::font::FontId; use crate::geom::{ Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform, }; -use crate::image::ImageId; +use crate::image::Image; use crate::library::text::Lang; use crate::util::EcoString; @@ -303,12 +303,9 @@ pub enum Element { /// A geometric shape with optional fill and stroke. Shape(Shape), /// An image and its size. - Image(ImageId, Size), + Image(Image, Size), /// A link to an external resource and its trigger region. Link(Destination, Size), - /// A pin identified by index. This is used to find elements on the pages - /// and use their location in formatting. Exporters can just ignore it. - Pin(usize), } impl Debug for Element { @@ -319,7 +316,6 @@ impl Debug for Element { Self::Shape(shape) => write!(f, "{shape:?}"), Self::Image(image, _) => write!(f, "{image:?}"), Self::Link(dest, _) => write!(f, "Link({dest:?})"), - Self::Pin(idx) => write!(f, "Pin({idx})"), } } } diff --git a/src/image.rs b/src/image.rs index 8ef404fe9..c2631477f 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,213 +1,139 @@ //! Image handling. -use std::collections::{hash_map::Entry, HashMap}; -use std::ffi::OsStr; -use std::fmt::{self, Debug, Formatter}; use std::io; -use std::path::Path; -use std::sync::Arc; -use image::io::Reader as ImageReader; -use image::{DynamicImage, ImageFormat}; +use crate::loading::Buffer; -use crate::diag::{failed_to_load, StrResult}; -use crate::loading::{FileHash, Loader}; - -/// A unique identifier for a loaded image. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -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 const fn from_raw(v: u32) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub const fn into_raw(self) -> u32 { - self.0 - } +/// A raster or vector image. +/// +/// Values of this type are cheap to clone and hash. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Image { + /// The raw, undecoded image data. + data: Buffer, + /// The format of the encoded `buffer`. + format: ImageFormat, + /// The width in pixels. + width: u32, + /// The height in pixels. + height: u32, } -/// Storage for loaded and decoded images. -pub struct ImageStore { - loader: Arc, - files: HashMap, - images: Vec, -} - -impl ImageStore { - /// Create a new, empty image store. - pub fn new(loader: Arc) -> Self { - Self { - loader, - files: HashMap::new(), - images: vec![], - } - } - - /// 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 this store's [`load()`](Self::load) - /// method. - #[track_caller] - pub fn get(&self, id: ImageId) -> &Image { - &self.images[id.0 as usize] - } - - /// Load and decode an image file from a path relative to the compilation - /// environment's root. - pub fn load(&mut self, path: &Path) -> StrResult { - let mut try_load = || -> io::Result { - let hash = self.loader.resolve(path)?; - Ok(*match self.files.entry(hash) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => { - let buffer = self.loader.load(path)?; - let ext = - path.extension().and_then(OsStr::to_str).unwrap_or_default(); - let image = Image::parse(&buffer, ext)?; - let id = ImageId(self.images.len() as u32); - self.images.push(image); - entry.insert(id) - } - }) - }; - - try_load().map_err(|err| failed_to_load("image", path, err)) - } -} - -/// A loaded image. -#[derive(Debug)] -pub enum Image { +/// A decoded image. +pub enum DecodedImage { /// A pixel raster format, like PNG or JPEG. - Raster(RasterImage), + Raster(image::DynamicImage), /// An SVG vector graphic. - Svg(Svg), + Svg(usvg::Tree), +} + +/// A raster or vector image format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ImageFormat { + /// Raster format for illustrations and transparent graphics. + Png, + /// Lossy raster format suitable for photos. + Jpg, + /// Raster format that is typically used for short animated clips. + Gif, + /// The vector graphics format of the web. + Svg, } impl Image { - /// Parse an image from raw data. The file extension is used as a hint for - /// which error message describes the problem best. - pub fn parse(data: &[u8], ext: &str) -> io::Result { - match Svg::parse(data) { - Ok(svg) => return Ok(Self::Svg(svg)), - Err(err) if matches!(ext, "svg" | "svgz") => return Err(err), - Err(_) => {} - } + /// Create an image from a raw buffer and a file extension. + /// + /// The file extension is used to determine the format. + pub fn new(data: Buffer, ext: &str) -> io::Result { + let format = match ext { + "svg" | "svgz" => ImageFormat::Svg, + "png" => ImageFormat::Png, + "jpg" | "jpeg" => ImageFormat::Jpg, + "gif" => ImageFormat::Gif, + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unknown image format", + )); + } + }; - match RasterImage::parse(data) { - Ok(raster) => return Ok(Self::Raster(raster)), - Err(err) if matches!(ext, "png" | "jpg" | "jpeg" | "gif") => return Err(err), - Err(_) => {} - } + let (width, height) = match format { + ImageFormat::Svg => { + let opts = usvg::Options::default(); + let tree = + usvg::Tree::from_data(&data, &opts.to_ref()).map_err(invalid)?; - Err(io::Error::new( - io::ErrorKind::InvalidData, - "unknown image format", - )) + let size = tree.svg_node().size; + let width = size.width().ceil() as u32; + let height = size.height().ceil() as u32; + (width, height) + } + _ => { + let cursor = io::Cursor::new(&data); + let format = convert_format(format); + let reader = image::io::Reader::with_format(cursor, format); + reader.into_dimensions().map_err(invalid)? + } + }; + + Ok(Self { data, format, width, height }) + } + + /// The raw image data. + pub fn data(&self) -> &Buffer { + &self.data + } + + /// The format of the image. + pub fn format(&self) -> ImageFormat { + self.format } /// The width of the image in pixels. pub fn width(&self) -> u32 { - match self { - Self::Raster(image) => image.width(), - Self::Svg(image) => image.width(), - } + self.width } /// The height of the image in pixels. pub fn height(&self) -> u32 { - match self { - Self::Raster(image) => image.height(), - Self::Svg(image) => image.height(), - } + self.height + } + + /// Decode the image. + pub fn decode(&self) -> io::Result { + Ok(match self.format { + ImageFormat::Svg => { + let opts = usvg::Options::default(); + let tree = + usvg::Tree::from_data(&self.data, &opts.to_ref()).map_err(invalid)?; + DecodedImage::Svg(tree) + } + _ => { + let cursor = io::Cursor::new(&self.data); + let format = convert_format(self.format); + let reader = image::io::Reader::with_format(cursor, format); + let dynamic = reader.decode().map_err(invalid)?; + DecodedImage::Raster(dynamic) + } + }) } } -/// A raster image, supported through the image crate. -pub struct RasterImage { - /// The original format the image was encoded in. - pub format: ImageFormat, - /// The decoded image. - pub buf: DynamicImage, -} - -impl RasterImage { - /// Parse an image from raw data in a supported format (PNG, JPEG or GIF). - /// - /// The image format is determined automatically. - pub fn parse(data: &[u8]) -> io::Result { - let cursor = io::Cursor::new(data); - let reader = ImageReader::new(cursor).with_guessed_format()?; - let format = reader - .format() - .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?; - - let buf = reader - .decode() - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; - - Ok(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() +/// Convert a raster image format to the image crate's format. +fn convert_format(format: ImageFormat) -> image::ImageFormat { + match format { + ImageFormat::Png => image::ImageFormat::Png, + ImageFormat::Jpg => image::ImageFormat::Jpeg, + ImageFormat::Gif => image::ImageFormat::Gif, + ImageFormat::Svg => panic!("must be a raster format"), } } -impl Debug for RasterImage { - 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() - } -} - -/// An SVG image, supported through the usvg crate. -pub struct Svg(pub usvg::Tree); - -impl Svg { - /// Parse an SVG file from a data buffer. This also handles `.svgz` - /// compressed files. - pub fn parse(data: &[u8]) -> io::Result { - let usvg_opts = usvg::Options::default(); - usvg::Tree::from_data(data, &usvg_opts.to_ref()) - .map(Self) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) - } - - /// The width of the image in rounded-up nominal SVG pixels. - pub fn width(&self) -> u32 { - self.0.svg_node().size.width().ceil() as u32 - } - - /// The height of the image in rounded-up nominal SVG pixels. - pub fn height(&self) -> u32 { - self.0.svg_node().size.height().ceil() as u32 - } -} - -impl Debug for Svg { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Svg") - .field("width", &self.0.svg_node().size.width()) - .field("height", &self.0.svg_node().size.height()) - .field("viewBox", &self.0.svg_node().view_box) - .finish() - } +/// Turn any error into an I/O error. +fn invalid(error: E) -> io::Error +where + E: std::error::Error + Send + Sync + 'static, +{ + io::Error::new(io::ErrorKind::InvalidData, error) } diff --git a/src/lib.rs b/src/lib.rs index 572a05417..9da37df79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,7 +57,6 @@ use crate::diag::TypResult; use crate::eval::Scope; use crate::font::FontStore; use crate::frame::Frame; -use crate::image::ImageStore; use crate::loading::Loader; use crate::model::StyleMap; use crate::source::{SourceId, SourceStore}; @@ -80,8 +79,6 @@ pub struct Context { pub sources: SourceStore, /// Stores parsed fonts. pub fonts: FontStore, - /// Stores decoded images. - pub images: ImageStore, /// The context's configuration. config: Config, } @@ -93,7 +90,6 @@ impl Context { loader: Arc::clone(&loader), sources: SourceStore::new(Arc::clone(&loader)), fonts: FontStore::new(Arc::clone(&loader)), - images: ImageStore::new(loader), config, } } diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs index feb37ae64..784afa01f 100644 --- a/src/library/graphics/image.rs +++ b/src/library/graphics/image.rs @@ -1,10 +1,12 @@ -use crate::image::ImageId; +use std::ffi::OsStr; + +use crate::image::Image; use crate::library::prelude::*; use crate::library::text::TextNode; /// Show a raster or vector graphic. #[derive(Debug, Hash)] -pub struct ImageNode(pub ImageId); +pub struct ImageNode(pub Image); #[node] impl ImageNode { @@ -16,12 +18,20 @@ impl ImageNode { args.expect::>("path to image file")?; let full = vm.locate(&path).at(span)?; - let id = vm.ctx.images.load(&full).at(span)?; + let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default(); + let image = vm + .ctx + .loader + .load(&full) + .and_then(|buffer| Image::new(buffer, ext)) + .map_err(|err| failed_to_load("image", &full, err)) + .at(span)?; + let width = args.named("width")?; let height = args.named("height")?; Ok(Content::inline( - ImageNode(id).pack().sized(Spec::new(width, height)), + ImageNode(image).pack().sized(Spec::new(width, height)), )) } } @@ -29,13 +39,12 @@ impl ImageNode { impl Layout for ImageNode { fn layout( &self, - ctx: &mut Context, + _: &mut Context, regions: &Regions, styles: StyleChain, ) -> TypResult> { - let img = ctx.images.get(self.0); - let pxw = img.width() as f64; - let pxh = img.height() as f64; + let pxw = self.0.width() as f64; + let pxh = self.0.height() as f64; let px_ratio = pxw / pxh; // Find out whether the image is wider or taller than the target size. @@ -71,7 +80,7 @@ impl Layout for ImageNode { // the frame to the target size, center aligning the image in the // process. let mut frame = Frame::new(fitted); - frame.push(Point::zero(), Element::Image(self.0, fitted)); + frame.push(Point::zero(), Element::Image(self.0.clone(), fitted)); frame.resize(target, Align::CENTER_HORIZON); // Create a clipping group if only part of the image should be visible. diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 6c5c97655..168aca262 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -25,8 +25,6 @@ pub enum ParChild { Spacing(Spacing), /// An arbitrary inline-level node. Node(LayoutNode), - /// A pin identified by index. - Pin(usize), } #[node] @@ -101,7 +99,6 @@ impl Debug for ParChild { Self::Quote { double } => write!(f, "Quote({double})"), Self::Spacing(kind) => write!(f, "{:?}", kind), Self::Node(node) => node.fmt(f), - Self::Pin(idx) => write!(f, "Pin({idx})"), } } } @@ -197,7 +194,6 @@ type Range = std::ops::Range; // paragraph's full text. const SPACING_REPLACE: char = ' '; // Space const NODE_REPLACE: char = '\u{FFFC}'; // Object Replacement Character -const PIN_REPLACE: char = '\u{200D}'; // Zero Width Joiner /// A paragraph representation in which children are already layouted and text /// is already preshaped. @@ -278,8 +274,6 @@ enum Segment<'a> { Spacing(Spacing), /// An arbitrary inline-level layout node. Node(&'a LayoutNode), - /// A pin identified by index. - Pin(usize), } impl Segment<'_> { @@ -289,7 +283,6 @@ impl Segment<'_> { Self::Text(len) => len, Self::Spacing(_) => SPACING_REPLACE.len_utf8(), Self::Node(_) => NODE_REPLACE.len_utf8(), - Self::Pin(_) => PIN_REPLACE.len_utf8(), } } } @@ -307,8 +300,6 @@ enum Item<'a> { Frame(Frame), /// A repeating node that fills the remaining space. Repeat(&'a RepeatNode, StyleChain<'a>), - /// A pin identified by index. - Pin(usize), } impl<'a> Item<'a> { @@ -326,7 +317,6 @@ impl<'a> Item<'a> { Self::Text(shaped) => shaped.text.len(), Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(), Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(), - Self::Pin(_) => PIN_REPLACE.len_utf8(), } } @@ -336,7 +326,7 @@ impl<'a> Item<'a> { Self::Text(shaped) => shaped.width, Self::Absolute(v) => *v, Self::Frame(frame) => frame.width(), - Self::Fractional(_) | Self::Repeat(_, _) | Self::Pin(_) => Length::zero(), + Self::Fractional(_) | Self::Repeat(_, _) => Length::zero(), } } } @@ -467,7 +457,6 @@ fn collect<'a>( ParChild::Quote { .. } => Some('"'), ParChild::Spacing(_) => Some(SPACING_REPLACE), ParChild::Node(_) => Some(NODE_REPLACE), - ParChild::Pin(_) => Some(PIN_REPLACE), }); full.push_str(quoter.quote("es, double, peeked)); @@ -484,10 +473,6 @@ fn collect<'a>( full.push(NODE_REPLACE); Segment::Node(node) } - &ParChild::Pin(idx) => { - full.push(PIN_REPLACE); - Segment::Pin(idx) - } }; if let Some(last) = full.chars().last() { @@ -556,7 +541,6 @@ fn prepare<'a>( items.push(Item::Frame(frame)); } } - Segment::Pin(idx) => items.push(Item::Pin(idx)), } cursor = end; @@ -1187,11 +1171,6 @@ fn commit( } offset = before + fill; } - Item::Pin(idx) => { - let mut frame = Frame::new(Size::zero()); - frame.push(Point::zero(), Element::Pin(*idx)); - push(&mut offset, frame); - } } } diff --git a/src/loading/fs.rs b/src/loading/fs.rs index 70ab5e538..55aa967b5 100644 --- a/src/loading/fs.rs +++ b/src/loading/fs.rs @@ -6,7 +6,7 @@ use memmap2::Mmap; use same_file::Handle; use walkdir::WalkDir; -use super::{FileHash, Loader}; +use super::{Buffer, FileHash, Loader}; use crate::font::FontInfo; /// Loads fonts and files from the local file system. @@ -130,7 +130,7 @@ impl Loader for FsLoader { } } - fn load(&self, path: &Path) -> io::Result> { - fs::read(path) + fn load(&self, path: &Path) -> io::Result { + Ok(fs::read(path)?.into()) } } diff --git a/src/loading/mem.rs b/src/loading/mem.rs index 320de3493..36e920d9b 100644 --- a/src/loading/mem.rs +++ b/src/loading/mem.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::io; use std::path::{Path, PathBuf}; -use super::{FileHash, Loader}; +use super::{Buffer, FileHash, Loader}; use crate::font::FontInfo; use crate::util::PathExt; @@ -61,10 +61,10 @@ impl Loader for MemLoader { } } - fn load(&self, path: &Path) -> io::Result> { + fn load(&self, path: &Path) -> io::Result { self.files .get(&path.normalize()) - .map(|cow| cow.clone().into_owned()) + .map(|cow| cow.clone().into_owned().into()) .ok_or_else(|| io::ErrorKind::NotFound.into()) } } @@ -90,7 +90,7 @@ mod tests { // Test that the file can be loaded. assert_eq!( - loader.load(Path::new("directory/../PTSans.ttf")).unwrap(), + loader.load(Path::new("directory/../PTSans.ttf")).unwrap().as_slice(), data ); } diff --git a/src/loading/mod.rs b/src/loading/mod.rs index d37dd1fc4..ecc1e8d5e 100644 --- a/src/loading/mod.rs +++ b/src/loading/mod.rs @@ -8,10 +8,14 @@ mod mem; pub use fs::*; pub use mem::*; +use std::fmt::{self, Debug, Formatter}; use std::io; +use std::ops::Deref; use std::path::Path; +use std::sync::Arc; use crate::font::FontInfo; +use crate::util::Prehashed; /// A hash that identifies a file. /// @@ -29,7 +33,7 @@ pub trait Loader { fn resolve(&self, path: &Path) -> io::Result; /// Load a file from a path. - fn load(&self, path: &Path) -> io::Result>; + fn load(&self, path: &Path) -> io::Result; } /// A loader which serves nothing. @@ -44,7 +48,61 @@ impl Loader for BlankLoader { Err(io::ErrorKind::NotFound.into()) } - fn load(&self, _: &Path) -> io::Result> { + fn load(&self, _: &Path) -> io::Result { Err(io::ErrorKind::NotFound.into()) } } + +/// A shared buffer that is cheap to clone. +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Buffer(Prehashed>>); + +impl Buffer { + /// Return a view into the buffer. + pub fn as_slice(&self) -> &[u8] { + self + } + + /// Return a copy of the buffer as a vector. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } +} + +impl From<&[u8]> for Buffer { + fn from(slice: &[u8]) -> Self { + Self(Prehashed::new(Arc::new(slice.to_vec()))) + } +} + +impl From> for Buffer { + fn from(vec: Vec) -> Self { + Self(Prehashed::new(Arc::new(vec))) + } +} + +impl From>> for Buffer { + fn from(arc: Arc>) -> Self { + Self(Prehashed::new(arc)) + } +} + +impl Deref for Buffer { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for Buffer { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl Debug for Buffer { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Buffer(..)") + } +} diff --git a/src/model/content.rs b/src/model/content.rs index efbaed0e4..ae86e2790 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -90,8 +90,6 @@ pub enum Content { /// A node that can be realized with styles, optionally with attached /// properties. Show(ShowNode, Option), - /// A pin identified by index. - Pin(usize), /// Content with attached styles. Styled(Arc<(Self, StyleMap)>), /// A sequence of multiple nodes. @@ -281,7 +279,6 @@ impl Debug for Content { Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"), Self::Page(page) => page.fmt(f), Self::Show(node, _) => node.fmt(f), - Self::Pin(idx) => write!(f, "Pin({idx})"), Self::Styled(styled) => { let (sub, map) = styled.as_ref(); map.fmt(f)?; @@ -651,9 +648,6 @@ impl<'a> ParBuilder<'a> { Content::Inline(node) => { self.0.supportive(ParChild::Node(node.clone()), styles); } - &Content::Pin(idx) => { - self.0.ignorant(ParChild::Pin(idx), styles); - } _ => return false, } @@ -673,7 +667,7 @@ impl<'a> ParBuilder<'a> { && children .items() .find_map(|child| match child { - ParChild::Spacing(_) | ParChild::Pin(_) => None, + ParChild::Spacing(_) => None, ParChild::Text(_) | ParChild::Quote { .. } => Some(true), ParChild::Node(_) => Some(false), }) diff --git a/src/source.rs b/src/source.rs index 145791b23..226480197 100644 --- a/src/source.rs +++ b/src/source.rs @@ -73,7 +73,7 @@ impl SourceStore { /// root. /// /// If there already exists a source file for this path, it is - /// [replaced](SourceFile::replace). + /// [replaced](Source::replace). pub fn load(&mut self, path: &Path) -> StrResult { let mut try_load = || -> io::Result { let hash = self.loader.resolve(path)?; @@ -82,7 +82,7 @@ impl SourceStore { } let data = self.loader.load(path)?; - let src = String::from_utf8(data).map_err(|_| { + let src = String::from_utf8(data.to_vec()).map_err(|_| { io::Error::new(io::ErrorKind::InvalidData, "file is not valid utf-8") })?; @@ -99,7 +99,7 @@ impl SourceStore { /// will use the inserted file instead of going through [`Loader::load`]. /// /// If the path is resolvable and points to an existing source file, it is - /// [replaced](SourceFile::replace). + /// [replaced](Source::replace). pub fn provide(&mut self, path: impl AsRef, src: String) -> SourceId { let path = path.as_ref(); let hash = self.loader.resolve(path).ok(); @@ -122,7 +122,7 @@ impl SourceStore { id } - /// Fully [replace](SourceFile::replace) the source text of a file. + /// Fully [replace](Source::replace) the source text of a file. /// /// This panics if no source file with this `id` exists. #[track_caller] @@ -130,7 +130,7 @@ impl SourceStore { self.sources[id.0 as usize].replace(src) } - /// [Edit](SourceFile::edit) a source file by replacing the given range. + /// [Edit](Source::edit) a source file by replacing the given range. /// /// This panics if no source file with this `id` exists or if the `replace` /// range is out of bounds. @@ -144,7 +144,7 @@ impl SourceStore { self.sources[id.0 as usize].edit(replace, with) } - /// Map a span that points into a [file](SourceFile::range) stored in this + /// Map a span that points into a [file](Source::range) stored in this /// source store to a byte range. /// /// Panics if the span does not point into this source store.