Skip to content

Commit

Permalink
[image_picker_android] Improved Bitmap resize on Android (flutter#3423)
Browse files Browse the repository at this point in the history
Improves Bitmap load and resize on Android.

Original PR on flutter/plugins: flutter/plugins#6947

Issue: flutter#118383
  • Loading branch information
beroso authored May 10, 2023
1 parent 90164b7 commit a1929a6
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 13 deletions.
1 change: 1 addition & 0 deletions packages/image_picker/image_picker_android/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy <sanekyy@gmail.com>
Anton Borries <mail@antonborri.es>
Alex Li <google@alexv525.com>
Rahul Raj <64.rahulraj@gmail.com>
André Sousa <andrelvsousa@gmail.com>
4 changes: 4 additions & 0 deletions packages/image_picker/image_picker_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
*
* <p>This value is necessary to tell the Bitmap decoder to subsample the original image,
* returning a smaller image to save memory.
*
* @see <a
* href="https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap">
* Loading Large Bitmaps Efficiently</a>
*/
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -102,4 +109,32 @@ public void onResizeImageIfNeeded_whenImagePathIsNotBitmap_shouldReturnPathAndNo
assertThat(resizedImagePath, equalTo(nonBitmapImagePath));
}
}

@Test
public void onResizeImageIfNeeded_whenResizeIsNotNecessary_shouldOnlyQueryBitmapDimensions() {
try (MockedStatic<BitmapFactory> mockBitmapFactory =
mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) {
String outputFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, 100);
ArgumentCaptor<BitmapFactory.Options> 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<BitmapFactory> mockBitmapFactory =
mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) {
String outputFile = resizer.resizeImageIfNeeded(imageFile.getPath(), 50.0, 50.0, 100);
ArgumentCaptor<BitmapFactory.Options> argument =
ArgumentCaptor.forClass(BitmapFactory.Options.class);
mockBitmapFactory.verify(
() -> BitmapFactory.decodeFile(anyString(), argument.capture()), times(2));
List<BitmapFactory.Options> capturedOptions = argument.getAllValues();
assertTrue(capturedOptions.get(0).inJustDecodeBounds);
assertFalse(capturedOptions.get(1).inJustDecodeBounds);
}
}
}
2 changes: 1 addition & 1 deletion packages/image_picker/image_picker_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit a1929a6

Please sign in to comment.