//! The meat of this crate. use std::{marker::PhantomData, mem::MaybeUninit, num::NonZeroUsize}; use crate::color::ColorFmt; mod frame; pub use frame::{Frame, FrameMut, Frames}; mod load; use load::PaletteMode; pub use load::{max_nb_pixels, ImageSource, Input, LoadFlags, PaletteSort}; mod metadata; use metadata::{Metadata, MetadataMut}; mod raw; use raw::PlumWrapper; mod store; pub use store::{ImageDest, Output}; /// An image whose pixels all directly contain their colour. /// /// Most of this type's API is provided by the [`Image`] trait. #[derive(Debug, Clone)] pub struct DirectImage(raw::ImgWrapper, PhantomData); // Convenience aliases for the supported color formats. /// `DirectImage` pub type DirectImage16 = DirectImage; /// `DirectImage` pub type DirectImage32 = DirectImage; /// `DirectImage` pub type DirectImage32X = DirectImage; /// `DirectImage` pub type DirectImage64 = DirectImage; /// An image whose pixels are all indices into a shared palette. /// /// Most of this type's API is provided by the [`Image`] trait. #[derive(Debug, Clone)] pub struct PalettedImage(raw::ImgWrapper, PhantomData); // Convenience aliases for the supported color formats. /// `PalettedImage` pub type PalettedImage16 = PalettedImage; /// `PalettedImage` pub type PalettedImage32 = PalettedImage; /// `PalettedImage` pub type PalettedImage32X = PalettedImage; /// `PalettedImage` pub type PalettedImage64 = PalettedImage; /// Either a [`DirectImage`], or a [`PalettedImage`]. /// /// Like both of these types, most of `DynImage`'s API is provided by the [`Image`] trait. #[derive(Debug, Clone)] pub enum DynImage { /// The image is not paletted. Direct(DirectImage), /// The image is paletted. Paletted(PalettedImage), } // Convenience aliases for the supported color formats. /// `DynImage` pub type DynImage16 = DynImage; /// `DynImage` pub type DynImage32 = DynImage; /// `DynImage` pub type DynImage32X = DynImage; /// `DynImage` pub type DynImage64 = DynImage; /// Common API between [`DirectImage`], [`PalettedImage`], and [`DynImage`]. /// /// This trait is [sealed], it cannot be implemented outside of this crate. /// /// [sealed]: https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed pub trait Image: PlumWrapper { /// Returns how many frames the image contains. /// /// [`frames()`][Self::frames()] and [`foreach_frame_mut()`][Self::foreach_frame_mut()] will both iterate exactly this many times. fn nb_frames(&self) -> usize; /// Returns how many pixels wide the image is. /// /// Note that all frames have the same size. fn width(&self) -> usize; /// Returns how many pixels tall the image is. /// /// Note that all frames have the same size. fn height(&self) -> usize; /// How many pixels are in each frame. fn nb_pixels(&self) -> usize { self.height() * self.width() } /// The image's format (PNG, JPEG...). /// /// This is set when it is loaded, and is also used to determine the format it should be stored as. fn format(&self) -> ImageFormat; /// Sets a different format to store the image as. fn set_format(&mut self, format: ImageFormat); /// The image's alpha mode. /// /// This is set when it is loaded, and is also used to determine how it should be stored. fn alpha_mode(&self) -> AlphaMode; /// Sets a different alpha mode to store the image as. /// /// Note that this does **not** alter the pixel data! /// Simply setting this without modifying the image's pixel data will effectively invert the alpha channel for the entire image. fn set_alpha_mode(&mut self, mode: AlphaMode); /// Retrieves the image's embedded colour palette, if there is one. fn palette(&self) -> Option<&[Fmt]>; /// Retrieves the pixel at the given position in the image. /// /// See also [`Frame::pixel()`], or consider indexing into a [`Frame`]. /// /// # Panics /// /// This function panics if any of `frame_idx`, `x`, or `y` is out of range. fn pixel(&self, frame_idx: usize, x: usize, y: usize) -> Fmt; /// Retrieves the pixel at the given position in the image. /// /// Consider also indexing into a [`FrameMut`]. /// /// # Paletted images /// /// Be careful: for paletted images, the reference returned is to the palette, so modifying it will alter **all** corresponding pixels in the image, across all frames. /// /// # Panics /// /// This function panics if any of `frame_idx`, `x`, or `y` is out of range. fn pixel_mut(&mut self, frame_idx: usize, x: usize, y: usize) -> &mut Fmt; /// Provides more convenient access to a specific frame of the image's. /// /// To use this with `&dyn Image`, consider using [`Frame::new`] instead. /// /// # Panics /// /// This function panics if `frame_idx` is out of range. fn frame(&self, frame_idx: usize) -> Frame<'_, Fmt, Self> where Self: Sized, { Frame::new(self, frame_idx) } /// Provides more convenient access to a specific frame of the image's. /// /// To use this with `&dyn Image`, consider using [`FrameMut::new`] instead. /// /// # Panics /// /// This function panics if `frame_idx` is out of range. fn frame_mut(&mut self, frame_idx: usize) -> FrameMut<'_, Fmt, Self> where Self: Sized, { FrameMut::new(self, frame_idx) } /// Iterates through the image's frames. fn frames(&self) -> Frames<'_, Fmt, Self> where Self: Sized, { Frames { img: self, begin: 0, end: self.nb_frames(), _pix_fmt: PhantomData, } } /// Iterates through the image's frames, with a callback that is allowed to mutate the image. /// /// An [`Iterator`] cannot be provided, because the returned [`FrameMut`]s may not outlive each iteration. fn foreach_frame_mut)>(&mut self, mut f: F) where Self: Sized, { for i in 0..self.nb_frames() { f(self.frame_mut(i)) } } /// Iterates through the image's frames, with a faillible callback that is allowed to mutate the image. /// /// An [`Iterator`] cannot be provided, because the returned [`FrameMut`]s may not outlive each iteration. fn try_foreach_frame_mut) -> Result<(), E>>( &mut self, mut f: F, ) -> Result<(), E> where Self: Sized, { for i in 0..self.nb_frames() { f(self.frame_mut(i))? } Ok(()) } /// Iterates through the image's metadata nodes. fn metadata(&self) -> Metadata<'_>; /// Iterates through the image's metadata nodes, allowing in-place modification of them. fn metadata_mut(&mut self) -> MetadataMut<'_>; /// Stores the image to [an image destination][ImageDest]. fn store(&self, output: Dest) -> std::io::Result where Self: Sized; } /// Controls the meaning of an image's alpha channel. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum AlphaMode { /// `0` = opaque, `::MAX` = transparent /// /// This is less conventional, but can be useful for applications that ignore alpha, /// since e.g. `new_zeroed` returns a fully opaque image instead of fully transparent. #[default] ZeroIsOpaque, /// `0` = transparent, `::MAX` = opaque /// /// This is the conventional meaning of "alpha". ZeroIsTransparent, } /// Indicates the file format the image used (when loading it) or will use (when storing it). /// /// For more information, see the libplum's [Supported file formats](http://github.com/aaaaaa123456789/libplum/blob/v1.2/docs/formats.md) page. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] // libplum itself can add new formats without breaking semver, let's do the same. pub enum ImageFormat { /// No image type. /// /// Used by an image for which an image type hasn't been specified, such as one just created by [`DirectImage::new_zeroed()`]. /// It may also be used as an explicit "no type" designator. None = libplum_sys::PLUM_IMAGE_NONE as isize, /// BMP (Windows Bitmap) file. Bmp = libplum_sys::PLUM_IMAGE_BMP as isize, /// GIF (CompuServe's Graphics Interchange Format) format, including both static images and animations. Gif = libplum_sys::PLUM_IMAGE_GIF as isize, /// PNG (Portable Network Graphics) file. Png = libplum_sys::PLUM_IMAGE_PNG as isize, /// Animated PNG file. /// /// Treated as a separate format because it is not actually compatible with PNG (due to using ancillary chunks to store critical animation information), which will cause APNG-unaware viewers and editors to handle it incorrectly. Apng = libplum_sys::PLUM_IMAGE_APNG as isize, /// JPEG (Joint Photographers Expert Group) file. Jpeg = libplum_sys::PLUM_IMAGE_JPEG as isize, /// netpbm's PNM (Portable Anymap) format. /// /// When loading, it represents any possible PNM file; however, only PPM and PAM files will be written. Pnm = libplum_sys::PLUM_IMAGE_PNM as isize, } impl DirectImage { /// Creates a new image, setting all of its pixel data to zeroes. /// /// Note that this will make the image all-opaque or all-transparent, depending on `alpha_mode`. pub fn new_zeroed( format: ImageFormat, alpha_mode: AlphaMode, nb_frames: usize, width: usize, height: usize, ) -> Self { let wrapper = raw::ImgWrapper::new_zeroed::( nb_frames.try_into().unwrap(), width.try_into().unwrap(), height.try_into().unwrap(), ); debug_assert_eq!(wrapper.palette, std::ptr::null_mut()); let mut this = Self(wrapper, PhantomData); this.set_format(format); this.set_alpha_mode(alpha_mode); this } /// Creates a new image, setting all of its pixel data by repeatedly calling a function. /// /// The function is passed the coordinates (frame, x, y) of the pixel it should init, every time it is called. pub fn from_fn Fmt>( format: ImageFormat, alpha_mode: AlphaMode, nb_frames: usize, width: usize, height: usize, mut init: F, ) -> Self { // SAFETY: the loop walks over every pixel, as is guaranteed by the assertion. let wrapper = unsafe { raw::ImgWrapper::new( nb_frames.try_into().unwrap(), width.try_into().unwrap(), height.try_into().unwrap(), |pixels| { debug_assert_eq!(nb_frames * height * width, pixels.len()); for frame in 0..nb_frames { for y in 0..height { for x in 0..width { pixels[(frame * height + y) * width + x].write(init(frame, x, y)); } } } }, ) }; debug_assert_eq!(wrapper.palette, std::ptr::null_mut()); let mut this = Self(wrapper, PhantomData); this.set_format(format); this.set_alpha_mode(alpha_mode); this } /// Creates a new image, initializing its pixel data in an user-defined way. /// /// This can be used if [`from_fn`][Self::from_fn]'s init order is not suitable; for example, /// if you'd like to paint the image non-linearly to improve cache locality and thus performance. /// /// # Safety /// /// The `init` function must initialize the whole provided slice. pub unsafe fn new])>( format: ImageFormat, alpha_mode: AlphaMode, nb_frames: usize, width: usize, height: usize, init: F, ) -> Self { // SAFETY: deferred to the caller. let wrapper = unsafe { raw::ImgWrapper::new::( nb_frames.try_into().unwrap(), width.try_into().unwrap(), height.try_into().unwrap(), init, ) }; debug_assert_eq!(wrapper.palette, std::ptr::null_mut()); let mut this = Self(wrapper, PhantomData); this.set_format(format); this.set_alpha_mode(alpha_mode); this } /// Loads an image from [an image source][ImageSource]. /// /// This will convert any image stored as paletted into a direct-color image. /// /// See [`DynImage::load`] if you don't want that. pub fn load( input: Src, flags: LoadFlags, alpha_mode: AlphaMode, ) -> std::io::Result { Self::load_limited(input, flags, alpha_mode, max_nb_pixels::()) } /// Loads an image from [an image source][ImageSource], limiting the amount of memory that can /// be allocated. /// /// This will convert any image stored as paletted into a direct-color image. /// /// See [`DynImage::load`] if you don't want that. /// /// ## Memory limit /// /// Note that the `nb_pixels_max` argument only limits how much memory is allocated for the pixel /// data itself, not for the entire image or for temporary buffers. /// /// The image struct, as well as the metadata nodes and other bookkeeping, can push the total /// allocation a little higher than that, though not much. pub fn load_limited( input: Src, flags: LoadFlags, alpha_mode: AlphaMode, nb_pixels_max: usize, ) -> std::io::Result { let image = input.load::(flags, alpha_mode, PaletteMode::None, nb_pixels_max)?; let wrapper = raw::ImgWrapper::from(image); debug_assert_eq!(wrapper.palette, std::ptr::null_mut()); Ok(Self(wrapper, PhantomData)) } /// Retrieves the pixel at the given position in the image. /// /// Consider also indexing into a [`Frame`]. pub fn pixel_at(&self, frame: usize, x: usize, y: usize) -> &Fmt { let height = self.height(); let width = self.width(); // SAFETY: this is a direct image, so the backing array is of `Fmt`s. &(unsafe { self.pix_array::() })[(frame * height + y) * width + x] } /// Retrieves the pixel at the given position in the image. /// /// Consider also indexing into a [`FrameMut`]. pub fn pixel_at_mut(&mut self, frame: usize, x: usize, y: usize) -> &mut Fmt { let height = self.height(); let width = self.width(); // SAFETY: this is a direct image, so the backing array is of `Fmt`s. &mut (unsafe { self.pix_array_mut::() })[(frame * height + y) * width + x] } } impl PalettedImage { /// Creates a new image, setting all of its pixel data to zeroes. /// /// Note that this will make the image all-opaque or all-transparent, depending on `alpha_mode`. /// /// This function can fail if the palette ends up being more than 256 items long; in that case, the length is returned as an error. pub fn new_zeroed>( format: ImageFormat, alpha_mode: AlphaMode, nb_frames: usize, width: usize, height: usize, palette: It, ) -> Result { let mut wrapper = raw::ImgWrapper::new_zeroed::( nb_frames.try_into().unwrap(), width.try_into().unwrap(), height.try_into().unwrap(), ); debug_assert_eq!(wrapper.palette, std::ptr::null_mut()); let palette_ptr = wrapper.collect(palette); wrapper.palette = palette_ptr.as_ptr().cast(); // SAFETY: `collect` returns a pointer to a properly initialized slice of elements. let slice_len = unsafe { palette_ptr.as_ref() }.len(); let Some(max_index) = slice_len.checked_sub(1).and_then(|max| max.try_into().ok()) else { return Err(slice_len); }; wrapper.max_palette_index = max_index; let mut this = Self(wrapper, PhantomData); this.set_format(format); this.set_alpha_mode(alpha_mode); Ok(this) } /// Creates a new image, setting all of its pixel data by repeatedly calling a function. /// /// The function is passed the coordinates (frame, x, y) of the pixel it should init, every time it is called. /// /// This function can fail if the palette ends up being more than 256 items long; in that case, the length is returned as an error. pub fn from_fn Fmt, It: IntoIterator>( format: ImageFormat, alpha_mode: AlphaMode, nb_frames: usize, width: usize, height: usize, mut init: F, palette: It, ) -> Result { // SAFETY: the loop walks over every pixel, as is guaranteed by the assertion. let mut wrapper = unsafe { raw::ImgWrapper::new( nb_frames.try_into().unwrap(), width.try_into().unwrap(), height.try_into().unwrap(), |pixels| { debug_assert_eq!(nb_frames * height * width, pixels.len()); for frame in 0..nb_frames { for y in 0..height { for x in 0..width { pixels[(frame * height + y) * width + x].write(init(frame, x, y)); } } } }, ) }; debug_assert_eq!(wrapper.palette, std::ptr::null_mut()); let palette_ptr = wrapper.collect(palette); wrapper.palette = palette_ptr.as_ptr().cast(); // SAFETY: `collect` returns a pointer to a properly initialized slice of elements. let slice_len = unsafe { palette_ptr.as_ref() }.len(); let Some(max_index) = slice_len.checked_sub(1).and_then(|max| max.try_into().ok()) else { return Err(slice_len); }; wrapper.max_palette_index = max_index; let mut this = Self(wrapper, PhantomData); this.set_format(format); this.set_alpha_mode(alpha_mode); Ok(this) } /// Creates a new image, initializing its pixel data in an user-defined way. /// /// This can be used if [`from_fn`][Self::from_fn]'s init order is not suitable; for example, /// if you'd like to paint the image non-linearly to improve cache locality and thus performance. /// /// This function can fail if the palette ends up being more than 256 items long; in that case, the length is returned as an error. /// /// # Safety /// /// The `init` function must initialize the whole provided slice. pub unsafe fn new]), It: IntoIterator>( format: ImageFormat, alpha_mode: AlphaMode, nb_frames: usize, width: usize, height: usize, init: F, palette: It, ) -> Result { // SAFETY: deferred to the caller. let mut wrapper = unsafe { raw::ImgWrapper::new::( nb_frames.try_into().unwrap(), width.try_into().unwrap(), height.try_into().unwrap(), init, ) }; debug_assert_eq!(wrapper.palette, std::ptr::null_mut()); let palette_ptr = wrapper.collect(palette); wrapper.palette = palette_ptr.as_ptr().cast(); // SAFETY: `collect` returns a pointer to a properly initialized slice of elements. let slice_len = unsafe { palette_ptr.as_ref() }.len(); let Some(max_index) = slice_len.checked_sub(1).and_then(|max| max.try_into().ok()) else { return Err(slice_len); }; wrapper.max_palette_index = max_index; let mut this = Self(wrapper, PhantomData); this.set_format(format); this.set_alpha_mode(alpha_mode); Ok(this) } /// Loads an image from [an image source][ImageSource]. /// /// This will attempt to generate a palette from direct-color images, and error out if palette generation fails (typically because the image contains more than 256 unique colors). /// /// See [`DynImage::load`] if you don't want that. pub fn load( input: Src, flags: LoadFlags, alpha_mode: AlphaMode, ) -> std::io::Result { Self::load_limited(input, flags, alpha_mode, max_nb_pixels::()) } /// Loads an image from [an image source][ImageSource], limiting the amount of memory that can /// be allocated. /// /// This will attempt to generate a palette from direct-color images, and error out if palette generation fails (typically because the image contains more than 256 unique colors). /// /// See [`DynImage::load`] if you don't want that. /// /// ## Memory limit /// /// Note that the `nb_pixels_max` argument only limits how much memory is allocated for the pixel /// data itself, not for the entire image or for temporary buffers. /// /// The image struct, as well as the metadata nodes and other bookkeeping, can push the total /// allocation a little higher than that, though not much. pub fn load_limited( input: Src, flags: LoadFlags, alpha_mode: AlphaMode, nb_pixels_max: usize, ) -> std::io::Result { let image = input.load::(flags, alpha_mode, PaletteMode::Force, nb_pixels_max)?; let wrapper = raw::ImgWrapper::from(image); debug_assert_ne!(wrapper.palette, std::ptr::null_mut()); Ok(Self(wrapper, PhantomData)) } /// Returns the image's palette. pub fn palette(&self) -> &[Fmt] { debug_assert_ne!(self.as_img().palette, std::ptr::null_mut()); // SAFETY: all `ColorFmt`s are `#[repr(transparent)]` and contain a single int, so casting is okay. let ptr = self.as_img().palette.cast(); let len = usize::from(self.as_img().max_palette_index) + 1; // SAFETY: the slice is guaranteed to be correctly initialized, aligned, etc by libplum and other Rust code. unsafe { std::slice::from_raw_parts(ptr, len) } } /// Returns the image's palette, for in-place modification. pub fn palette_mut(&mut self) -> &mut [Fmt] { debug_assert_ne!(self.as_img().palette, std::ptr::null_mut()); // SAFETY: all `ColorFmt`s are `#[repr(transparent)]` and contain a single int, so casting is okay. let ptr = self.as_img().palette.cast(); let len = usize::from(self.as_img().max_palette_index) + 1; // SAFETY: the slice is guaranteed to be correctly initialized, aligned, etc by libplum and other Rust code. unsafe { std::slice::from_raw_parts_mut(ptr, len) } } /// Retrieves a reference to the index at the given coordinates. /// /// Consider also indexing into a [`Frame`]. pub fn index_at(&self, frame: usize, x: usize, y: usize) -> &u8 { let height = self.height(); let width = self.width(); // SAFETY: this is an indexed image, so the backing array is of `u8`s. &(unsafe { self.pix_array::() })[(frame * height + y) * width + x] } /// Retrieves a mutable reference to the index at the given coordinates. /// /// Consider also indexing into a [`FrameMut`]. pub fn index_at_mut(&mut self, frame: usize, x: usize, y: usize) -> &mut u8 { let height = self.height(); let width = self.width(); // SAFETY: this is an indexed image, so the backing array is of `u8`s. &mut (unsafe { self.pix_array_mut::() })[(frame * height + y) * width + x] } } impl DynImage { /// Loads an image from [an image source][ImageSource]. /// /// If `prefer_palette` is `true`, this will attempt to generate a palette from a direct-color image; however, failure to do so (typically because the image contains more than 256 colours) is not considered an error, and instead returns a [`DirectImage`]. /// /// If you don't want to deal with the overhead of checking which image type is contained each time, consider either [`DirectImage::load()`] or [`PalettedImage::load()`]. pub fn load( input: Src, flags: LoadFlags, alpha_mode: AlphaMode, prefer_palette: bool, ) -> std::io::Result { Self::load_limited( input, flags, alpha_mode, prefer_palette, max_nb_pixels::(), ) } /// Loads an image from [an image source][ImageSource], limiting the amount of memory that can /// be allocated. /// /// If `prefer_palette` is `true`, this will attempt to generate a palette from a direct-color image; however, failure to do so (typically because the image contains more than 256 colours) is not considered an error, and instead returns a [`DirectImage`]. /// /// ## Memory limit /// /// Note that the `nb_pixels_max` argument only limits how much memory is allocated for the pixel /// data itself, not for the entire image or for temporary buffers. /// /// The image struct, as well as the metadata nodes and other bookkeeping, can push the total /// allocation a little higher than that, though not much. pub fn load_limited( input: Src, flags: LoadFlags, alpha_mode: AlphaMode, prefer_palette: bool, nb_pixels_max: usize, ) -> std::io::Result { let image = input.load::( flags, alpha_mode, if prefer_palette { PaletteMode::Generate } else { PaletteMode::Load }, nb_pixels_max, ); image.map(|img| { let wrapper = raw::ImgWrapper::from(img); if wrapper.palette.is_null() { DirectImage(wrapper, PhantomData).into() } else { PalettedImage(wrapper, PhantomData).into() } }) } /// Returns a trait object for accessing the [`Image`] API of the underlying image. /// /// Using this instead of simply casting `self as &dyn Image` is a little faster, because it inspects the enumeration only once instead of at every method call. pub fn as_dyn_image(&self) -> &dyn Image { match self { DynImage::Direct(img) => img, DynImage::Paletted(img) => img, } } /// Returns a trait object for accessing the [`Image`] API of the underlying image. /// /// Using this instead of simply casting `self as &mut dyn Image` is a little faster, because it inspects the enumeration only once instead of at every method call. pub fn as_mut_dyn_image(&mut self) -> &mut dyn Image { match self { DynImage::Direct(img) => img, DynImage::Paletted(img) => img, } } } impl From> for DynImage { fn from(value: DirectImage) -> Self { Self::Direct(value) } } impl From> for DynImage { fn from(value: PalettedImage) -> Self { Self::Paletted(value) } } // TODO(?) // impl From> for DirectImage {} // impl TryFrom> for PalettedImage {}