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()); }); } }