Skip to content

Commit a1929a6

Browse files
authored
[image_picker_android] Improved Bitmap resize on Android (flutter#3423)
Improves Bitmap load and resize on Android. Original PR on flutter/plugins: flutter/plugins#6947 Issue: flutter#118383
1 parent 90164b7 commit a1929a6

File tree

5 files changed

+113
-13
lines changed

5 files changed

+113
-13
lines changed

packages/image_picker/image_picker_android/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,4 @@ Aleksandr Yurkovskiy <sanekyy@gmail.com>
6464
Anton Borries <mail@antonborri.es>
6565
Alex Li <google@alexv525.com>
6666
Rahul Raj <64.rahulraj@gmail.com>
67+
André Sousa <andrelvsousa@gmail.com>

packages/image_picker/image_picker_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.8.6+12
2+
3+
* Improves image resizing performance by decoding Bitmap only when needed.
4+
15
## 0.8.6+11
26

37
* Updates gradle to 7.6.1.

packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import android.graphics.Bitmap;
88
import android.graphics.BitmapFactory;
99
import android.util.Log;
10+
import androidx.annotation.NonNull;
1011
import androidx.annotation.Nullable;
12+
import androidx.core.util.SizeFCompat;
1113
import java.io.ByteArrayOutputStream;
1214
import java.io.File;
1315
import java.io.FileOutputStream;
@@ -30,8 +32,8 @@ class ImageResizer {
3032
*/
3133
String resizeImageIfNeeded(
3234
String imagePath, @Nullable Double maxWidth, @Nullable Double maxHeight, int imageQuality) {
33-
Bitmap bmp = decodeFile(imagePath);
34-
if (bmp == null) {
35+
SizeFCompat originalSize = readFileDimensions(imagePath);
36+
if (originalSize.getWidth() == -1 || originalSize.getHeight() == -1) {
3537
return imagePath;
3638
}
3739
boolean shouldScale = maxWidth != null || maxHeight != null || imageQuality < 100;
@@ -41,7 +43,26 @@ String resizeImageIfNeeded(
4143
try {
4244
String[] pathParts = imagePath.split("/");
4345
String imageName = pathParts[pathParts.length - 1];
44-
File file = resizedImage(bmp, maxWidth, maxHeight, imageQuality, imageName);
46+
SizeFCompat targetSize =
47+
calculateTargetSize(
48+
(double) originalSize.getWidth(),
49+
(double) originalSize.getHeight(),
50+
maxWidth,
51+
maxHeight);
52+
BitmapFactory.Options options = new BitmapFactory.Options();
53+
options.inSampleSize =
54+
calculateSampleSize(options, (int) targetSize.getWidth(), (int) targetSize.getHeight());
55+
Bitmap bmp = decodeFile(imagePath, options);
56+
if (bmp == null) {
57+
return imagePath;
58+
}
59+
File file =
60+
resizedImage(
61+
bmp,
62+
(double) targetSize.getWidth(),
63+
(double) targetSize.getHeight(),
64+
imageQuality,
65+
imageName);
4566
copyExif(imagePath, file.getPath());
4667
return file.getPath();
4768
} catch (IOException e) {
@@ -50,10 +71,19 @@ String resizeImageIfNeeded(
5071
}
5172

5273
private File resizedImage(
53-
Bitmap bmp, Double maxWidth, Double maxHeight, int imageQuality, String outputImageName)
74+
Bitmap bmp, Double width, Double height, int imageQuality, String outputImageName)
5475
throws IOException {
55-
double originalWidth = bmp.getWidth() * 1.0;
56-
double originalHeight = bmp.getHeight() * 1.0;
76+
Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false);
77+
File file =
78+
createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality);
79+
return file;
80+
}
81+
82+
private SizeFCompat calculateTargetSize(
83+
@NonNull Double originalWidth,
84+
@NonNull Double originalHeight,
85+
@Nullable Double maxWidth,
86+
@Nullable Double maxHeight) {
5787

5888
boolean hasMaxWidth = maxWidth != null;
5989
boolean hasMaxHeight = maxHeight != null;
@@ -90,10 +120,7 @@ private File resizedImage(
90120
}
91121
}
92122

93-
Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false);
94-
File file =
95-
createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality);
96-
return file;
123+
return new SizeFCompat(width.floatValue(), height.floatValue());
97124
}
98125

99126
private File createFile(File externalFilesDirectory, String child) {
@@ -112,14 +139,47 @@ private void copyExif(String filePathOri, String filePathDest) {
112139
exifDataCopier.copyExif(filePathOri, filePathDest);
113140
}
114141

115-
private Bitmap decodeFile(String path) {
116-
return BitmapFactory.decodeFile(path);
142+
private SizeFCompat readFileDimensions(String path) {
143+
BitmapFactory.Options options = new BitmapFactory.Options();
144+
options.inJustDecodeBounds = true;
145+
decodeFile(path, options);
146+
return new SizeFCompat(options.outWidth, options.outHeight);
147+
}
148+
149+
private Bitmap decodeFile(String path, @Nullable BitmapFactory.Options opts) {
150+
return BitmapFactory.decodeFile(path, opts);
117151
}
118152

119153
private Bitmap createScaledBitmap(Bitmap bmp, int width, int height, boolean filter) {
120154
return Bitmap.createScaledBitmap(bmp, width, height, filter);
121155
}
122156

157+
/**
158+
* Calculates the largest sample size value that is a power of two based on a target width and
159+
* height.
160+
*
161+
* <p>This value is necessary to tell the Bitmap decoder to subsample the original image,
162+
* returning a smaller image to save memory.
163+
*
164+
* @see <a
165+
* href="https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap">
166+
* Loading Large Bitmaps Efficiently</a>
167+
*/
168+
private int calculateSampleSize(
169+
BitmapFactory.Options options, int targetWidth, int targetHeight) {
170+
final int height = options.outHeight;
171+
final int width = options.outWidth;
172+
int sampleSize = 1;
173+
if (height > targetHeight || width > targetWidth) {
174+
final int halfHeight = height / 2;
175+
final int halfWidth = width / 2;
176+
while ((halfHeight / sampleSize) >= targetHeight && (halfWidth / sampleSize) >= targetWidth) {
177+
sampleSize *= 2;
178+
}
179+
}
180+
return sampleSize;
181+
}
182+
123183
private File createImageOnExternalDirectory(String name, Bitmap bitmap, int imageQuality)
124184
throws IOException {
125185
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@
66

77
import static org.hamcrest.MatcherAssert.assertThat;
88
import static org.hamcrest.core.IsEqual.equalTo;
9+
import static org.junit.Assert.assertFalse;
910
import static org.junit.Assert.assertNotNull;
11+
import static org.junit.Assert.assertTrue;
12+
import static org.mockito.ArgumentMatchers.anyString;
13+
import static org.mockito.Mockito.mockStatic;
14+
import static org.mockito.Mockito.times;
1015

1116
import android.graphics.Bitmap;
1217
import android.graphics.BitmapFactory;
1318
import java.io.File;
1419
import java.io.IOException;
20+
import java.util.List;
1521
import org.junit.After;
1622
import org.junit.Before;
1723
import org.junit.Test;
1824
import org.junit.rules.TemporaryFolder;
1925
import org.junit.runner.RunWith;
26+
import org.mockito.ArgumentCaptor;
2027
import org.mockito.MockedStatic;
2128
import org.mockito.Mockito;
2229
import org.mockito.MockitoAnnotations;
@@ -102,4 +109,32 @@ public void onResizeImageIfNeeded_whenImagePathIsNotBitmap_shouldReturnPathAndNo
102109
assertThat(resizedImagePath, equalTo(nonBitmapImagePath));
103110
}
104111
}
112+
113+
@Test
114+
public void onResizeImageIfNeeded_whenResizeIsNotNecessary_shouldOnlyQueryBitmapDimensions() {
115+
try (MockedStatic<BitmapFactory> mockBitmapFactory =
116+
mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) {
117+
String outputFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, 100);
118+
ArgumentCaptor<BitmapFactory.Options> argument =
119+
ArgumentCaptor.forClass(BitmapFactory.Options.class);
120+
mockBitmapFactory.verify(() -> BitmapFactory.decodeFile(anyString(), argument.capture()));
121+
BitmapFactory.Options capturedOptions = argument.getValue();
122+
assertTrue(capturedOptions.inJustDecodeBounds);
123+
}
124+
}
125+
126+
@Test
127+
public void onResizeImageIfNeeded_whenResizeIsNecessary_shouldDecodeBitmapPixels() {
128+
try (MockedStatic<BitmapFactory> mockBitmapFactory =
129+
mockStatic(BitmapFactory.class, Mockito.CALLS_REAL_METHODS)) {
130+
String outputFile = resizer.resizeImageIfNeeded(imageFile.getPath(), 50.0, 50.0, 100);
131+
ArgumentCaptor<BitmapFactory.Options> argument =
132+
ArgumentCaptor.forClass(BitmapFactory.Options.class);
133+
mockBitmapFactory.verify(
134+
() -> BitmapFactory.decodeFile(anyString(), argument.capture()), times(2));
135+
List<BitmapFactory.Options> capturedOptions = argument.getAllValues();
136+
assertTrue(capturedOptions.get(0).inJustDecodeBounds);
137+
assertFalse(capturedOptions.get(1).inJustDecodeBounds);
138+
}
139+
}
105140
}

packages/image_picker/image_picker_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Android implementation of the image_picker plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
55

6-
version: 0.8.6+11
6+
version: 0.8.6+12
77

88
environment:
99
sdk: ">=2.18.0 <4.0.0"

0 commit comments

Comments
 (0)