Improve API

This commit is contained in:
Michael Pfaff 2024-08-09 01:13:09 -04:00
parent da37d7a182
commit 0c2533efa8
2 changed files with 250 additions and 87 deletions

View File

@ -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"

View File

@ -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,)+
}
#[derive(Clone, Copy)]
#[repr(u8)]
pub enum ImageFormat {
impl $name {
fn from_image(value: image::$name) -> Option<Self> {
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,)+
}
}
}
};
}
ffi_enum!(ImageFormat {
Png,
Jpeg,
Gif,
@ -28,35 +55,9 @@ pub enum ImageFormat {
Farbfeld,
Avif,
Qoi,
}
});
impl ImageFormat {
fn from(value: image::ImageFormat) -> Option<Self> {
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<Self> {
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<NonNull<u8>>;
@ -104,63 +115,215 @@ pub union LoadedImage {
pub struct OkLoadedImage {
pub ptr: NonNull<u8>,
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<NonNull<Infallible>>,
// TODO: allocate a buffer to hold a message string and pass that back to the caller.
pub message: NonNull<u8>,
pub message_len: usize,
}
#[cold]
fn new_err_message(alloc: AllocFn, e: impl Into<Cow<'static, str>>) -> (NonNull<u8>, 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<Cow<'static, str>>) -> 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<NonNull<u8>>,
pub ok: OkEncodedImage,
pub err: ErrEncodedImage,
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct OkEncodedImage {
pub ptr: NonNull<u8>,
pub len: usize,
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct ErrEncodedImage {
pub marker: Option<NonNull<Infallible>>,
pub message: NonNull<u8>,
pub message_len: usize,
}
impl EncodedImage {
fn new_err(alloc: AllocFn, e: impl Into<Cow<'static, str>>) -> 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, info: ImageInfo {
LoadedImage {
ok: OkLoadedImage {
ptr: NonNull::from(output).cast(),
len: byte_count,
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]
|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(),
},
}
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)
}