Improve API
This commit is contained in:
parent
da37d7a182
commit
0c2533efa8
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[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]
|
[profile.release-lto]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
|
|
335
src/lib.rs
335
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)]
|
macro_rules! ffi_enum {
|
||||||
#[repr(C)]
|
($name:ident {
|
||||||
pub struct ImageInfo {
|
$($variant:ident),+ $(,)?
|
||||||
width: u32,
|
}) => {
|
||||||
height: u32,
|
#[derive(Debug, Clone, Copy)]
|
||||||
color_type: ColorType,
|
#[repr(u8)]
|
||||||
|
pub enum $name {
|
||||||
|
$($variant,)+
|
||||||
|
}
|
||||||
|
|
||||||
|
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,)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
ffi_enum!(ImageFormat {
|
||||||
#[repr(u8)]
|
|
||||||
pub enum ImageFormat {
|
|
||||||
Png,
|
Png,
|
||||||
Jpeg,
|
Jpeg,
|
||||||
Gif,
|
Gif,
|
||||||
|
@ -28,35 +55,9 @@ pub enum ImageFormat {
|
||||||
Farbfeld,
|
Farbfeld,
|
||||||
Avif,
|
Avif,
|
||||||
Qoi,
|
Qoi,
|
||||||
}
|
});
|
||||||
|
|
||||||
impl ImageFormat {
|
ffi_enum!(ColorType {
|
||||||
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 {
|
|
||||||
L8,
|
L8,
|
||||||
La8,
|
La8,
|
||||||
Rgb8,
|
Rgb8,
|
||||||
|
@ -67,26 +68,36 @@ pub enum ColorType {
|
||||||
Rgba16,
|
Rgba16,
|
||||||
Rgb32F,
|
Rgb32F,
|
||||||
Rgba32F,
|
Rgba32F,
|
||||||
}
|
});
|
||||||
|
|
||||||
impl ColorType {
|
ffi_enum!(ExtendedColorType {
|
||||||
fn from(value: image::ColorType) -> Option<Self> {
|
A8,
|
||||||
use image::ColorType::*;
|
L1,
|
||||||
Some(match value {
|
La1,
|
||||||
L8 => Self::L8,
|
Rgb1,
|
||||||
La8 => Self::La8,
|
Rgba1,
|
||||||
Rgb8 => Self::Rgb8,
|
L2,
|
||||||
Rgba8 => Self::Rgba8,
|
La2,
|
||||||
L16 => Self::L16,
|
Rgb2,
|
||||||
La16 => Self::La16,
|
Rgba2,
|
||||||
Rgb16 => Self::Rgb16,
|
L4,
|
||||||
Rgba16 => Self::Rgba16,
|
La4,
|
||||||
Rgb32F => Self::Rgb32F,
|
Rgb4,
|
||||||
Rgba32F => Self::Rgba32F,
|
Rgba4,
|
||||||
_ => return None,
|
L8,
|
||||||
})
|
La8,
|
||||||
}
|
Rgb8,
|
||||||
}
|
Rgba8,
|
||||||
|
L16,
|
||||||
|
La16,
|
||||||
|
Rgb16,
|
||||||
|
Rgba16,
|
||||||
|
Bgr8,
|
||||||
|
Bgra8,
|
||||||
|
Rgb32F,
|
||||||
|
Rgba32F,
|
||||||
|
Cmyk8,
|
||||||
|
});
|
||||||
|
|
||||||
/// The allocated region must be zeroed by the implementation.
|
/// The allocated region must be zeroed by the implementation.
|
||||||
pub type AllocFn = extern "system" fn(length: usize) -> Option<NonNull<u8>>;
|
pub type AllocFn = extern "system" fn(length: usize) -> Option<NonNull<u8>>;
|
||||||
|
@ -104,63 +115,215 @@ pub union LoadedImage {
|
||||||
pub struct OkLoadedImage {
|
pub struct OkLoadedImage {
|
||||||
pub ptr: NonNull<u8>,
|
pub ptr: NonNull<u8>,
|
||||||
pub len: usize,
|
pub len: usize,
|
||||||
pub info: ImageInfo,
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub color_type: ColorType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct ErrLoadedImage {
|
pub struct ErrLoadedImage {
|
||||||
pub marker: Option<NonNull<Infallible>>,
|
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 {
|
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 {
|
fn load_image_common(alloc: AllocFn, dec: impl ImageDecoder) -> LoadedImage {
|
||||||
let (width, height) = dec.dimensions();
|
let (width, height) = dec.dimensions();
|
||||||
let Some(color_type) = ColorType::from(dec.color_type()) else {
|
let Some(color_type) = ColorType::from_image(dec.color_type()) else {
|
||||||
return LoadedImage::NULL;
|
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 {
|
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 {
|
let Some(output) = alloc_slice(alloc, byte_count) else {
|
||||||
return LoadedImage::NULL;
|
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(e) = dec.read_image(output) {
|
||||||
if let Err(_) = dec.read_image(output) {
|
return LoadedImage::new_err(alloc, format!("Image decoding failed: {e}"));
|
||||||
return LoadedImage::NULL;
|
}
|
||||||
|
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]
|
#[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 input = unsafe { std::slice::from_raw_parts(input, input_len) };
|
||||||
let Ok(iformat) = image::guess_format(input) else {
|
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 {
|
match format {
|
||||||
Ok(ImageFormat::Jpeg) => {
|
Ok(ImageFormat::Jpeg) => {
|
||||||
JpegDecoder::new(Cursor::new(input))
|
JpegDecoder::new(Cursor::new(input)).map(|dec| load_image_common(alloc, dec))
|
||||||
.map(|dec| load_image_common(alloc, dec))
|
|
||||||
}
|
}
|
||||||
Ok(ImageFormat::Png) => {
|
Ok(ImageFormat::Png) => {
|
||||||
PngDecoder::new(Cursor::new(input))
|
PngDecoder::new(Cursor::new(input)).map(|dec| load_image_common(alloc, dec))
|
||||||
.map(|dec| load_image_common(alloc, dec))
|
|
||||||
}
|
}
|
||||||
Ok(ImageFormat::WebP) => {
|
Ok(ImageFormat::WebP) => {
|
||||||
WebPDecoder::new(Cursor::new(input))
|
WebPDecoder::new(Cursor::new(input)).map(|dec| load_image_common(alloc, dec))
|
||||||
.map(|dec| load_image_common(alloc, dec))
|
|
||||||
}
|
}
|
||||||
Err(format) => Err(ImageError::Unsupported(UnsupportedError::from(ImageFormatHint::Exact(format)))),
|
Err(format) => Err(ImageError::Unsupported(UnsupportedError::from(
|
||||||
Ok(_) => Err(ImageError::Unsupported(UnsupportedError::from(ImageFormatHint::Exact(iformat)))),
|
ImageFormatHint::Exact(format),
|
||||||
}.unwrap_or_else(#[cold] |_| LoadedImage::NULL)
|
))),
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue