//! Image handling. use std::io; use std::sync::Arc; use crate::diag::{format_xml_like_error, StrResult}; use crate::util::Buffer; /// 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, } impl Image { /// Create an image from a buffer and a format. /// /// Extracts the width and height. pub fn new(data: Buffer, format: ImageFormat) -> StrResult { let (width, height) = determine_size(&data, format)?; 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 { self.width } /// The height of the image in pixels. pub fn height(&self) -> u32 { self.height } /// Decode the image. #[comemo::memoize] pub fn decode(&self) -> StrResult> { Ok(Arc::new(match self.format { ImageFormat::Vector(VectorFormat::Svg) => { let opts = usvg::Options::default(); let tree = usvg::Tree::from_data(&self.data, &opts.to_ref()) .map_err(format_usvg_error)?; DecodedImage::Svg(tree) } ImageFormat::Raster(format) => { let cursor = io::Cursor::new(&self.data); let reader = image::io::Reader::with_format(cursor, format.into()); let dynamic = reader.decode().map_err(format_image_error)?; DecodedImage::Raster(dynamic, format) } })) } } /// A raster or vector image format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ImageFormat { /// A raster graphics format. Raster(RasterFormat), /// A vector graphics format. Vector(VectorFormat), } /// A raster graphics format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum RasterFormat { /// 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, } /// A vector graphics format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum VectorFormat { /// The vector graphics format of the web. Svg, } impl From for image::ImageFormat { fn from(format: RasterFormat) -> Self { match format { RasterFormat::Png => image::ImageFormat::Png, RasterFormat::Jpg => image::ImageFormat::Jpeg, RasterFormat::Gif => image::ImageFormat::Gif, } } } impl From for RasterFormat { fn from(format: ttf_parser::RasterImageFormat) -> Self { match format { ttf_parser::RasterImageFormat::PNG => RasterFormat::Png, } } } impl From for ImageFormat { fn from(format: ttf_parser::RasterImageFormat) -> Self { Self::Raster(format.into()) } } /// A decoded image. pub enum DecodedImage { /// A decoded pixel raster. Raster(image::DynamicImage, RasterFormat), /// An decoded SVG tree. Svg(usvg::Tree), } /// Determine the image size in pixels. #[comemo::memoize] fn determine_size(data: &Buffer, format: ImageFormat) -> StrResult<(u32, u32)> { match format { ImageFormat::Raster(format) => { let cursor = io::Cursor::new(&data); let reader = image::io::Reader::with_format(cursor, format.into()); Ok(reader.into_dimensions().map_err(format_image_error)?) } ImageFormat::Vector(VectorFormat::Svg) => { let opts = usvg::Options::default(); let tree = usvg::Tree::from_data(data, &opts.to_ref()).map_err(format_usvg_error)?; let size = tree.svg_node().size; let width = size.width().ceil() as u32; let height = size.height().ceil() as u32; Ok((width, height)) } } } /// Format the user-facing raster graphic decoding error message. fn format_image_error(error: image::ImageError) -> String { match error { image::ImageError::Limits(_) => "file is too large".into(), _ => "failed to decode image".into(), } } /// Format the user-facing SVG decoding error message. fn format_usvg_error(error: usvg::Error) -> String { match error { usvg::Error::NotAnUtf8Str => "file is not valid utf-8".into(), usvg::Error::MalformedGZip => "file is not compressed correctly".into(), usvg::Error::ElementsLimitReached => "file is too large".into(), usvg::Error::InvalidSize => { "failed to parse svg: width, height, or viewbox is invalid".into() } usvg::Error::ParsingFailed(error) => format_xml_like_error("svg", error), } }