lum_platform_image_converter/lib/src/android/native.dart

110 lines
3.8 KiB
Dart

import 'dart:typed_data';
import 'package:jni/jni.dart';
import 'package:platform_image_converter/src/android/bindings.g.dart';
import 'package:platform_image_converter/src/image_conversion_exception.dart';
import 'package:platform_image_converter/src/image_converter_platform_interface.dart';
import 'package:platform_image_converter/src/output_format.dart';
import 'package:platform_image_converter/src/output_resize.dart';
/// Android image converter using BitmapFactory and Bitmap compression.
///
/// Implements image conversion for Android 9+ (API level 28+) platforms using
/// BitmapFactory for decoding and Bitmap.compress for encoding via JNI.\n///
/// **Features:**
/// - Supports JPEG, PNG, WebP, GIF, BMP input formats
/// - Can read HEIC files (Android 9+)
/// - Cannot write HEIC (throws UnsupportedError)
/// - Efficient memory usage with ByteArrayOutputStream
///
/// **API Stack:**
/// - `BitmapFactory.decodeByteArray`: Auto-detect and decode input
/// - `Bitmap.createScaledBitmap`: Resize image with filtering
/// - `Bitmap.compress`: Encode to target format with quality control
/// - `ByteArrayOutputStream`: Memory-based output buffer
///
/// **Limitations:**
/// - HEIC output not supported (use JPEG or PNG instead)
/// - Requires Android 9+ for full format support
///
/// **Performance:**
/// - Native image decoding via BitmapFactory
/// - Efficient compression with quality adjustment
final class ImageConverterAndroid implements ImageConverterPlatform {
const ImageConverterAndroid();
@override
Uint8List convert({
required Uint8List inputData,
OutputFormat format = OutputFormat.jpeg,
int quality = 100,
ResizeMode resizeMode = const OriginalResizeMode(),
}) {
return using((arena) {
final inputJBytes = JByteArray.from(inputData)..releasedBy(arena);
final originalBitmap = BitmapFactory.decodeByteArray(
inputJBytes,
0,
inputData.length,
)?..releasedBy(arena);
if (originalBitmap == null) {
throw const ImageDecodingException('Invalid image data.');
}
final originalWidth = originalBitmap.getWidth();
final originalHeight = originalBitmap.getHeight();
final (newWidth, newHeight) = resizeMode.calculateSize(
originalWidth,
originalHeight,
);
final Bitmap? bitmapToCompress;
if (newWidth == originalWidth && newHeight == originalHeight) {
bitmapToCompress = originalBitmap;
} else {
bitmapToCompress = Bitmap.createScaledBitmap(
originalBitmap,
newWidth,
newHeight,
true, // filter
)?..releasedBy(arena);
}
if (bitmapToCompress == null) {
throw const ImageConversionException(
'Bitmap could not be prepared for compression.',
);
}
final compressFormat = switch (format) {
OutputFormat.jpeg => Bitmap$CompressFormat.JPEG,
OutputFormat.png => Bitmap$CompressFormat.PNG,
// TODO: WebP is deprecated since Android 10, consider using WebP_LOSSY or WebP_LOSSLESS
OutputFormat.webp => Bitmap$CompressFormat.WEBP,
OutputFormat.heic => throw UnsupportedError(
'HEIC output format is not supported on Android.',
),
}..releasedBy(arena);
final outputStream = ByteArrayOutputStream()..releasedBy(arena);
final success = bitmapToCompress.compress(
compressFormat,
quality,
outputStream,
);
if (!success) {
throw ImageEncodingException(format, 'Failed to compress bitmap.');
}
final outputJBytes = outputStream.toByteArray()?..releasedBy(arena);
if (outputJBytes == null) {
throw ImageEncodingException(
format,
'Failed to get byte array from output stream.',
);
}
return Uint8List.fromList(outputJBytes.toList());
});
}
}