Skip to content

Commit

Permalink
bitmap: Add compress()/compress_raw() fns since API 30
Browse files Browse the repository at this point in the history
Map the new compression functions that are available in API level 30,
which also take a `DataSpace` that has recently been added in #438.

Also turn `BitmapError` into a proper `non_exhaustive` `enum` with
`num_enum`'s `catch_all` parser, similar to `MediaError`.
  • Loading branch information
MarijnS95 committed Oct 14, 2023
1 parent 2412804 commit 63a5239
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 11 deletions.
3 changes: 3 additions & 0 deletions ndk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
- **Breaking:** looper: Provide `event` value to file descriptor poll callback. (#435)
- **Breaking:** `HardwareBufferFormat` is no longer exported from `hardware_buffer` and `native_window`, and can only be reached through the `hardware_buffer_format` module. (#436)
- **Breaking:** `get_` prefixes have been removed from all public functions in light of the [C-GETTER](https://rust-lang.github.io/api-guidelines/naming.html#getter-names-follow-rust-convention-c-getter) convention. (#437)
- Add `DataSpace` type and relevant functions on `Bitmap` and `NativeWindow`. (#438)
- bitmap: Add `Bitmap::compress()` and `Bitmap::compress_raw()` functions. (#440)
- **Breaking:** Turn `BitmapError` into a `non_exhaustive` `enum`. (#440)

# 0.7.0 (2022-07-24)

Expand Down
234 changes: 223 additions & 11 deletions ndk/src/bitmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,47 @@
#![cfg(feature = "bitmap")]

use jni_sys::{jobject, JNIEnv};
use num_enum::{IntoPrimitive, TryFromPrimitive, TryFromPrimitiveError};
use std::mem::MaybeUninit;
use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive, TryFromPrimitiveError};
use std::{error, fmt, mem::MaybeUninit};
use thiserror::Error;

#[cfg(feature = "api-level-30")]
use crate::data_space::DataSpace;
#[cfg(feature = "api-level-30")]
use crate::hardware_buffer::HardwareBufferRef;

#[repr(i32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
#[non_exhaustive]
pub enum BitmapError {
Unknown,
#[doc(alias = "ANDROID_BITMAP_RESULT_ALLOCATION_FAILED")]
AllocationFailed = ffi::ANDROID_BITMAP_RESULT_ALLOCATION_FAILED,
#[doc(alias = "ANDROID_BITMAP_RESULT_BAD_PARAMETER")]
BadParameter = ffi::ANDROID_BITMAP_RESULT_BAD_PARAMETER,
#[doc(alias = "ANDROID_BITMAP_RESULT_JNI_EXCEPTION")]
JniException = ffi::ANDROID_BITMAP_RESULT_JNI_EXCEPTION,
// Use the OK discriminant, as no-one will be able to call `as i32` and only has access to the
// constants via `From` provided by `IntoPrimitive` which reads the contained value.
#[num_enum(catch_all)]
Unknown(i32) = ffi::AAUDIO_OK,
}

impl fmt::Display for BitmapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}

impl error::Error for BitmapError {}

pub type Result<T, E = BitmapError> = std::result::Result<T, E>;

impl BitmapError {
pub(crate) fn from_status(status: i32) -> Result<()> {
Err(match status {
ffi::ANDROID_BITMAP_RESULT_SUCCESS => return Ok(()),
ffi::ANDROID_BITMAP_RESULT_ALLOCATION_FAILED => BitmapError::AllocationFailed,
ffi::ANDROID_BITMAP_RESULT_BAD_PARAMETER => BitmapError::BadParameter,
ffi::ANDROID_BITMAP_RESULT_JNI_EXCEPTION => BitmapError::JniException,
_ => BitmapError::Unknown,
})
match status {
ffi::ANDROID_BITMAP_RESULT_SUCCESS => Ok(()),
x => Err(Self::from(x)),
}
}
}

Expand Down Expand Up @@ -153,6 +163,132 @@ impl Bitmap {
Ok(HardwareBufferRef::from_ptr(non_null))
}
}

/// [Lock] the pixels in `self` and compress them as described by [`info()`].
///
/// Unlike [`compress_raw()`] this requires a [`Bitmap`] object (as `self`) backed by a
/// [`jobject`].
///
/// # Parameters
/// - `format`: [`BitmapCompressFormat`] to compress to.
/// - `quality`: Hint to the compressor, `0-100`. The value is interpreted differently
/// depending on [`BitmapCompressFormat`].
/// - `compress_callback`: Closure that writes the compressed data. Will be called on the
/// current thread, each time the compressor has compressed more data that is ready to be
/// written. May be called more than once for each call to this method.
///
/// [Lock]: Self::lock_pixels()
/// [`info()`]: Self::info()
/// [`compress_raw()`]: Self::compress_raw()
#[cfg(feature = "api-level-30")]
#[doc(alias = "AndroidBitmap_compress")]
pub fn compress<F: FnMut(&[u8]) -> Result<(), ()>>(
&self,
format: BitmapCompressFormat,
quality: i32,
compress_callback: F,
) -> Result<(), BitmapCompressError> {
let info = self.info()?;
let data_space = self.data_space()?;
let pixels = self.lock_pixels()?;
// SAFETY: When lock_pixels() succeeds, assume it returns a valid pointer that stays
// valid until we call unlock_pixels().
let result = unsafe {
Self::compress_raw(
&info,
data_space,
pixels,
format,
quality,
compress_callback,
)
};
self.unlock_pixels()?;
result
}

/// Compress `pixels` as described by `info`.
///
/// Unlike [`compress()`] this takes a raw pointer to pixels and does not need a [`Bitmap`]
/// object backed by a [`jobject`].
///
/// # Parameters
/// - `info`: Description of the pixels to compress.
/// - `data_space`: [`DataSpace`] describing the color space of the pixels. Should _not_ be
/// [`DataSpace::Unknown`] [^1].
/// - `pixels`: Pointer to pixels to compress.
/// - `format`: [`BitmapCompressFormat`] to compress to.
/// - `quality`: Hint to the compressor, `0-100`. The value is interpreted differently
/// depending on [`BitmapCompressFormat`].
/// - `compress_callback`: Closure that writes the compressed data. Will be called on the
/// current thread, each time the compressor has compressed more data that is ready to be
/// written. May be called more than once for each call to this method.
///
/// [`compress()`]: Self::compress()
/// [^1]: <https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/apex/android_bitmap.cpp;l=275-279;drc=7ba5c2fb3d1e35eb37a9cc522b30ba51f49ea491>
#[cfg(feature = "api-level-30")]
#[doc(alias = "AndroidBitmap_compress")]
pub unsafe fn compress_raw<F: FnMut(&[u8]) -> Result<(), ()>>(
info: &BitmapInfo,
data_space: DataSpace,
pixels: *const std::ffi::c_void,
format: BitmapCompressFormat,
quality: i32,
compress_callback: F,
) -> Result<(), BitmapCompressError> {
if data_space == DataSpace::Unknown {
return Err(BitmapCompressError::DataSpaceUnknown);
}

use std::{any::Any, ffi::c_void, panic::AssertUnwindSafe};
struct CallbackState<F: FnMut(&[u8]) -> Result<(), ()>> {
callback: F,
panic: Option<Box<dyn Any + Send>>,
}
let mut cb_state = CallbackState::<F> {
callback: compress_callback,
panic: None,
};

extern "C" fn compress_cb<F: FnMut(&[u8]) -> Result<(), ()>>(
context: *mut c_void,
data: *const c_void,
size: usize,
) -> bool {
// SAFETY: This callback will only be called serially on a single thread. Both the
// panic state and the FnMut context need to be available mutably.
let cb_state = unsafe { context.cast::<CallbackState<F>>().as_mut() }.unwrap();
let data = unsafe { std::slice::from_raw_parts(data.cast(), size) };
let panic = std::panic::catch_unwind(AssertUnwindSafe(|| (cb_state.callback)(data)));
match panic {
Ok(r) => r.is_ok(),
Err(e) => {
cb_state.panic = Some(e);
false
}
}
}

let status = unsafe {
ffi::AndroidBitmap_compress(
&info.inner,
u32::from(data_space)
.try_into()
.expect("i32 overflow in DataSpace"),
pixels,
format as i32,
quality,
<*mut _>::cast(&mut cb_state),
Some(compress_cb::<F>),
)
};

if let Some(panic) = cb_state.panic {
std::panic::resume_unwind(panic)
}

Ok(BitmapError::from_status(status)?)
}
}

/// Possible values for [`ffi::ANDROID_BITMAP_FLAGS_ALPHA_MASK`] within [`BitmapInfoFlags`]
Expand Down Expand Up @@ -240,6 +376,34 @@ impl std::fmt::Debug for BitmapInfo {
}

impl BitmapInfo {
pub fn new(width: u32, height: u32, stride: u32, format: BitmapFormat) -> Self {
Self {
inner: ffi::AndroidBitmapInfo {
width,
height,
stride,
format: u32::from(format) as i32,
flags: 0,
},
}
}

#[cfg(feature = "api-level-30")]
pub fn new_with_flags(
width: u32,
height: u32,
stride: u32,
format: BitmapFormat,
flags: BitmapInfoFlags,
) -> Self {
Self {
inner: ffi::AndroidBitmapInfo {
flags: flags.0,
..Self::new(width, height, stride, format).inner
},
}
}

/// The bitmap width in pixels.
pub fn width(&self) -> u32 {
self.inner.width
Expand Down Expand Up @@ -280,3 +444,51 @@ impl BitmapInfo {
BitmapInfoFlags(self.inner.flags)
}
}

/// Specifies the formats that can be compressed to with [`Bitmap::compress()`] and
/// [`Bitmap::compress_raw()`].
#[cfg(feature = "api-level-30")]
#[repr(u32)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[doc(alias = "AndroidBitmapCompressFormat")]
pub enum BitmapCompressFormat {
/// Compress to the JPEG format.
///
/// quality of `0` means compress for the smallest size. `100` means compress for max visual
/// quality.
#[doc(alias = "ANDROID_BITMAP_COMPRESS_FORMAT_JPEG")]
Jpeg = ffi::AndroidBitmapCompressFormat::ANDROID_BITMAP_COMPRESS_FORMAT_JPEG.0,
/// Compress to the PNG format.
///
/// PNG is lossless, so quality is ignored.
#[doc(alias = "ANDROID_BITMAP_COMPRESS_FORMAT_PNG")]
Png = ffi::AndroidBitmapCompressFormat::ANDROID_BITMAP_COMPRESS_FORMAT_PNG.0,
/// Compress to the WEBP lossless format.
///
/// quality refers to how much effort to put into compression. A value of `0` means to
/// compress quickly, resulting in a relatively large file size. `100` means to spend more time
/// compressing, resulting in a smaller file.
#[doc(alias = "ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSY")]
WebPLossy = ffi::AndroidBitmapCompressFormat::ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSY.0,
/// Compress to the WEBP lossy format.
///
/// quality of `0` means compress for the smallest size. `100` means compress for max visual quality.
#[doc(alias = "ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSLESS")]
WebPLossless = ffi::AndroidBitmapCompressFormat::ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSLESS.0,
}

/// Encapsulates possible errors returned by [`Bitmap::compress()`] or [`Bitmap::compress_raw()`].
#[derive(Debug, Error)]
pub enum BitmapCompressError {
#[error(transparent)]
BitmapError(#[from] BitmapError),
/// Only returned when [`Bitmap::compress()`] fails to read a valid [`DataSpace`] via
/// [`Bitmap::data_space()`].
#[error(transparent)]
DataSpaceFromPrimitiveError(#[from] TryFromPrimitiveError<DataSpace>),
/// [`Bitmap`] compression requires a known [`DataSpace`]. [`DataSpace::Unknown`] is invalid
/// even though it is typically treated as `sRGB`, for that [`DataSpace::Srgb`] has to be passed
/// explicitly.
#[error("The dataspace for this Bitmap is Unknown")]
DataSpaceUnknown,
}

0 comments on commit 63a5239

Please sign in to comment.