//! A library providing a uniform interface to several image formats. //! //! How about a quick example? //! //! ```rust //! use plumers::prelude::*; //! //! // A 5x5 GIF, containing pure white and one magenta pixel in the middle. //! const GIF: [u8; 38] = [ //! 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x05, 0x00, //! 0x05, 0x00, 0x80, 0x01, 0x00, 0xff, 0x00, 0xff, //! 0xff, 0xff, 0xff, 0x2c, 0x00, 0x00, 0x00, 0x00, //! 0x05, 0x00, 0x05, 0x00, 0x00, 0x02, 0x05, 0x8c, //! 0x8f, 0x06, 0xc9, 0x51, 0x00, 0x3b, //! ]; //! //! let mut img = //! PalettedImage32::load(GIF, Default::default(), AlphaMode::ZeroIsTransparent).unwrap(); //! assert_eq!(img.nb_frames(), 1); //! //! // Swap the green and blue components of each colour. //! for color in img.palette_mut() { //! let (red, green, blue, alpha) = color.decode(); //! *color = Rgb32::encode(red, blue, green, alpha); //! } //! //! // Also set the top row of pixels to be yellow as well. //! // (This shows how to mutate the image in-place.) //! //! // It's possible to create colours directly, if desired. (CSS-like notation is used here.) //! assert_eq!(img.palette()[0], Rgb32(0xFFFF00FFu32.swap_bytes())); //! //! let width = img.width(); //! let mut frame = img.frame_mut(0); //! for x in 0..width { //! frame[(x, 0)] = 0; // Index 0 maps to yellow, as asserted above. //! } //! //! img.set_format(ImageFormat::Bmp); //! # if false { // Do not actually access the file system, please. //! img.store("test.bmp").unwrap(); //! # } //! ``` //! //! # Elevator pitch //! //! This crate is a wrapper around [libplum], so its documentation may come in handy from time to time; this documentation occasionally references it. //! //! libplum is a small (~8000 lines of code), entirely self-contained library, fully supporting the [GIF, BMP, PNG, APNG, JPEG, and PNM formats][prelude::ImageFormat]. //! It does *not* provide any format-specific APIs, instead opting for a more streamlined, one-size-fits-all API; but it *does* handle animated formats. //! //! Notably, formats are automatically detected when loading images, which makes loading quite convenient. //! Images are loaded into a pixel buffer, with a choice of [four different pixel formats][color]. //! //! Something quite unique to this library, unlike other format-agnostic designs, is first-class support for colour palettes. //! This can reduce memory usage, speed up whole-image operations, and let you access this information for some (arguably niche) use cases. //! //! While libplum is written in C, particular care has been put into correctly handling invalid data, and it's been extensively fuzz-tested. //! Therefore, it is reasonable to expect it to be memory-safe, yet also quite efficient. //! //! # Getting started //! //! If using this library, you'll likely want to manipulate images. //! The first order of business is [picking a *pixel format*][color#comparison], i.e. how the pixels will be stored, and whether you want your image to be paletted. //! //! Do you want palettes? | This type is for you: //! ----------------------|---------------------- //! Yes | [`PalettedImage`][prelude::PalettedImage] //! No | [`DirectImage`][prelude::DirectImage] //! Sometimes | [`DynImage`][prelude::DynImage] //! //! Then, obtaining an image; either create one from scratch using one of the `new*` methods, or `load` it. //! (If the image comes from a source you can't trust, for example if you're a web service, consider `load_limited` instead). //! //! [libplum]: http://github.com/aaaaaa123456789/libplum #![deny( clippy::undocumented_unsafe_blocks, missing_abi, missing_copy_implementations, missing_debug_implementations, missing_docs, single_use_lifetimes, trivial_numeric_casts, unused_lifetimes, unsafe_op_in_unsafe_fn, unused_unsafe, variant_size_differences )] use std::{ffi::CStr, fmt::Display, write}; pub mod color; pub mod image; /// For convenient access, one can simply `use plumers::prelude::*;`. pub mod prelude { pub use crate::color::{ColorFmt, Rgb16, Rgb32, Rgb32X, Rgb64}; pub use crate::image::{ AlphaMode, DirectImage, DirectImage16, DirectImage32, DirectImage32X, DirectImage64, DynImage, DynImage16, DynImage32, DynImage32X, DynImage64, Image, ImageFormat, ImageSource, LoadFlags, PaletteSort, PalettedImage, PalettedImage16, PalettedImage32, PalettedImage32X, PalettedImage64, }; } /// An error that can be generated by libplum. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] // libplum itself can add errors without breaking semver, let's do the same. pub enum Error { /// Invalid argument for function. InvalidArguments = libplum_sys::PLUM_ERR_INVALID_ARGUMENTS as _, /// Invalid image data or unknown format. InvalidFileFormat = libplum_sys::PLUM_ERR_INVALID_FILE_FORMAT as _, /// Invalid image metadata. InvalidMetadata = libplum_sys::PLUM_ERR_INVALID_METADATA as _, /// Invalid palette index. InvalidColorIndex = libplum_sys::PLUM_ERR_INVALID_COLOR_INDEX as _, /// Too many colors in image. TooManyColors = libplum_sys::PLUM_ERR_TOO_MANY_COLORS as _, /// Image palette not defined. UndefinedPalette = libplum_sys::PLUM_ERR_UNDEFINED_PALETTE as _, /// Image dimensions too large. ImageTooLarge = libplum_sys::PLUM_ERR_IMAGE_TOO_LARGE as _, /// Image contains no image data. NoData = libplum_sys::PLUM_ERR_NO_DATA as _, /// Multiple frames not supported. NoMultiFrame = libplum_sys::PLUM_ERR_NO_MULTI_FRAME as _, /// Could not access file. FileInaccessible = libplum_sys::PLUM_ERR_FILE_INACCESSIBLE as _, /// File input/output error. FileError = libplum_sys::PLUM_ERR_FILE_ERROR as _, /// Out of memory. OutOfMemory = libplum_sys::PLUM_ERR_OUT_OF_MEMORY as _, } impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // SAFETY: this function expects an error constant. let msg = unsafe { libplum_sys::plum_get_error_text(*self as _) }; // SAFETY: The returned messages are static strings, and they are all smaller than `isize::MAX`. // Additionally, the pointer is guaranteed to be non-NULL if the error constant is valid. let msg = unsafe { CStr::from_ptr::<'static>(msg) }.to_string_lossy(); write!(f, "{msg}") } } impl std::error::Error for Error {} /// `Result` pub type Result = core::result::Result; impl Error { fn from_raw(raw: libplum_sys::plum_errors) -> Self { match raw { libplum_sys::PLUM_ERR_INVALID_ARGUMENTS => Self::InvalidArguments, libplum_sys::PLUM_ERR_INVALID_FILE_FORMAT => Self::InvalidFileFormat, libplum_sys::PLUM_ERR_INVALID_METADATA => Self::InvalidMetadata, libplum_sys::PLUM_ERR_INVALID_COLOR_INDEX => Self::InvalidColorIndex, libplum_sys::PLUM_ERR_TOO_MANY_COLORS => Self::TooManyColors, libplum_sys::PLUM_ERR_UNDEFINED_PALETTE => Self::UndefinedPalette, libplum_sys::PLUM_ERR_IMAGE_TOO_LARGE => Self::ImageTooLarge, libplum_sys::PLUM_ERR_NO_DATA => Self::NoData, libplum_sys::PLUM_ERR_NO_MULTI_FRAME => Self::NoMultiFrame, libplum_sys::PLUM_ERR_FILE_ERROR => Self::FileError, libplum_sys::PLUM_ERR_OUT_OF_MEMORY => Self::OutOfMemory, raw => panic!("Unknown error value {raw} was returned"), } } } #[cfg(test)] mod tests { use std::{fs::File, ops::Deref}; use crate::{prelude::*, Error}; // Using one-letter constants here for conciseness. // Otherwise this takes *way* too much room. const Z: Rgb32 = Rgb32(0x000000FFu32.swap_bytes()); // Black. const W: Rgb32 = Rgb32(0xFFFFFFFFu32.swap_bytes()); // White. const R: Rgb32 = Rgb32(0xFF000080u32.swap_bytes()); // Red. const G: Rgb32 = Rgb32(0x00FF0080u32.swap_bytes()); // Green. const B: Rgb32 = Rgb32(0x0000FF80u32.swap_bytes()); // Blue. const Y: Rgb32 = Rgb32(0xFFFF0080u32.swap_bytes()); // Yellow. const TEST_SQUARE_PIXELS: [[Rgb32; 21]; 21] = [ [ Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, ], [ Z, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, Z, ], [ Z, W, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, W, Z, ], [ Z, W, Z, B, B, B, B, B, B, B, Z, R, R, R, R, R, R, R, Z, W, Z, ], [ Z, W, Z, B, B, B, B, B, B, B, Z, R, R, R, R, R, R, R, Z, W, Z, ], [ Z, W, Z, B, B, B, B, B, B, B, Z, R, R, R, R, R, R, R, Z, W, Z, ], [ Z, W, Z, B, B, B, B, B, B, B, Z, R, R, R, R, R, R, R, Z, W, Z, ], [ Z, W, Z, B, B, B, B, B, B, B, Z, R, R, R, R, R, R, R, Z, W, Z, ], [ Z, W, Z, B, B, B, B, B, B, B, Z, R, R, R, R, R, R, R, Z, W, Z, ], [ Z, W, Z, B, B, B, B, B, B, B, Z, R, R, R, R, R, R, R, Z, W, Z, ], [ Z, W, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, W, Z, ], [ Z, W, Z, Y, Y, Y, Y, Y, Y, Y, Z, G, G, G, G, G, G, G, Z, W, Z, ], [ Z, W, Z, Y, Y, Y, Y, Y, Y, Y, Z, G, G, G, G, G, G, G, Z, W, Z, ], [ Z, W, Z, Y, Y, Y, Y, Y, Y, Y, Z, G, G, G, G, G, G, G, Z, W, Z, ], [ Z, W, Z, Y, Y, Y, Y, Y, Y, Y, Z, G, G, G, G, G, G, G, Z, W, Z, ], [ Z, W, Z, Y, Y, Y, Y, Y, Y, Y, Z, G, G, G, G, G, G, G, Z, W, Z, ], [ Z, W, Z, Y, Y, Y, Y, Y, Y, Y, Z, G, G, G, G, G, G, G, Z, W, Z, ], [ Z, W, Z, Y, Y, Y, Y, Y, Y, Y, Z, G, G, G, G, G, G, G, Z, W, Z, ], [ Z, W, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, W, Z, ], [ Z, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, Z, ], [ Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, ], ]; #[test] fn load_test_square_unpaletted() { let file = File::open("libplum-sys/testsq.png").expect("Failed to open testsq.png!"); let image = DirectImage32::load(file, Default::default(), AlphaMode::ZeroIsTransparent) .expect("Failed to load testsq.png!"); assert_eq!(image.nb_frames(), 1); let mut frames = image.frames(); let frame = frames.next().unwrap(); assert!(frames.next().is_none()); assert!(frames.next().is_none()); // Lightly check that the iterator is indeed fused. assert_eq!(image.height(), TEST_SQUARE_PIXELS.len()); assert_eq!(image.width(), TEST_SQUARE_PIXELS[0].len()); for (y, row) in TEST_SQUARE_PIXELS.iter().enumerate() { for (x, &expected) in row.iter().enumerate() { let got = frame[(x, y)]; assert!(got == expected, "({x}, {y}): {got} != {expected}"); } } } #[test] fn load_test_square_paletted() { let file = File::open("libplum-sys/testsq.png").expect("Failed to open testsq.png!"); let image = PalettedImage32::load(file, Default::default(), AlphaMode::ZeroIsTransparent) .expect("Failed to load testsq.png!"); // The image is indexed, so its internal palette should have been preserved. assert_eq!(image.palette(), &[B, R, Y, G, W, Z]); assert_eq!(image.nb_frames(), 1); let mut frames = image.frames(); let frame = frames.next().unwrap(); assert!(frames.next().is_none()); assert!(frames.next().is_none()); // Lightly check that the iterator is indeed fused. assert_eq!(image.height(), TEST_SQUARE_PIXELS.len()); assert_eq!(image.width(), TEST_SQUARE_PIXELS[0].len()); for (y, row) in TEST_SQUARE_PIXELS.iter().enumerate() { for (x, &expected) in row.iter().enumerate() { let got = frame.pixel(x, y); assert!(got == expected, "({x}, {y}): {got} != {expected}"); } } } #[test] fn no_oom() { // Taken from libplum's `untrusted.md`. const EVIL: [u8; 43] = [ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x05, 0x00, 0x00, 0x01, 0x00, 0x2c, 0xff, 0x7f, 0xff, 0x7f, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b, ]; let result = DirectImage32::load_limited(EVIL, Default::default(), Default::default(), 1024 * 1024); let err = result .expect_err("Oversized GIF should have failed to load!") .into_inner() .expect("Oversized GIF should not have caused an I/O error!") .downcast::() .expect("Oversized GIF should have been rejected by libplum!"); assert_eq!(err.deref(), &Error::ImageTooLarge); } }