diff --git a/packages/image_picker/image_picker_android/AUTHORS b/packages/image_picker/image_picker_android/AUTHORS index 493a0b4ef9c2..57d4f75a1d35 100644 --- a/packages/image_picker/image_picker_android/AUTHORS +++ b/packages/image_picker/image_picker_android/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +André Sousa diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 6f586c7eec0f..08b8150371cc 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.6+12 + +* Improves image resizing performance by decoding Bitmap only when needed. + ## 0.8.6+11 * Updates gradle to 7.6.1. diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java index 5d0d5d8938d8..9ec4c0aa3baa 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java @@ -7,7 +7,9 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.SizeFCompat; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -30,8 +32,8 @@ class ImageResizer { */ String resizeImageIfNeeded( String imagePath, @Nullable Double maxWidth, @Nullable Double maxHeight, int imageQuality) { - Bitmap bmp = decodeFile(imagePath); - if (bmp == null) { + SizeFCompat originalSize = readFileDimensions(imagePath); + if (originalSize.getWidth() == -1 || originalSize.getHeight() == -1) { return imagePath; } boolean shouldScale = maxWidth != null || maxHeight != null || imageQuality < 100; @@ -41,7 +43,26 @@ String resizeImageIfNeeded( try { String[] pathParts = imagePath.split("/"); String imageName = pathParts[pathParts.length - 1]; - File file = resizedImage(bmp, maxWidth, maxHeight, imageQuality, imageName); + SizeFCompat targetSize = + calculateTargetSize( + (double) originalSize.getWidth(), + (double) originalSize.getHeight(), + maxWidth, + maxHeight); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = + calculateSampleSize(options, (int) targetSize.getWidth(), (int) targetSize.getHeight()); + Bitmap bmp = decodeFile(imagePath, options); + if (bmp == null) { + return imagePath; + } + File file = + resizedImage( + bmp, + (double) targetSize.getWidth(), + (double) targetSize.getHeight(), + imageQuality, + imageName); copyExif(imagePath, file.getPath()); return file.getPath(); } catch (IOException e) { @@ -50,10 +71,19 @@ String resizeImageIfNeeded( } private File resizedImage( - Bitmap bmp, Double maxWidth, Double maxHeight, int imageQuality, String outputImageName) + Bitmap bmp, Double width, Double height, int imageQuality, String outputImageName) throws IOException { - double originalWidth = bmp.getWidth() * 1.0; - double originalHeight = bmp.getHeight() * 1.0; + Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false); + File file = + createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality); + return file; + } + + private SizeFCompat calculateTargetSize( + @NonNull Double originalWidth, + @NonNull Double originalHeight, + @Nullable Double maxWidth, + @Nullable Double maxHeight) { boolean hasMaxWidth = maxWidth != null; boolean hasMaxHeight = maxHeight != null; @@ -90,10 +120,7 @@ private File resizedImage( } } - Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false); - File file = - createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality); - return file; + return new SizeFCompat(width.floatValue(), height.floatValue()); } private File createFile(File externalFilesDirectory, String child) { @@ -112,14 +139,47 @@ private void copyExif(String filePathOri, String filePathDest) { exifDataCopier.copyExif(filePathOri, filePathDest); } - private Bitmap decodeFile(String path) { - return BitmapFactory.decodeFile(path); + private SizeFCompat readFileDimensions(String path) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + decodeFile(path, options); + return new SizeFCompat(options.outWidth, options.outHeight); + } + + private Bitmap decodeFile(String path, @Nullable BitmapFactory.Options opts) { + return BitmapFactory.decodeFile(path, opts); } private Bitmap createScaledBitmap(Bitmap bmp, int width, int height, boolean filter) { return Bitmap.createScaledBitmap(bmp, width, height, filter); } + /** + * Calculates the largest sample size value that is a power of two based on a target width and + * height. + * + *

This value is necessary to tell the Bitmap decoder to subsample the original image, + * returning a smaller image to save memory. + * + * @see + * Loading Large Bitmaps Efficiently + */ + private int calculateSampleSize( + BitmapFactory.Options options, int targetWidth, int targetHeight) { + final int height = options.outHeight; + final int width = options.outWidth; + int sampleSize = 1; + if (height > targetHeight || width > targetWidth) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + while ((halfHeight / sampleSize) >= targetHeight && (halfWidth / sampleSize) >= targetWidth) { + sampleSize *= 2; + } + } + return sampleSize; + } + private File createImageOnExternalDirectory(String name, Bitmap bitmap, int imageQuality) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java index 45b2a45e8eeb..a2f742f23349 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java @@ -6,17 +6,24 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import java.io.File; import java.io.IOException; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -102,4 +109,32 @@ public void onResizeImageIfNeeded_whenImagePathIsNotBitmap_shouldReturnPathAndNo assertThat(resizedImagePath, equalTo(nonBitmapImagePath)); } } + + @Test + public void onResizeImageIfNeeded_whenResizeIsNotNecessary_shouldOnlyQueryBitmapDimensions() { + try (MockedStatic mockBitmapFactory = + mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) { + String outputFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, 100); + ArgumentCaptor argument = + ArgumentCaptor.forClass(BitmapFactory.Options.class); + mockBitmapFactory.verify(() -> BitmapFactory.decodeFile(anyString(), argument.capture())); + BitmapFactory.Options capturedOptions = argument.getValue(); + assertTrue(capturedOptions.inJustDecodeBounds); + } + } + + @Test + public void onResizeImageIfNeeded_whenResizeIsNecessary_shouldDecodeBitmapPixels() { + try (MockedStatic mockBitmapFactory = + mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) { + String outputFile = resizer.resizeImageIfNeeded(imageFile.getPath(), 50.0, 50.0, 100); + ArgumentCaptor argument = + ArgumentCaptor.forClass(BitmapFactory.Options.class); + mockBitmapFactory.verify( + () -> BitmapFactory.decodeFile(anyString(), argument.capture()), times(2)); + List capturedOptions = argument.getAllValues(); + assertTrue(capturedOptions.get(0).inJustDecodeBounds); + assertFalse(capturedOptions.get(1).inJustDecodeBounds); + } + } } diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index 2e8b6045a7d2..c3e47b9bd718 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -3,7 +3,7 @@ description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.6+11 +version: 0.8.6+12 environment: sdk: ">=2.18.0 <4.0.0"