110 lines
3.8 KiB
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());
|
|
});
|
|
}
|
|
}
|