diff --git a/Cargo.toml b/Cargo.toml index 8c02880..833d3d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -image = { version = "0.25.1", default-features = false, features = ["jpeg", "png", "webp", "rayon"] } +image = { version = "0.25.1", git = "https://git.pfaff.dev/michael/image.git", default-features = false, features = ["jpeg", "png", "webp", "rayon"] } [profile.release-lto] inherits = "release" diff --git a/src/lib.rs b/src/lib.rs index 6229bb4..b68dba7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,45 @@ -use std::{convert::Infallible, io::Cursor, ptr::NonNull}; +use std::{borrow::Cow, convert::Infallible, io::Cursor, ptr::NonNull}; -use image::{codecs::{jpeg::JpegDecoder, png::PngDecoder, webp::WebPDecoder}, error::{ImageFormatHint, UnsupportedError}, ImageDecoder, ImageError}; +use image::{ + codecs::{ + jpeg::{JpegDecoder, JpegEncoder}, + png::{PngDecoder, PngEncoder}, + webp::{WebPDecoder, WebPEncoder}, + }, + error::{ImageFormatHint, UnsupportedError}, + ImageDecoder, ImageEncoder, ImageError, +}; -#[derive(Clone, Copy)] -#[repr(C)] -pub struct ImageInfo { - width: u32, - height: u32, - color_type: ColorType, +macro_rules! ffi_enum { + ($name:ident { + $($variant:ident),+ $(,)? + }) => { + #[derive(Debug, Clone, Copy)] + #[repr(u8)] + pub enum $name { + $($variant,)+ + } + + impl $name { + fn from_image(value: image::$name) -> Option { + use image::$name::*; + Some(match value { + $($variant => Self::$variant,)+ + _ => return None, + }) + } + + fn to_image(self) -> image::$name { + use image::$name::*; + match self { + $(Self::$variant => $variant,)+ + } + } + } + }; } -#[derive(Clone, Copy)] -#[repr(u8)] -pub enum ImageFormat { +ffi_enum!(ImageFormat { Png, Jpeg, Gif, @@ -28,35 +55,9 @@ pub enum ImageFormat { Farbfeld, Avif, Qoi, -} +}); -impl ImageFormat { - fn from(value: image::ImageFormat) -> Option { - use image::ImageFormat::*; - Some(match value { - Png => Self::Png, - Jpeg => Self::Jpeg, - Gif => Self::Gif, - WebP => Self::WebP, - Pnm => Self::Pnm, - Tiff => Self::Tiff, - Tga => Self::Tga, - Dds => Self::Dds, - Bmp => Self::Bmp, - Ico => Self::Ico, - Hdr => Self::Hdr, - OpenExr => Self::OpenExr, - Farbfeld => Self::Farbfeld, - Avif => Self::Avif, - Qoi => Self::Qoi, - _ => return None, - }) - } -} - -#[derive(Clone, Copy)] -#[repr(u8)] -pub enum ColorType { +ffi_enum!(ColorType { L8, La8, Rgb8, @@ -67,26 +68,36 @@ pub enum ColorType { Rgba16, Rgb32F, Rgba32F, -} +}); -impl ColorType { - fn from(value: image::ColorType) -> Option { - use image::ColorType::*; - Some(match value { - L8 => Self::L8, - La8 => Self::La8, - Rgb8 => Self::Rgb8, - Rgba8 => Self::Rgba8, - L16 => Self::L16, - La16 => Self::La16, - Rgb16 => Self::Rgb16, - Rgba16 => Self::Rgba16, - Rgb32F => Self::Rgb32F, - Rgba32F => Self::Rgba32F, - _ => return None, - }) - } -} +ffi_enum!(ExtendedColorType { + A8, + L1, + La1, + Rgb1, + Rgba1, + L2, + La2, + Rgb2, + Rgba2, + L4, + La4, + Rgb4, + Rgba4, + L8, + La8, + Rgb8, + Rgba8, + L16, + La16, + Rgb16, + Rgba16, + Bgr8, + Bgra8, + Rgb32F, + Rgba32F, + Cmyk8, +}); /// The allocated region must be zeroed by the implementation. pub type AllocFn = extern "system" fn(length: usize) -> Option>; @@ -104,63 +115,215 @@ pub union LoadedImage { pub struct OkLoadedImage { pub ptr: NonNull, pub len: usize, - pub info: ImageInfo, + pub width: u32, + pub height: u32, + pub color_type: ColorType, } #[derive(Clone, Copy)] #[repr(C)] pub struct ErrLoadedImage { pub marker: Option>, - // TODO: allocate a buffer to hold a message string and pass that back to the caller. + pub message: NonNull, + pub message_len: usize, +} + +#[cold] +fn new_err_message(alloc: AllocFn, e: impl Into>) -> (NonNull, usize) { + let message_tmp = match e.into() { + Cow::Owned(s) => s, + Cow::Borrowed(s) => return (NonNull::from(s.as_bytes()).cast(), s.len()), + }; + let Some(message) = alloc_slice(alloc, message_tmp.len()) else { + const FALLBACK_MESSAGE: &'static str = "Allocator returned an error, so I cannot provide you with the error message."; + return (NonNull::from(FALLBACK_MESSAGE.as_bytes()).cast(), FALLBACK_MESSAGE.len()) + }; + message.copy_from_slice(message_tmp.as_bytes()); + let len = message.len(); + (NonNull::from(message).cast(), len) } impl LoadedImage { - pub const NULL: Self = Self { err: ErrLoadedImage { marker: None } }; + fn new_err(alloc: AllocFn, e: impl Into>) -> Self { + let (message, message_len) = new_err_message(alloc, e); + Self { err: ErrLoadedImage { marker: None, message, message_len } } + } +} + +#[repr(C)] +pub union EncodedImage { + pub ptr: Option>, + + pub ok: OkEncodedImage, + pub err: ErrEncodedImage, +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct OkEncodedImage { + pub ptr: NonNull, + pub len: usize, +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct ErrEncodedImage { + pub marker: Option>, + pub message: NonNull, + pub message_len: usize, +} + +impl EncodedImage { + fn new_err(alloc: AllocFn, e: impl Into>) -> Self { + let (message, message_len) = new_err_message(alloc, e); + Self { err: ErrEncodedImage { marker: None, message, message_len } } + } +} + +#[derive(Clone, Copy)] +#[repr(u8)] +pub enum EncoderProfile { + /// Optimized for fastest encoding and least loss. Lossiness depends on format. + Fast, + /// Optimized for smallest output. More lossy than [`Fast`]. + Small, +} + +fn alloc_slice(alloc: AllocFn, len: usize) -> Option<&'static mut [u8]> { + let Some(data) = alloc(len) else { + return None; + }; + Some(unsafe { NonNull::slice_from_raw_parts(data, len).as_mut() }) } fn load_image_common(alloc: AllocFn, dec: impl ImageDecoder) -> LoadedImage { let (width, height) = dec.dimensions(); - let Some(color_type) = ColorType::from(dec.color_type()) else { - return LoadedImage::NULL; + let Some(color_type) = ColorType::from_image(dec.color_type()) else { + return LoadedImage::new_err(alloc, format!("Unsupported color type {:?}. This is a bug. Please report to the developers of libimage", dec.color_type())); }; let Ok(byte_count) = usize::try_from(dec.total_bytes()) else { - return LoadedImage::NULL; + return LoadedImage::new_err(alloc, format!("Unsupported decoded image size {}.", dec.total_bytes())); }; - let Some(output) = alloc(byte_count) else { - return LoadedImage::NULL; + let Some(output) = alloc_slice(alloc, byte_count) else { + return LoadedImage::new_err(alloc, "Allocation failed. This indicates that you have run out of memory or that there exists a bug in the caller of libimage::load_image."); }; - let output = unsafe { NonNull::slice_from_raw_parts(output, byte_count).as_mut() }; - if let Err(_) = dec.read_image(output) { - return LoadedImage::NULL; + if let Err(e) = dec.read_image(output) { + return LoadedImage::new_err(alloc, format!("Image decoding failed: {e}")); + } + LoadedImage { + ok: OkLoadedImage { + ptr: NonNull::from(output).cast(), + len: byte_count, + width, + height, + color_type, + }, } - LoadedImage { ok: OkLoadedImage { ptr: NonNull::from(output).cast(), len: byte_count, info: ImageInfo { - width, - height, - color_type, - } } } } #[no_mangle] -pub extern "system" fn load_image(input: *const u8, input_len: usize, alloc: AllocFn) -> LoadedImage { +pub extern "system" fn load_image( + input: *const u8, + input_len: usize, + alloc: AllocFn, +) -> LoadedImage { let input = unsafe { std::slice::from_raw_parts(input, input_len) }; let Ok(iformat) = image::guess_format(input) else { - return LoadedImage::NULL; + return LoadedImage::new_err(alloc, "Unable to guess the formmat of the input."); }; - let format = ImageFormat::from(iformat).ok_or(iformat); + let format = ImageFormat::from_image(iformat).ok_or(iformat); match format { Ok(ImageFormat::Jpeg) => { - JpegDecoder::new(Cursor::new(input)) - .map(|dec| load_image_common(alloc, dec)) + JpegDecoder::new(Cursor::new(input)).map(|dec| load_image_common(alloc, dec)) } Ok(ImageFormat::Png) => { - PngDecoder::new(Cursor::new(input)) - .map(|dec| load_image_common(alloc, dec)) + PngDecoder::new(Cursor::new(input)).map(|dec| load_image_common(alloc, dec)) } Ok(ImageFormat::WebP) => { - WebPDecoder::new(Cursor::new(input)) - .map(|dec| load_image_common(alloc, dec)) + WebPDecoder::new(Cursor::new(input)).map(|dec| load_image_common(alloc, dec)) } - Err(format) => Err(ImageError::Unsupported(UnsupportedError::from(ImageFormatHint::Exact(format)))), - Ok(_) => Err(ImageError::Unsupported(UnsupportedError::from(ImageFormatHint::Exact(iformat)))), - }.unwrap_or_else(#[cold] |_| LoadedImage::NULL) + Err(format) => Err(ImageError::Unsupported(UnsupportedError::from( + ImageFormatHint::Exact(format), + ))), + Ok(_) => Err(ImageError::Unsupported(UnsupportedError::from( + ImageFormatHint::Exact(iformat), + ))), + } + .unwrap_or_else( + #[cold] + |e| LoadedImage::new_err(alloc, format!("Image decoding failed: {e}")), + ) +} + +fn check_buffer_size(size: usize, width: u32, height: u32, color_type: ExtendedColorType) -> Result<(), String> { + let Some(expected_len) = (width as usize).checked_mul(height as usize) + .and_then(|x| x.checked_mul(color_type.to_image().bits_per_pixel() as usize)) + .map(|x| x.div_ceil(8)) else { + return Err(format!("{width}x{height} {color_type:?} image is too large for the host CPU architecture")); + }; + if size != expected_len { + return Err(format!("Expected a {width}x{height} {color_type:?} image to have a pixel buffer of length {expected_len}, but found {size}")); + } + Ok(()) +} + +#[no_mangle] +pub extern "system" fn encode_image( + input: *const u8, + input_len: usize, + alloc: AllocFn, + width: u32, + height: u32, + color_type: ExtendedColorType, + format: ImageFormat, + profile: EncoderProfile, +) -> EncodedImage { + if let Err(e) = check_buffer_size(input_len, width, height, color_type) { + return EncodedImage::new_err(alloc, e); + } + let input = unsafe { std::slice::from_raw_parts(input, input_len) }; + let mut buf = Vec::new(); + let r = match format { + ImageFormat::Jpeg => JpegEncoder::new_with_quality( + &mut buf, + match profile { + EncoderProfile::Fast => 100, + EncoderProfile::Small => 90, + }, + ) + .write_image(input, width, height, color_type.to_image()), + ImageFormat::Png => PngEncoder::new_with_quality( + &mut buf, + match profile { + EncoderProfile::Fast => image::codecs::png::CompressionType::Fast, + EncoderProfile::Small => image::codecs::png::CompressionType::Best, + }, + match profile { + EncoderProfile::Fast => image::codecs::png::FilterType::NoFilter, + EncoderProfile::Small => image::codecs::png::FilterType::Adaptive, + }, + ) + .write_image(input, width, height, color_type.to_image()), + ImageFormat::WebP => WebPEncoder::new_lossless(&mut buf).write_image( + input, + width, + height, + color_type.to_image(), + ), + _ => return EncodedImage::new_err(alloc, format!("Unsupported format for encoding: {format:?}")), + }; + if let Err(e) = r { + return EncodedImage::new_err(alloc, format!("Image encoding failed: {e}")); + } + + let Some(output) = alloc_slice(alloc, buf.len()) else { + return EncodedImage::new_err(alloc, "Allocation failed. This indicates that you have run out of memory or that there exists a bug in the caller of libimage::encode_image."); + }; + output.copy_from_slice(&buf); + EncodedImage { + ok: OkEncodedImage { + len: output.len(), + ptr: NonNull::from(output).cast(), + }, + } }