summaryrefslogtreecommitdiff
path: root/src/image.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/image.rs')
-rw-r--r--src/image.rs290
1 files changed, 108 insertions, 182 deletions
diff --git a/src/image.rs b/src/image.rs
index 8ef404fe..c2631477 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::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
- }
+use crate::loading::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,
}
-/// Storage for loaded and decoded images.
-pub struct ImageStore {
- loader: Arc<dyn Loader>,
- files: HashMap<FileHash, ImageId>,
- images: Vec<Image>,
+/// A decoded image.
+pub enum DecodedImage {
+ /// A pixel raster format, like PNG or JPEG.
+ Raster(image::DynamicImage),
+ /// An SVG vector graphic.
+ Svg(usvg::Tree),
}
-impl ImageStore {
- /// Create a new, empty image store.
- pub fn new(loader: Arc<dyn Loader>) -> Self {
- Self {
- loader,
- files: HashMap::new(),
- images: vec![],
- }
- }
+/// 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,
+}
- /// Get a reference to a loaded image.
+impl Image {
+ /// Create an image from a raw buffer and a file extension.
///
- /// 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]
- }
+ /// The file extension is used to determine the format.
+ pub fn new(data: Buffer, ext: &str) -> io::Result<Self> {
+ 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",
+ ));
+ }
+ };
- /// Load and decode an image file from a path relative to the compilation
- /// environment's root.
- pub fn load(&mut self, path: &Path) -> StrResult<ImageId> {
- let mut try_load = || -> io::Result<ImageId> {
- 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)
- }
- })
+ 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)?;
+
+ 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)?
+ }
};
- try_load().map_err(|err| failed_to_load("image", path, err))
+ Ok(Self { data, format, width, height })
}
-}
-/// A loaded image.
-#[derive(Debug)]
-pub enum Image {
- /// A pixel raster format, like PNG or JPEG.
- Raster(RasterImage),
- /// An SVG vector graphic.
- Svg(Svg),
-}
+ /// The raw image data.
+ pub fn data(&self) -> &Buffer {
+ &self.data
+ }
-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<Self> {
- match Svg::parse(data) {
- 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" | "gif") => return Err(err),
- Err(_) => {}
- }
-
- Err(io::Error::new(
- io::ErrorKind::InvalidData,
- "unknown image format",
- ))
+ /// 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<DecodedImage> {
+ 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<Self> {
- 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()
- }
-}
-
-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()
+/// 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"),
}
}
-/// 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<Self> {
- 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<E>(error: E) -> io::Error
+where
+ E: std::error::Error + Send + Sync + 'static,
+{
+ io::Error::new(io::ErrorKind::InvalidData, error)
}