summaryrefslogtreecommitdiff
path: root/src/image.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/image.rs')
-rw-r--r--src/image.rs127
1 files changed, 127 insertions, 0 deletions
diff --git a/src/image.rs b/src/image.rs
new file mode 100644
index 00000000..bdfc19a6
--- /dev/null
+++ b/src/image.rs
@@ -0,0 +1,127 @@
+//! Image handling.
+
+use std::collections::{hash_map::Entry, HashMap};
+use std::fmt::{self, Debug, Formatter};
+use std::io::Cursor;
+
+use image::io::Reader as ImageReader;
+use image::{DynamicImage, GenericImageView, ImageFormat};
+use serde::{Deserialize, Serialize};
+
+use crate::loading::Loader;
+
+/// A loaded image.
+pub struct Image {
+ /// The original format the image was encoded in.
+ pub format: ImageFormat,
+ /// The decoded image.
+ pub buf: DynamicImage,
+}
+
+impl Image {
+ /// Parse an image from raw data in a supported format (PNG or JPEG).
+ ///
+ /// The image format is determined automatically.
+ pub fn parse(data: &[u8]) -> Option<Self> {
+ 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 Image {
+ 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()
+ }
+}
+
+/// Caches decoded images.
+pub struct ImageCache {
+ /// Loaded images indexed by [`ImageId`].
+ images: Vec<Image>,
+ /// Maps from paths to loaded images.
+ paths: HashMap<String, ImageId>,
+ /// Callback for loaded images.
+ on_load: Option<Box<dyn Fn(ImageId, &Image)>>,
+}
+
+impl ImageCache {
+ /// Create a new, empty image cache.
+ pub fn new() -> Self {
+ Self {
+ images: vec![],
+ paths: HashMap::new(),
+ on_load: None,
+ }
+ }
+
+ /// Load and decode an image file from a path.
+ pub fn load(&mut self, loader: &mut dyn Loader, path: &str) -> Option<ImageId> {
+ Some(match self.paths.entry(path.to_string()) {
+ Entry::Occupied(entry) => *entry.get(),
+ Entry::Vacant(entry) => {
+ let buffer = loader.load_file(path)?;
+ let image = Image::parse(&buffer)?;
+ let id = ImageId(self.images.len() as u32);
+ if let Some(callback) = &self.on_load {
+ callback(id, &image);
+ }
+ self.images.push(image);
+ *entry.insert(id)
+ }
+ })
+ }
+
+ /// 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 [`load()`](Self::load).
+ #[track_caller]
+ pub fn get(&self, id: ImageId) -> &Image {
+ &self.images[id.0 as usize]
+ }
+
+ /// Register a callback which is invoked each time an image is loaded.
+ pub fn on_load<F>(&mut self, f: F)
+ where
+ F: Fn(ImageId, &Image) + 'static,
+ {
+ self.on_load = Some(Box::new(f));
+ }
+}
+
+/// A unique identifier for a loaded image.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
+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 fn from_raw(v: u32) -> Self {
+ Self(v)
+ }
+
+ /// Convert into the raw underlying value.
+ pub fn into_raw(self) -> u32 {
+ self.0
+ }
+}