|
7 | 7 | #![cfg(feature = "bitmap")] |
8 | 8 |
|
9 | 9 | 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; |
12 | 13 |
|
13 | 14 | #[cfg(feature = "api-level-30")] |
14 | 15 | use crate::data_space::DataSpace; |
15 | 16 | #[cfg(feature = "api-level-30")] |
16 | 17 | use crate::hardware_buffer::HardwareBufferRef; |
17 | 18 |
|
18 | 19 | #[repr(i32)] |
19 | | -#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 20 | +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive, IntoPrimitive)] |
| 21 | +#[non_exhaustive] |
20 | 22 | pub enum BitmapError { |
21 | | - Unknown, |
22 | 23 | #[doc(alias = "ANDROID_BITMAP_RESULT_ALLOCATION_FAILED")] |
23 | 24 | AllocationFailed = ffi::ANDROID_BITMAP_RESULT_ALLOCATION_FAILED, |
24 | 25 | #[doc(alias = "ANDROID_BITMAP_RESULT_BAD_PARAMETER")] |
25 | 26 | BadParameter = ffi::ANDROID_BITMAP_RESULT_BAD_PARAMETER, |
26 | 27 | #[doc(alias = "ANDROID_BITMAP_RESULT_JNI_EXCEPTION")] |
27 | 28 | 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, |
28 | 33 | } |
29 | 34 |
|
| 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 | + |
30 | 43 | pub type Result<T, E = BitmapError> = std::result::Result<T, E>; |
31 | 44 |
|
32 | 45 | impl BitmapError { |
33 | 46 | 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 | + } |
41 | 51 | } |
42 | 52 | } |
43 | 53 |
|
@@ -153,6 +163,132 @@ impl Bitmap { |
153 | 163 | Ok(HardwareBufferRef::from_ptr(non_null)) |
154 | 164 | } |
155 | 165 | } |
| 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 | + } |
156 | 292 | } |
157 | 293 |
|
158 | 294 | /// Possible values for [`ffi::ANDROID_BITMAP_FLAGS_ALPHA_MASK`] within [`BitmapInfoFlags`] |
@@ -240,6 +376,34 @@ impl std::fmt::Debug for BitmapInfo { |
240 | 376 | } |
241 | 377 |
|
242 | 378 | 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 | + |
243 | 407 | /// The bitmap width in pixels. |
244 | 408 | pub fn width(&self) -> u32 { |
245 | 409 | self.inner.width |
@@ -280,3 +444,51 @@ impl BitmapInfo { |
280 | 444 | BitmapInfoFlags(self.inner.flags) |
281 | 445 | } |
282 | 446 | } |
| 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