From f15ee7efb68eff188b5993d21d663e2120b5dd08 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Mon, 6 Dec 2021 14:58:57 +0100 Subject: Add SVG capabilities --- src/export/pdf.rs | 95 +++++++++++++++++++++++++++--------------------- src/image.rs | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 43 deletions(-) (limited to 'src') diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 8df31660..100a53f9 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -10,13 +10,14 @@ use pdf_writer::types::{ ActionType, AnnotationType, CidFontType, FontFlags, SystemInfo, UnicodeCmap, }; use pdf_writer::{Content, Filter, Finish, Name, PdfWriter, Rect, Ref, Str, TextStr}; +use svg2pdf::{convert_tree_into, Options}; use ttf_parser::{name_id, GlyphId, Tag}; use super::subset; use crate::font::{find_name, FaceId, FontStore}; use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; use crate::geom::{self, Color, Em, Length, Paint, Point, Size, Transform}; -use crate::image::{Image, ImageId, ImageStore}; +use crate::image::{Image, ImageId, ImageStore, RasterImage}; use crate::Context; /// Export a collection of frames into a PDF file. @@ -90,7 +91,7 @@ impl<'a> PdfExporter<'a> { let postscript_name = find_name(ttf.names(), name_id::POST_SCRIPT_NAME) .unwrap_or_else(|| "unknown".to_string()); - let base_font = format!("ABCDEF+{}", postscript_name); + let base_font = format_eco!("ABCDEF+{}", postscript_name); let base_font = Name(base_font.as_bytes()); let cmap_name = Name(b"Custom"); let system_info = SystemInfo { @@ -218,44 +219,58 @@ impl<'a> PdfExporter<'a> { let height = img.height(); // Add the primary image. - if let Ok((data, filter, has_color)) = encode_image(img) { - let mut image = self.writer.image_xobject(image_ref, &data); - image.filter(filter); - image.width(width as i32); - image.height(height as i32); - image.bits_per_component(8); - - let space = image.color_space(); - if has_color { - space.device_rgb(); - } else { - space.device_gray(); + match img { + Image::Raster(img) => { + if let Ok((data, filter, has_color)) = encode_image(img) { + let mut image = self.writer.image_xobject(image_ref, &data); + image.filter(filter); + image.width(width as i32); + image.height(height as i32); + image.bits_per_component(8); + + let space = image.color_space(); + if has_color { + space.device_rgb(); + } else { + space.device_gray(); + } + + // 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); + let mask_ref = self.alloc.bump(); + image.s_mask(mask_ref); + image.finish(); + + let mut mask = + self.writer.image_xobject(mask_ref, &alpha_data); + mask.filter(alpha_filter); + mask.width(width as i32); + mask.height(height as i32); + mask.color_space().device_gray(); + mask.bits_per_component(8); + } + } else { + // TODO: Warn that image could not be encoded. + self.writer + .image_xobject(image_ref, &[]) + .width(0) + .height(0) + .bits_per_component(1) + .color_space() + .device_gray(); + } } - - // 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); - let mask_ref = self.alloc.bump(); - image.s_mask(mask_ref); - image.finish(); - - let mut mask = self.writer.image_xobject(mask_ref, &alpha_data); - mask.filter(alpha_filter); - mask.width(width as i32); - mask.height(height as i32); - mask.color_space().device_gray(); - mask.bits_per_component(8); + Image::Svg(img) => { + let next_ref = convert_tree_into( + &img.0, + Options::default(), + &mut self.writer, + image_ref, + ); + self.alloc = next_ref; } - } else { - // TODO: Warn that image could not be encoded. - self.writer - .image_xobject(image_ref, &[]) - .width(0) - .height(0) - .bits_per_component(1) - .color_space() - .device_gray(); } } } @@ -636,7 +651,7 @@ impl<'a> PageExporter<'a> { /// whether the image has color. /// /// Skips the alpha channel as that's encoded separately. -fn encode_image(img: &Image) -> ImageResult<(Vec, Filter, bool)> { +fn encode_image(img: &RasterImage) -> ImageResult<(Vec, Filter, bool)> { Ok(match (img.format, &img.buf) { // 8-bit gray JPEG. (ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => { @@ -677,7 +692,7 @@ fn encode_image(img: &Image) -> ImageResult<(Vec, Filter, bool)> { } /// Encode an image's alpha channel if present. -fn encode_alpha(img: &Image) -> (Vec, Filter) { +fn encode_alpha(img: &RasterImage) -> (Vec, Filter) { let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); (deflate(&pixels), Filter::FlateDecode) } diff --git a/src/image.rs b/src/image.rs index 512b24b1..f6574045 100644 --- a/src/image.rs +++ b/src/image.rs @@ -9,6 +9,7 @@ use std::rc::Rc; use image::io::Reader as ImageReader; use image::{DynamicImage, GenericImageView, ImageFormat}; use serde::{Deserialize, Serialize}; +use usvg::{Error as USvgError, Tree}; use crate::loading::{FileHash, Loader}; @@ -88,14 +89,112 @@ impl ImageStore { } /// A loaded image. -pub struct Image { +#[derive(Debug)] +pub enum Image { + Raster(RasterImage), + Svg(Svg), +} + +impl Image { + /// Parse an image from raw data. This will prioritize SVG images and then + /// try to decode a supported raster format. + pub fn parse(data: &[u8]) -> io::Result { + match Svg::parse(data) { + Ok(svg) => Ok(Self::Svg(svg)), + Err(e) if e.kind() == io::ErrorKind::InvalidData => { + Ok(Self::Raster(RasterImage::parse(data)?)) + } + Err(e) => Err(e), + } + } + + /// The width of the image in pixels. + pub fn width(&self) -> u32 { + match self { + Self::Raster(image) => image.width(), + Self::Svg(image) => image.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(), + } + } + + pub fn is_vector(&self) -> bool { + match self { + Self::Raster(_) => false, + Self::Svg(_) => true, + } + } +} + +/// An SVG image, supported through the usvg crate. +pub struct Svg(pub 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(); + let tree = Tree::from_data(data, &usvg_opts.to_ref()).map_err(|e| match e { + USvgError::NotAnUtf8Str => { + io::Error::new(io::ErrorKind::InvalidData, "file is not valid utf-8") + } + USvgError::MalformedGZip => io::Error::new( + io::ErrorKind::InvalidData, + "could not extract gzipped SVG", + ), + USvgError::ElementsLimitReached => io::Error::new( + io::ErrorKind::Other, + "SVG file has more than 1 million elements", + ), + USvgError::InvalidSize => io::Error::new( + io::ErrorKind::InvalidData, + "SVG width or height not greater than zero", + ), + USvgError::ParsingFailed(error) => io::Error::new( + io::ErrorKind::InvalidData, + format!("SVG parsing error: {}", error.to_string()), + ), + })?; + + Ok(Self(tree)) + } + + /// 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() + } +} + +/// 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 Image { +impl RasterImage { /// Parse an image from raw data in a supported format (PNG or JPEG). /// /// The image format is determined automatically. @@ -124,7 +223,7 @@ impl Image { } } -impl Debug for Image { +impl Debug for RasterImage { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_struct("Image") .field("format", &self.format) -- cgit v1.2.3 From 7c829c5c1b67ac8e8fbe4fc4ba01468d100bfb47 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Tue, 7 Dec 2021 00:06:54 +0100 Subject: Bump pdf-writer, svg2pdf and error messages Co-Authored-By: Laurenz --- src/export/pdf.rs | 5 +- src/image.rs | 133 ++++++++++++++++++++++++------------------------------ 2 files changed, 61 insertions(+), 77 deletions(-) (limited to 'src') diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 100a53f9..b5f4c009 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -10,7 +10,6 @@ use pdf_writer::types::{ ActionType, AnnotationType, CidFontType, FontFlags, SystemInfo, UnicodeCmap, }; use pdf_writer::{Content, Filter, Finish, Name, PdfWriter, Rect, Ref, Str, TextStr}; -use svg2pdf::{convert_tree_into, Options}; use ttf_parser::{name_id, GlyphId, Tag}; use super::subset; @@ -263,9 +262,9 @@ impl<'a> PdfExporter<'a> { } } Image::Svg(img) => { - let next_ref = convert_tree_into( + let next_ref = svg2pdf::convert_tree_into( &img.0, - Options::default(), + svg2pdf::Options::default(), &mut self.writer, image_ref, ); diff --git a/src/image.rs b/src/image.rs index f6574045..a7b62503 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,6 +1,7 @@ //! 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; @@ -9,7 +10,6 @@ use std::rc::Rc; use image::io::Reader as ImageReader; use image::{DynamicImage, GenericImageView, ImageFormat}; use serde::{Deserialize, Serialize}; -use usvg::{Error as USvgError, Tree}; use crate::loading::{FileHash, Loader}; @@ -66,7 +66,8 @@ impl ImageStore { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { let buffer = self.loader.load(path)?; - let image = Image::parse(&buffer)?; + 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); if let Some(callback) = &self.on_load { callback(id, &image); @@ -91,21 +92,32 @@ impl ImageStore { /// A loaded image. #[derive(Debug)] pub enum Image { + /// A pixel raster format, like PNG or JPEG. Raster(RasterImage), + /// An SVG vector graphic. Svg(Svg), } impl Image { - /// Parse an image from raw data. This will prioritize SVG images and then - /// try to decode a supported raster format. - pub fn parse(data: &[u8]) -> io::Result { + /// 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) => Ok(Self::Svg(svg)), - Err(e) if e.kind() == io::ErrorKind::InvalidData => { - Ok(Self::Raster(RasterImage::parse(data)?)) - } - Err(e) => Err(e), + Ok(svg) => return Ok(Self::Svg(svg)), + Err(err) if matches!(ext, "svg" | "svgz") => return Err(err), + Err(_) => {} } + + match RasterImage::parse(data) { + Ok(raster) => return Ok(Self::Raster(raster)), + Err(err) if matches!(ext, "png" | "jpg" | "jpeg") => return Err(err), + Err(_) => {} + } + + Err(io::Error::new( + io::ErrorKind::InvalidData, + "unknown image format", + )) } /// The width of the image in pixels. @@ -123,67 +135,6 @@ impl Image { Self::Svg(image) => image.height(), } } - - pub fn is_vector(&self) -> bool { - match self { - Self::Raster(_) => false, - Self::Svg(_) => true, - } - } -} - -/// An SVG image, supported through the usvg crate. -pub struct Svg(pub 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(); - let tree = Tree::from_data(data, &usvg_opts.to_ref()).map_err(|e| match e { - USvgError::NotAnUtf8Str => { - io::Error::new(io::ErrorKind::InvalidData, "file is not valid utf-8") - } - USvgError::MalformedGZip => io::Error::new( - io::ErrorKind::InvalidData, - "could not extract gzipped SVG", - ), - USvgError::ElementsLimitReached => io::Error::new( - io::ErrorKind::Other, - "SVG file has more than 1 million elements", - ), - USvgError::InvalidSize => io::Error::new( - io::ErrorKind::InvalidData, - "SVG width or height not greater than zero", - ), - USvgError::ParsingFailed(error) => io::Error::new( - io::ErrorKind::InvalidData, - format!("SVG parsing error: {}", error.to_string()), - ), - })?; - - Ok(Self(tree)) - } - - /// 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() - } } /// A raster image, supported through the image crate. @@ -201,9 +152,9 @@ impl RasterImage { 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::new(io::ErrorKind::InvalidData, "unknown image format") - })?; + let format = reader + .format() + .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?; let buf = reader .decode() @@ -233,3 +184,37 @@ impl Debug for RasterImage { .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() + } +} -- cgit v1.2.3