diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 1ab21108d70f..fb4f49e4a969 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.5+7 + +* Improved Bitmap resizing. + ## 0.8.5+6 * Updates minimum Flutter version to 3.0. 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 2a93785678af..9c230ed581d9 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 @@ -6,6 +6,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Point; import android.util.Log; import androidx.annotation.Nullable; import java.io.ByteArrayOutputStream; @@ -33,8 +34,10 @@ String resizeImageIfNeeded( @Nullable Double maxWidth, @Nullable Double maxHeight, @Nullable Integer imageQuality) { - Bitmap bmp = decodeFile(imagePath); - if (bmp == null) { + BitmapFactory.Options queryOptions = new BitmapFactory.Options(); + queryOptions.inJustDecodeBounds = true; + decodeFile(imagePath, queryOptions); + if (queryOptions.outWidth == -1 || queryOptions.outHeight == -1) { return null; } boolean shouldScale = @@ -45,7 +48,18 @@ String resizeImageIfNeeded( try { String[] pathParts = imagePath.split("/"); String imageName = pathParts[pathParts.length - 1]; - File file = resizedImage(bmp, maxWidth, maxHeight, imageQuality, imageName); + Point size = + calculateSize( + Double.valueOf(queryOptions.outWidth), + Double.valueOf(queryOptions.outHeight), + maxWidth, + maxHeight); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calculateInSampleSize(options, size.x, size.y); + options.inJustDecodeBounds = false; + File file = + resizedImage( + decodeFile(imagePath, options), maxWidth, maxHeight, imageQuality, imageName); copyExif(imagePath, file.getPath()); return file.getPath(); } catch (IOException e) { @@ -63,6 +77,15 @@ private File resizedImage( imageQuality = 100; } + Point size = calculateSize(originalWidth, originalHeight, maxWidth, maxHeight); + Bitmap scaledBmp = createScaledBitmap(bmp, size.x, size.y, false); + File file = + createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality); + return file; + } + + private Point calculateSize( + Double originalWidth, Double originalHeight, Double maxWidth, Double maxHeight) { boolean hasMaxWidth = maxWidth != null; boolean hasMaxHeight = maxHeight != null; @@ -98,10 +121,7 @@ private File resizedImage( } } - Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false); - File file = - createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality); - return file; + return new Point(width.intValue(), height.intValue()); } private File createFile(File externalFilesDirectory, String child) { @@ -120,8 +140,8 @@ private void copyExif(String filePathOri, String filePathDest) { exifDataCopier.copyExif(filePathOri, filePathDest); } - private Bitmap decodeFile(String path) { - return BitmapFactory.decodeFile(path); + 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) { @@ -132,6 +152,20 @@ private boolean isImageQualityValid(Integer imageQuality) { return imageQuality != null && imageQuality > 0 && imageQuality < 100; } + private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + if (height > reqHeight || width > reqWidth) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + 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 73cfef9e88ea..312cffd74d92 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 @@ -5,16 +5,25 @@ package io.flutter.plugins.imagepicker; import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; +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.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; import org.robolectric.RobolectricTestRunner; @@ -70,4 +79,32 @@ public void onResizeImageIfNeeded_WhenParentDirectoryDoesNotExists_ShouldNotCras String outoutFile = invalidResizer.resizeImageIfNeeded(imageFile.getPath(), null, 50.0, null); assertThat(outoutFile, equalTo(nonExistentDirectory.getPath() + "/scaled_pngImage.png")); } + + @Test + public void onResizeImageIfNeeded_WhenResizeIsNotNecessary_ShouldOnlyQueryBitmap() { + try (MockedStatic mockBitmapFactory = + mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, null); + 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_ShouldAllocateBitmap() { + try (MockedStatic mockBitmapFactory = + mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), 50.0, 50.0, null); + 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 a0516685964c..b65a8ed0ee5e 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_android description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/plugins/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.5+6 +version: 0.8.5+7 environment: sdk: ">=2.14.0 <3.0.0"