Skip to content

Commit 2685f47

Browse files
authored
Improve CameraUtils.decodeBitmap (#83)
1 parent e40f93a commit 2685f47

File tree

6 files changed

+136
-31
lines changed

6 files changed

+136
-31
lines changed

cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@ public void testHasCameras() {
3636
assertFalse(CameraUtils.hasCameras(context));
3737
}
3838

39-
@Test
40-
public void testDecodeBitmap() {
41-
int w = 100, h = 200, color = Color.WHITE;
42-
Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
43-
source.setPixel(0, 0, color);
44-
final ByteArrayOutputStream os = new ByteArrayOutputStream();
39+
// Encodes bitmap and decodes again using our utility.
40+
private Task<Bitmap> encodeDecodeTask(Bitmap source) {
41+
return encodeDecodeTask(source, 0, 0);
42+
}
4543

44+
// Encodes bitmap and decodes again using our utility.
45+
private Task<Bitmap> encodeDecodeTask(Bitmap source, final int maxWidth, final int maxHeight) {
46+
final ByteArrayOutputStream os = new ByteArrayOutputStream();
4647
// Using lossy JPG we can't have strict comparison of values after compression.
4748
source.compress(Bitmap.CompressFormat.PNG, 100, os);
49+
final byte[] data = os.toByteArray();
4850

4951
final Task<Bitmap> decode = new Task<>();
5052
decode.listen();
@@ -59,9 +61,23 @@ public void onBitmapReady(Bitmap bitmap) {
5961
ui(new Runnable() {
6062
@Override
6163
public void run() {
62-
CameraUtils.decodeBitmap(os.toByteArray(), callback);
64+
if (maxWidth > 0 && maxHeight > 0) {
65+
CameraUtils.decodeBitmap(data, maxWidth, maxHeight, callback);
66+
} else {
67+
CameraUtils.decodeBitmap(data, callback);
68+
}
6369
}
6470
});
71+
return decode;
72+
}
73+
74+
@Test
75+
public void testDecodeBitmap() {
76+
int w = 100, h = 200, color = Color.WHITE;
77+
Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
78+
source.setPixel(0, 0, color);
79+
80+
Task<Bitmap> decode = encodeDecodeTask(source);
6581
Bitmap other = decode.await(800);
6682
assertNotNull(other);
6783
assertEquals(100, w);
@@ -73,4 +89,31 @@ public void run() {
7389

7490
// TODO: improve when we add EXIF writing to byte arrays
7591
}
92+
93+
94+
@Test
95+
public void testDecodeDownscaledBitmap() {
96+
int width = 1000, height = 2000;
97+
Bitmap source = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
98+
Task<Bitmap> task;
99+
Bitmap other;
100+
101+
task = encodeDecodeTask(source, 100, 100);
102+
other = task.await(800);
103+
assertNotNull(other);
104+
assertTrue(other.getWidth() <= 100);
105+
assertTrue(other.getHeight() <= 100);
106+
107+
task = encodeDecodeTask(source, Integer.MAX_VALUE, Integer.MAX_VALUE);
108+
other = task.await(800);
109+
assertNotNull(other);
110+
assertTrue(other.getWidth() == width);
111+
assertTrue(other.getHeight() == height);
112+
113+
task = encodeDecodeTask(source, 6000, 6000);
114+
other = task.await(800);
115+
assertNotNull(other);
116+
assertTrue(other.getWidth() == width);
117+
assertTrue(other.getHeight() == height);
118+
}
76119
}

cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ public void testCapturePicture_size() throws Exception {
459459
Size size = camera.getCaptureSize();
460460
camera.capturePicture();
461461
byte[] jpeg = waitForPicture(true);
462-
Bitmap b = CameraUtils.decodeBitmap(jpeg);
462+
Bitmap b = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE);
463463
// Result can actually have swapped dimensions
464464
// Which one, depends on factors including device physical orientation
465465
assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth());
@@ -497,7 +497,7 @@ public void testCaptureSnapshot_size() throws Exception {
497497
Size size = camera.getPreviewSize();
498498
camera.captureSnapshot();
499499
byte[] jpeg = waitForPicture(true);
500-
Bitmap b = CameraUtils.decodeBitmap(jpeg);
500+
Bitmap b = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE);
501501
// Result can actually have swapped dimensions
502502
// Which one, depends on factors including device physical orientation
503503
assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth());

cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import java.io.InputStream;
1616

1717
/**
18-
* Static utilities for dealing with camera I/O, orientation, etc.
18+
* Static utilities for dealing with camera I/O, orientations, etc.
1919
*/
2020
public class CameraUtils {
2121

@@ -27,6 +27,7 @@ public class CameraUtils {
2727
* @param context a valid Context
2828
* @return whether device has cameras
2929
*/
30+
@SuppressWarnings("WeakerAccess")
3031
public static boolean hasCameras(Context context) {
3132
PackageManager manager = context.getPackageManager();
3233
// There's also FEATURE_CAMERA_EXTERNAL , should we support it?
@@ -60,18 +61,34 @@ public static boolean hasCameraFacing(Context context, Facing facing) {
6061
* is that this cares about orientation, reading it from the EXIF header.
6162
* This is executed in a background thread, and returns the result to the original thread.
6263
*
63-
* This ignores flipping at the moment.
64-
* TODO care about flipping using Matrix.scale()
65-
*
6664
* @param source a JPEG byte array
6765
* @param callback a callback to be notified
6866
*/
67+
@SuppressWarnings("WeakerAccess")
6968
public static void decodeBitmap(final byte[] source, final BitmapCallback callback) {
69+
decodeBitmap(source, Integer.MAX_VALUE, Integer.MAX_VALUE, callback);
70+
}
71+
72+
/**
73+
* Decodes an input byte array and outputs a Bitmap that is ready to be displayed.
74+
* The difference with {@link android.graphics.BitmapFactory#decodeByteArray(byte[], int, int)}
75+
* is that this cares about orientation, reading it from the EXIF header.
76+
* This is executed in a background thread, and returns the result to the original thread.
77+
*
78+
* The image is also downscaled taking care of the maxWidth and maxHeight arguments.
79+
*
80+
* @param source a JPEG byte array
81+
* @param maxWidth the max allowed width
82+
* @param maxHeight the max allowed height
83+
* @param callback a callback to be notified
84+
*/
85+
@SuppressWarnings("WeakerAccess")
86+
public static void decodeBitmap(final byte[] source, final int maxWidth, final int maxHeight, final BitmapCallback callback) {
7087
final Handler ui = new Handler();
7188
WorkerHandler.run(new Runnable() {
7289
@Override
7390
public void run() {
74-
final Bitmap bitmap = decodeBitmap(source);
91+
final Bitmap bitmap = decodeBitmap(source, maxWidth, maxHeight);
7592
ui.post(new Runnable() {
7693
@Override
7794
public void run() {
@@ -83,7 +100,11 @@ public void run() {
83100
}
84101

85102

86-
static Bitmap decodeBitmap(byte[] source) {
103+
// TODO ignores flipping
104+
@SuppressWarnings({"SuspiciousNameCombination", "WeakerAccess"})
105+
/* for tests */ static Bitmap decodeBitmap(byte[] source, int maxWidth, int maxHeight) {
106+
if (maxWidth <= 0) maxWidth = Integer.MAX_VALUE;
107+
if (maxHeight <= 0) maxHeight = Integer.MAX_VALUE;
87108
int orientation;
88109
boolean flip;
89110
InputStream stream = null;
@@ -123,12 +144,30 @@ static Bitmap decodeBitmap(byte[] source) {
123144
flip = false;
124145
} finally {
125146
if (stream != null) {
126-
try { stream.close(); } catch (Exception e) {}
147+
try { stream.close(); } catch (Exception ignored) {}
127148
}
128149
}
129150

151+
Bitmap bitmap;
152+
if (maxWidth < Integer.MAX_VALUE || maxHeight < Integer.MAX_VALUE) {
153+
BitmapFactory.Options options = new BitmapFactory.Options();
154+
options.inJustDecodeBounds = true;
155+
BitmapFactory.decodeByteArray(source, 0, source.length, options);
156+
157+
int outHeight = options.outHeight;
158+
int outWidth = options.outWidth;
159+
if (orientation % 180 != 0) {
160+
outHeight = options.outWidth;
161+
outWidth = options.outHeight;
162+
}
163+
164+
options.inSampleSize = computeSampleSize(outWidth, outHeight, maxWidth, maxHeight);
165+
options.inJustDecodeBounds = false;
166+
bitmap = BitmapFactory.decodeByteArray(source, 0, source.length, options);
167+
} else {
168+
bitmap = BitmapFactory.decodeByteArray(source, 0, source.length);
169+
}
130170

131-
Bitmap bitmap = BitmapFactory.decodeByteArray(source, 0, source.length);
132171
if (orientation != 0 || flip) {
133172
Matrix matrix = new Matrix();
134173
matrix.setRotate(orientation);
@@ -141,7 +180,30 @@ static Bitmap decodeBitmap(byte[] source) {
141180
}
142181

143182

183+
private static int computeSampleSize(int width, int height, int maxWidth, int maxHeight) {
184+
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
185+
int inSampleSize = 1;
186+
if (height > maxHeight || width > maxWidth) {
187+
while ((height / inSampleSize) >= maxHeight
188+
|| (width / inSampleSize) >= maxWidth) {
189+
inSampleSize *= 2;
190+
}
191+
}
192+
return inSampleSize;
193+
}
194+
195+
196+
/**
197+
* Receives callbacks about a bitmap decoding operation.
198+
*/
144199
public interface BitmapCallback {
200+
201+
/**
202+
* Notifies that the bitmap was succesfully decoded.
203+
* This is run on the UI thread.
204+
*
205+
* @param bitmap decoded bitmap
206+
*/
145207
@UiThread void onBitmapReady(Bitmap bitmap);
146208
}
147209
}

cameraview/src/main/utils/com/otaliastudios/cameraview/CropHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ static byte[] cropToJpeg(YuvImage yuv, AspectRatio targetRatio, int jpegCompress
2121
// In doing so, EXIF data is deleted.
2222
static byte[] cropToJpeg(byte[] jpeg, AspectRatio targetRatio, int jpegCompression) {
2323

24-
Bitmap image = CameraUtils.decodeBitmap(jpeg);
24+
Bitmap image = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE);
2525
Rect cropRect = computeCrop(image.getWidth(), image.getHeight(), targetRatio);
2626
Bitmap crop = Bitmap.createBitmap(image, cropRect.left, cropRect.top, cropRect.width(), cropRect.height());
2727
image.recycle();

demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
2727
setContentView(R.layout.activity_picture_preview);
2828
final ImageView imageView = findViewById(R.id.image);
2929
final MessageView nativeCaptureResolution = findViewById(R.id.nativeCaptureResolution);
30-
final MessageView actualResolution = findViewById(R.id.actualResolution);
31-
final MessageView approxUncompressedSize = findViewById(R.id.approxUncompressedSize);
30+
// final MessageView actualResolution = findViewById(R.id.actualResolution);
31+
// final MessageView approxUncompressedSize = findViewById(R.id.approxUncompressedSize);
3232
final MessageView captureLatency = findViewById(R.id.captureLatency);
3333

3434
final long delay = getIntent().getLongExtra("delay", 0);
@@ -40,25 +40,25 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
4040
return;
4141
}
4242

43-
CameraUtils.decodeBitmap(b, new CameraUtils.BitmapCallback() {
43+
CameraUtils.decodeBitmap(b, 1000, 1000, new CameraUtils.BitmapCallback() {
4444
@Override
4545
public void onBitmapReady(Bitmap bitmap) {
4646
imageView.setImageBitmap(bitmap);
4747

48-
approxUncompressedSize.setTitle("Approx. uncompressed size");
49-
approxUncompressedSize.setMessage(getApproximateFileMegabytes(bitmap) + "MB");
48+
// approxUncompressedSize.setTitle("Approx. uncompressed size");
49+
// approxUncompressedSize.setMessage(getApproximateFileMegabytes(bitmap) + "MB");
5050

51-
captureLatency.setTitle("Capture latency");
51+
captureLatency.setTitle("Approx. capture latency");
5252
captureLatency.setMessage(delay + " milliseconds");
5353

5454
// ncr and ar might be different when cropOutput is true.
5555
AspectRatio nativeRatio = AspectRatio.of(nativeWidth, nativeHeight);
56-
AspectRatio finalRatio = AspectRatio.of(bitmap.getWidth(), bitmap.getHeight());
5756
nativeCaptureResolution.setTitle("Native capture resolution");
5857
nativeCaptureResolution.setMessage(nativeWidth + "x" + nativeHeight + " (" + nativeRatio + ")");
5958

60-
actualResolution.setTitle("Actual resolution");
61-
actualResolution.setMessage(bitmap.getWidth() + "x" + bitmap.getHeight() + " (" + finalRatio + ")");
59+
// AspectRatio finalRatio = AspectRatio.of(bitmap.getWidth(), bitmap.getHeight());
60+
// actualResolution.setTitle("Actual resolution");
61+
// actualResolution.setMessage(bitmap.getWidth() + "x" + bitmap.getHeight() + " (" + finalRatio + ")");
6262
}
6363
});
6464

demo/src/main/res/layout/activity_picture_preview.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
android:layout_width="match_parent"
2121
android:layout_height="wrap_content"/>
2222

23-
<com.otaliastudios.cameraview.demo.MessageView
23+
<!-- com.otaliastudios.cameraview.demo.MessageView
2424
android:id="@+id/actualResolution"
2525
android:layout_width="match_parent"
26-
android:layout_height="wrap_content"/>
26+
android:layout_height="wrap_content"/-->
2727

28-
<com.otaliastudios.cameraview.demo.MessageView
28+
<!-- com.otaliastudios.cameraview.demo.MessageView
2929
android:id="@+id/approxUncompressedSize"
3030
android:layout_width="match_parent"
31-
android:layout_height="wrap_content"/>
31+
android:layout_height="wrap_content"/-->
3232

3333
<com.otaliastudios.cameraview.demo.MessageView
3434
android:id="@+id/captureLatency"

0 commit comments

Comments
 (0)