Skip to content

Commit 6e36b1b

Browse files
authored
bitmap: Add compress()/compress_raw() fns since API 30 (#440)
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`.
1 parent 2412804 commit 6e36b1b

File tree

2 files changed

+226
-11
lines changed

2 files changed

+226
-11
lines changed

ndk/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
- **Breaking:** looper: Provide `event` value to file descriptor poll callback. (#435)
3333
- **Breaking:** `HardwareBufferFormat` is no longer exported from `hardware_buffer` and `native_window`, and can only be reached through the `hardware_buffer_format` module. (#436)
3434
- **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)
35+
- Add `DataSpace` type and relevant functions on `Bitmap` and `NativeWindow`. (#438)
36+
- bitmap: Add `Bitmap::compress()` and `Bitmap::compress_raw()` functions. (#440)
37+
- **Breaking:** Turn `BitmapError` into a `non_exhaustive` `enum`. (#440)
3538

3639
# 0.7.0 (2022-07-24)
3740

ndk/src/bitmap.rs

Lines changed: 223 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,47 @@
77
#![cfg(feature = "bitmap")]
88

99
use jni_sys::{jobject, JNIEnv};
10-
use num_enum::{IntoPrimitive, TryFromPrimitive, TryFromPrimitiveError};
11-
use std::mem::MaybeUninit;
10+
use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive, TryFromPrimitiveError};
11+
use std::{error, fmt, mem::MaybeUninit};
12+
use thiserror::Error;
1213

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

1819
#[repr(i32)]
19-
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20+
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
21+
#[non_exhaustive]
2022
pub enum BitmapError {
21-
Unknown,
2223
#[doc(alias = "ANDROID_BITMAP_RESULT_ALLOCATION_FAILED")]
2324
AllocationFailed = ffi::ANDROID_BITMAP_RESULT_ALLOCATION_FAILED,
2425
#[doc(alias = "ANDROID_BITMAP_RESULT_BAD_PARAMETER")]
2526
BadParameter = ffi::ANDROID_BITMAP_RESULT_BAD_PARAMETER,
2627
#[doc(alias = "ANDROID_BITMAP_RESULT_JNI_EXCEPTION")]
2728
JniException = ffi::ANDROID_BITMAP_RESULT_JNI_EXCEPTION,
29+
// Use the OK discriminant, as no-one will be able to call `as i32` and only has access to the
30+
// constants via `From` provided by `IntoPrimitive` which reads the contained value.
31+
#[num_enum(catch_all)]
32+
Unknown(i32) = ffi::AAUDIO_OK,
2833
}
2934

35+
impl fmt::Display for BitmapError {
36+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37+
write!(f, "{:?}", self)
38+
}
39+
}
40+
41+
impl error::Error for BitmapError {}
42+
3043
pub type Result<T, E = BitmapError> = std::result::Result<T, E>;
3144

3245
impl BitmapError {
3346
pub(crate) fn from_status(status: i32) -> Result<()> {
34-
Err(match status {
35-
ffi::ANDROID_BITMAP_RESULT_SUCCESS => return Ok(()),
36-
ffi::ANDROID_BITMAP_RESULT_ALLOCATION_FAILED => BitmapError::AllocationFailed,
37-
ffi::ANDROID_BITMAP_RESULT_BAD_PARAMETER => BitmapError::BadParameter,
38-
ffi::ANDROID_BITMAP_RESULT_JNI_EXCEPTION => BitmapError::JniException,
39-
_ => BitmapError::Unknown,
40-
})
47+
match status {
48+
ffi::ANDROID_BITMAP_RESULT_SUCCESS => Ok(()),
49+
x => Err(Self::from(x)),
50+
}
4151
}
4252
}
4353

@@ -153,6 +163,132 @@ impl Bitmap {
153163
Ok(HardwareBufferRef::from_ptr(non_null))
154164
}
155165
}
166+
167+
/// [Lock] the pixels in `self` and compress them as described by [`info()`].
168+
///
169+
/// Unlike [`compress_raw()`] this requires a [`Bitmap`] object (as `self`) backed by a
170+
/// [`jobject`].
171+
///
172+
/// # Parameters
173+
/// - `format`: [`BitmapCompressFormat`] to compress to.
174+
/// - `quality`: Hint to the compressor, `0-100`. The value is interpreted differently
175+
/// depending on [`BitmapCompressFormat`].
176+
/// - `compress_callback`: Closure that writes the compressed data. Will be called on the
177+
/// current thread, each time the compressor has compressed more data that is ready to be
178+
/// written. May be called more than once for each call to this method.
179+
///
180+
/// [Lock]: Self::lock_pixels()
181+
/// [`info()`]: Self::info()
182+
/// [`compress_raw()`]: Self::compress_raw()
183+
#[cfg(feature = "api-level-30")]
184+
#[doc(alias = "AndroidBitmap_compress")]
185+
pub fn compress<F: FnMut(&[u8]) -> Result<(), ()>>(
186+
&self,
187+
format: BitmapCompressFormat,
188+
quality: i32,
189+
compress_callback: F,
190+
) -> Result<(), BitmapCompressError> {
191+
let info = self.info()?;
192+
let data_space = self.data_space()?;
193+
let pixels = self.lock_pixels()?;
194+
// SAFETY: When lock_pixels() succeeds, assume it returns a valid pointer that stays
195+
// valid until we call unlock_pixels().
196+
let result = unsafe {
197+
Self::compress_raw(
198+
&info,
199+
data_space,
200+
pixels,
201+
format,
202+
quality,
203+
compress_callback,
204+
)
205+
};
206+
self.unlock_pixels()?;
207+
result
208+
}
209+
210+
/// Compress `pixels` as described by `info`.
211+
///
212+
/// Unlike [`compress()`] this takes a raw pointer to pixels and does not need a [`Bitmap`]
213+
/// object backed by a [`jobject`].
214+
///
215+
/// # Parameters
216+
/// - `info`: Description of the pixels to compress.
217+
/// - `data_space`: [`DataSpace`] describing the color space of the pixels. Should _not_ be
218+
/// [`DataSpace::Unknown`] [^1].
219+
/// - `pixels`: Pointer to pixels to compress.
220+
/// - `format`: [`BitmapCompressFormat`] to compress to.
221+
/// - `quality`: Hint to the compressor, `0-100`. The value is interpreted differently
222+
/// depending on [`BitmapCompressFormat`].
223+
/// - `compress_callback`: Closure that writes the compressed data. Will be called on the
224+
/// current thread, each time the compressor has compressed more data that is ready to be
225+
/// written. May be called more than once for each call to this method.
226+
///
227+
/// [`compress()`]: Self::compress()
228+
/// [^1]: <https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/apex/android_bitmap.cpp;l=275-279;drc=7ba5c2fb3d1e35eb37a9cc522b30ba51f49ea491>
229+
#[cfg(feature = "api-level-30")]
230+
#[doc(alias = "AndroidBitmap_compress")]
231+
pub unsafe fn compress_raw<F: FnMut(&[u8]) -> Result<(), ()>>(
232+
info: &BitmapInfo,
233+
data_space: DataSpace,
234+
pixels: *const std::ffi::c_void,
235+
format: BitmapCompressFormat,
236+
quality: i32,
237+
compress_callback: F,
238+
) -> Result<(), BitmapCompressError> {
239+
if data_space == DataSpace::Unknown {
240+
return Err(BitmapCompressError::DataSpaceUnknown);
241+
}
242+
243+
use std::{any::Any, ffi::c_void, panic::AssertUnwindSafe};
244+
struct CallbackState<F: FnMut(&[u8]) -> Result<(), ()>> {
245+
callback: F,
246+
panic: Option<Box<dyn Any + Send>>,
247+
}
248+
let mut cb_state = CallbackState::<F> {
249+
callback: compress_callback,
250+
panic: None,
251+
};
252+
253+
extern "C" fn compress_cb<F: FnMut(&[u8]) -> Result<(), ()>>(
254+
context: *mut c_void,
255+
data: *const c_void,
256+
size: usize,
257+
) -> bool {
258+
// SAFETY: This callback will only be called serially on a single thread. Both the
259+
// panic state and the FnMut context need to be available mutably.
260+
let cb_state = unsafe { context.cast::<CallbackState<F>>().as_mut() }.unwrap();
261+
let data = unsafe { std::slice::from_raw_parts(data.cast(), size) };
262+
let panic = std::panic::catch_unwind(AssertUnwindSafe(|| (cb_state.callback)(data)));
263+
match panic {
264+
Ok(r) => r.is_ok(),
265+
Err(e) => {
266+
cb_state.panic = Some(e);
267+
false
268+
}
269+
}
270+
}
271+
272+
let status = unsafe {
273+
ffi::AndroidBitmap_compress(
274+
&info.inner,
275+
u32::from(data_space)
276+
.try_into()
277+
.expect("i32 overflow in DataSpace"),
278+
pixels,
279+
format as i32,
280+
quality,
281+
<*mut _>::cast(&mut cb_state),
282+
Some(compress_cb::<F>),
283+
)
284+
};
285+
286+
if let Some(panic) = cb_state.panic {
287+
std::panic::resume_unwind(panic)
288+
}
289+
290+
Ok(BitmapError::from_status(status)?)
291+
}
156292
}
157293

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

242378
impl BitmapInfo {
379+
pub fn new(width: u32, height: u32, stride: u32, format: BitmapFormat) -> Self {
380+
Self {
381+
inner: ffi::AndroidBitmapInfo {
382+
width,
383+
height,
384+
stride,
385+
format: u32::from(format) as i32,
386+
flags: 0,
387+
},
388+
}
389+
}
390+
391+
#[cfg(feature = "api-level-30")]
392+
pub fn new_with_flags(
393+
width: u32,
394+
height: u32,
395+
stride: u32,
396+
format: BitmapFormat,
397+
flags: BitmapInfoFlags,
398+
) -> Self {
399+
Self {
400+
inner: ffi::AndroidBitmapInfo {
401+
flags: flags.0,
402+
..Self::new(width, height, stride, format).inner
403+
},
404+
}
405+
}
406+
243407
/// The bitmap width in pixels.
244408
pub fn width(&self) -> u32 {
245409
self.inner.width
@@ -280,3 +444,51 @@ impl BitmapInfo {
280444
BitmapInfoFlags(self.inner.flags)
281445
}
282446
}
447+
448+
/// Specifies the formats that can be compressed to with [`Bitmap::compress()`] and
449+
/// [`Bitmap::compress_raw()`].
450+
#[cfg(feature = "api-level-30")]
451+
#[repr(u32)]
452+
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
453+
#[doc(alias = "AndroidBitmapCompressFormat")]
454+
pub enum BitmapCompressFormat {
455+
/// Compress to the JPEG format.
456+
///
457+
/// quality of `0` means compress for the smallest size. `100` means compress for max visual
458+
/// quality.
459+
#[doc(alias = "ANDROID_BITMAP_COMPRESS_FORMAT_JPEG")]
460+
Jpeg = ffi::AndroidBitmapCompressFormat::ANDROID_BITMAP_COMPRESS_FORMAT_JPEG.0,
461+
/// Compress to the PNG format.
462+
///
463+
/// PNG is lossless, so quality is ignored.
464+
#[doc(alias = "ANDROID_BITMAP_COMPRESS_FORMAT_PNG")]
465+
Png = ffi::AndroidBitmapCompressFormat::ANDROID_BITMAP_COMPRESS_FORMAT_PNG.0,
466+
/// Compress to the WEBP lossless format.
467+
///
468+
/// quality refers to how much effort to put into compression. A value of `0` means to
469+
/// compress quickly, resulting in a relatively large file size. `100` means to spend more time
470+
/// compressing, resulting in a smaller file.
471+
#[doc(alias = "ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSY")]
472+
WebPLossy = ffi::AndroidBitmapCompressFormat::ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSY.0,
473+
/// Compress to the WEBP lossy format.
474+
///
475+
/// quality of `0` means compress for the smallest size. `100` means compress for max visual quality.
476+
#[doc(alias = "ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSLESS")]
477+
WebPLossless = ffi::AndroidBitmapCompressFormat::ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSLESS.0,
478+
}
479+
480+
/// Encapsulates possible errors returned by [`Bitmap::compress()`] or [`Bitmap::compress_raw()`].
481+
#[derive(Debug, Error)]
482+
pub enum BitmapCompressError {
483+
#[error(transparent)]
484+
BitmapError(#[from] BitmapError),
485+
/// Only returned when [`Bitmap::compress()`] fails to read a valid [`DataSpace`] via
486+
/// [`Bitmap::data_space()`].
487+
#[error(transparent)]
488+
DataSpaceFromPrimitiveError(#[from] TryFromPrimitiveError<DataSpace>),
489+
/// [`Bitmap`] compression requires a known [`DataSpace`]. [`DataSpace::Unknown`] is invalid
490+
/// even though it is typically treated as `sRGB`, for that [`DataSpace::Srgb`] has to be passed
491+
/// explicitly.
492+
#[error("The dataspace for this Bitmap is Unknown")]
493+
DataSpaceUnknown,
494+
}

0 commit comments

Comments
 (0)