plumers 1.0.2

Multi-format image library with first-class support for paletted images
Documentation
//! 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<T, plumers::Error>`
pub type Result<T> = core::result::Result<T, Error>;

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::<Error>()
            .expect("Oversized GIF should have been rejected by libplum!");
        assert_eq!(err.deref(), &Error::ImageTooLarge);
    }
}