plumers 1.0.2

Multi-format image library with first-class support for paletted images
Documentation
use std::{
    ffi::{c_int, c_void},
    fs::File,
    io::{BufWriter, Write},
    mem::MaybeUninit,
    num::NonZeroUsize,
    path::Path,
};

use crate::{color::ColorFmt, Error};

/// An object that an image can be stored to.
///
/// This trait is designed to be implementable outside of this crate, and could in theory be used outside of it.
/// But since [`store()`][Self::store()] takes a raw [`plum_image`][libplum_sys::plum_image], it is not very useful outside of this crate.
/// It is mainly provided for convenience, for any code that wishes to be generic over accepted destinations.
///
/// # Provided impls
///
/// The crate provides three classes of `impl`s:
/// - [`Path`] (and [`str`], which is path-like): opens the named file, and writes to it.
/// - Anything that implements [`Write`] (see below for details).
/// - Callbacks, which are the most flexible but also the most verbose.
///
/// ## [`Write`]ables
///
/// It is possible for a type to implement both [`FnMut(&[u8]) -> io::Result<u16>`][FnMut] (which qualifies it as a callback) and [`Write`] (which qualifies it as a writable).
///
/// For the purpose of disambiguation (which has to be done all the time because of Rust's [orphan rules]), "writables" have to be wrapped in a newtype: [`Output`].
/// Please remember that if `T` implements [`Write`], so does `&mut T` (and [`Write::by_ref()`] is a convenience to help with that).
///
/// However, some writables have "shortcut" impls provided for convenience (so you can use [`File`] directly instead of `Output<File>`, for example).
/// They are strictly developer-convenience shortcuts, with one exception: [`&mut [u8]`][slice], which performs *better* if not wrapped in `Output`, for internal reasons.
///
/// In fact, "writables" are implemented in terms of callbacks, and provided for convenience's sake, since they're so common (and writing the callback each time would get tedious).
///
/// ## Callbacks
///
/// Trying to use a closure as callback may yield error `E0308`, `E0599`, or other errors about trait bounds:
///
/// ```rust,compile_fail
/// # use std::{io::Write, num::NonZeroUsize};
/// use plumers::prelude::*;
///
/// # fn blah(image: &DirectImage32, file: &mut std::fs::File) -> std::io::Result<NonZeroUsize> {
/// let callback = |buffer| file.write(buffer).map(|len| len as u16);
/// image.store(callback)
/// # }
/// ```
///
/// ```text
/// error[E0308]: mismatched types
///  --> plumers/src/load.rs:9:1
///   |
/// 9 | image.store(callback)
///   | ^^^^^^^^^^^^^^^^^^^ one type is more general than the other
///   |
///   = note: expected trait `for<'a> FnMut(&'a [u8])`
///              found trait `FnMut(&[u8])`
/// note: this closure does not fulfill the lifetime requirements
///  --> plumers/src/load.rs:8:16
///   |
/// 8 | let callback = |buffer| file.write(buffer).map(|len| len as u16);
///   |                ^^^^^^^^
/// help: consider specifying the type of the closure parameters
///   |
/// 8 | let callback = |buffer: &_| file.write(buffer).map(|len| len as u16);
///   |                ~~~~~~~~~~~~
/// ```
///
/// The fix is to explicitly specify the parameter's type:
///
/// ```rust
/// # use std::{io::Write, num::NonZeroUsize};
/// use plumers::prelude::*;
///
/// # fn blah(image: &DirectImage32, file: &mut std::fs::File) -> std::io::Result<NonZeroUsize> {
/// //                    ↓↓↓↓ add this
/// let callback = |buffer: &_| file.write(buffer).map(|len| len as u16);
/// image.store(callback)
/// # }
/// ```
///
/// (The implicit type's lifetime seems to be less general than the explicit type's lifetime, despite both being elided.)
///
/// [orphan rules]: <https://doc.rust-lang.org/stable/reference/items/implementations.html#orphan-rules>
pub trait ImageDest {
    /// Attempts to store a [`plum_image`][libplum_sys::plum_image] into this source.
    fn store<Fmt: ColorFmt>(self, image: &libplum_sys::plum_image)
        -> std::io::Result<NonZeroUsize>;
}

impl<F: FnMut(&[u8]) -> std::io::Result<u16>> ImageDest for F {
    fn store<Fmt: ColorFmt>(
        self,
        image: &libplum_sys::plum_image,
    ) -> std::io::Result<NonZeroUsize> {
        struct Userdata<E, F>(F, MaybeUninit<E>);

        let mut userdata = Userdata(self, MaybeUninit::<std::io::Error>::uninit());
        let callback_struct = libplum_sys::plum_callback {
            callback: Some(callback_wrapper::<F>),
            userdata: &mut userdata as *mut _ as *mut c_void,
        };
        extern "C" fn callback_wrapper<F: FnMut(&[u8]) -> std::io::Result<u16>>(
            userdata: *mut c_void,
            buffer: *mut c_void,
            buf_len: c_int,
        ) -> c_int {
            // First, round-trip the `callback` pointer back to the same as the outer `callback` argument.
            let userdata = userdata.cast::<Userdata<_, F>>();
            // SAFETY: the pointer is extracted from the outer function's `&mut userdata`, so it's safe to deref.
            let Userdata(callback, error_return) = unsafe { &mut *userdata };

            // Then, prepare the buffer.

            // SAFETY: we are just undoing C's type-erasure.
            let buffer = buffer.cast();
            // "The `buf_len` argument is the size of the supplied bufferm and it will always be positive and no larger than `0x7FFF`."
            let buf_len = buf_len as u16;
            // SAFETY: libplum guarantees that the buffer is initialised. Additionally, `u8` has no alignment requirements.
            let buffer = unsafe { std::slice::from_raw_parts(buffer, buf_len.into()) };

            // Then, simply call it!
            let ret = callback(buffer);

            match ret {
                Ok(size) => {
                    assert!(size <= buf_len, "Wrote more bytes than were available!?");
                    size.into()
                }
                Err(err) => {
                    // SAFETY: since we return a negative value, libplum will not call us again;
                    //         therefore, we can't be overwriting something already initialized.
                    error_return.write(err);
                    -1
                }
            }
        }

        let ptr = &callback_struct as *const _;
        // TODO: actually, this can be `MaybeUninit`, which could improve codegen by skipping init of this var?
        let mut error_id: std::ffi::c_uint = 0;
        // SAFETY:
        //  -  `ptr` points to a valid C-ABI callback
        //  - the `mode` arg is correctly CALLBACK
        let ret = unsafe {
            libplum_sys::plum_store_image(
                image as *const _,
                ptr as *mut c_void,
                libplum_sys::PLUM_MODE_CALLBACK,
                &mut error_id as _,
            )
        };

        NonZeroUsize::new(ret).ok_or_else(|| {
            #[allow(trivial_numeric_casts)] // Necessary on some platforms, superfluous on others.
            let error = Error::from_raw(error_id as _);
            std::io::Error::new(std::io::ErrorKind::Other, error)
        })
    }
}

/// This struct is required to work around Rust's "[orphan rules]".
/// See [`ImageDest`].
///
/// Wrap any [`Write`]able you want to load an image from in this structure.
///
/// [orphan rules]: <https://doc.rust-lang.org/stable/reference/items/implementations.html#orphan-rules>
#[derive(Debug)]
pub struct Output<W: Write>(pub W);
impl<W: Write> From<W> for Output<W> {
    fn from(value: W) -> Self {
        Self(value)
    }
}

impl<W: Write> ImageDest for Output<W> {
    fn store<Fmt: ColorFmt>(
        mut self,
        image: &libplum_sys::plum_image,
    ) -> std::io::Result<NonZeroUsize> {
        let callback = |buffer: &[u8]| {
            self.0
                .write(buffer)
                // The `as u16` truncation is always OK, because libplum gives us a suitably-sized buffer.
                .map(|len| len as u16)
        };
        callback.store::<Fmt>(image)
    }
}

macro_rules! write_shortcut {
    ($(
        impl$(< $($ty_param:ident $(: $bound:tt $(+ $bounds:tt)* )? ),+ $(,)? >)?
        ImageDest for $t:ty {}
    )*) => {$(
        /// Convenience shorthand for the generic [`std::io::Write`] impl.
        impl$(< $($ty_param $(: $bound $(+ $bounds)* )? ),+ >)?
        ImageDest for $t {
            fn store<Fmt: ColorFmt>(
                self,
                image: &libplum_sys::plum_image,
            ) -> std::io::Result<NonZeroUsize> {
                Output(self).store::<Fmt>(image)
            }
        }
    )*};
}
write_shortcut! {
    impl ImageDest for File {}
    impl ImageDest for &File {}
    impl ImageDest for &mut File {}

    impl<W: Write> ImageDest for BufWriter<W> {}
    impl<W: Write> ImageDest for &mut BufWriter<W> {}
}

/// This implementation should be preferred over using the generic [`Write`] (or any of its shortcuts), as it has **less** overhead.
impl ImageDest for &mut [u8] {
    fn store<Fmt: ColorFmt>(
        self,
        image: &libplum_sys::plum_image,
    ) -> std::io::Result<NonZeroUsize> {
        let buffer = self as *mut _;
        let size = self.len();

        let mut error_id: std::ffi::c_uint = 0;
        // SAFETY: `buffer` points to a `size`-byte buffer.
        let ret = unsafe {
            libplum_sys::plum_store_image(
                image as *const _,
                buffer as *mut c_void,
                size,
                &mut error_id as _,
            )
        };

        NonZeroUsize::new(ret).ok_or_else(|| {
            #[allow(trivial_numeric_casts)] // Necessary on some platforms, superfluous on others.
            let error = Error::from_raw(error_id as _);
            std::io::Error::new(std::io::ErrorKind::Other, error)
        })
    }
}

impl ImageDest for &Path {
    fn store<Fmt: ColorFmt>(
        self,
        image: &libplum_sys::plum_image,
    ) -> std::io::Result<NonZeroUsize> {
        let file = File::create(self)?;
        Output(file).store::<Fmt>(image)
    }
}

impl ImageDest for &str {
    fn store<Fmt: ColorFmt>(
        self,
        image: &libplum_sys::plum_image,
    ) -> std::io::Result<NonZeroUsize> {
        let file = File::create(self)?;
        Output(file).store::<Fmt>(image)
    }
}