Reputation: 1268
I need to convert my Bitmap (image) to byte code to attach it in a bundle required for API call to upload it to some site, though I used what appears to be the most used of way of converting Bitmaps to ByteArray, it always consumes a lot of memory which make my app throw an OutOfMemoryError, my app from just 16 Mb (16000K) it jumps up to > 64Mb (64000K) which causes the error.
Furthermore, I need my image not to be more than 10 Mb size as the API won't allow me to upload more than that.
How to get ByteArray of my one of my external storage picture without exceeding 10 MB and without causing OutOfMemoryError ?
byte[] data = null;
Uri pictureUri = Uri.parse(uri_string);
InputStream inputStream = null;
try {
inputStream = getApplicationContext().getContentResolver().openInputStream(pictureUri);
Bitmap bi = BitmapFactory.decodeStream(inputStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bi.compress(Bitmap.CompressFormat.PNG, 100, baos);
data = baos.toByteArray();
bi.recycle();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
D/dalvikvm: GC_FOR_ALLOC freed 5435K, 35% free 17128K/26228K, paused 51ms, total 51ms
I/dalvikvm-heap: Grow heap (frag case) to 50.774MB for 31961104-byte allocation
D/dalvikvm: GC_FOR_ALLOC freed 6K, 16% free 48334K/57444K, paused 34ms, total 34ms
D/dalvikvm: GC_FOR_ALLOC freed 1085K, 16% free 49233K/58464K, paused 50ms, total 50ms
D/dalvikvm: GC_FOR_ALLOC freed 1045K, 17% free 50228K/60508K, paused 23ms, total 23ms
I/dalvikvm-heap: Grow heap (frag case) to 56.603MB for 4179344-byte allocation
D/dalvikvm: GC_FOR_ALLOC freed 2040K, 20% free 52269K/64592K, paused 144ms, total 144ms
D/dalvikvm: GC_FOR_ALLOC freed <1K, 20% free 52269K/64592K, paused 47ms, total 47ms
I/dalvikvm-heap: Forcing collection of SoftReferences for 8362484-byte allocation
D/dalvikvm: GC_BEFORE_OOM freed 70K, 20% free 52198K/64592K, paused 55ms, total 55ms
E/dalvikvm-heap: Out of memory on a 8362484-byte allocation.
I/dalvikvm: "pool-2-thread-1" prio=5 tid=36 RUNNABLE
| group="main" sCount=0 dsCount=0 obj=0x430b9d30 self=0x63f73198
| sysTid=18149 nice=0 sched=0/0 cgrp=apps handle=1677137248
| state=R schedstat=( 12681542275 603177890 1137 ) utm=1179 stm=89 core=4
I/dalvikvm: at java.io.ByteArrayOutputStream.expand(ByteArrayOutputStream.java:~91)
at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:201)
at android.graphics.Bitmap.nativeCompress(Native Method)
at android.graphics.Bitmap.compress(Bitmap.java:980)
I/dalvikvm: at com.example.pretest.services.PostDispatchWorker.postRandomData(PostDispatchWorker.java:82)
at com.example.pretest.services.PostDispatchWorker.doWork(PostDispatchWorker.java:64)
I/dalvikvm: at androidx.work.Worker$1.run(Worker.java:86)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
at java.lang.Thread.run(Thread.java:841)
W/System.err: java.lang.OutOfMemoryError
W/System.err: at java.io.ByteArrayOutputStream.expand(ByteArrayOutputStream.java:91)
at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:201)
W/System.err: at android.graphics.Bitmap.nativeCompress(Native Method)
at android.graphics.Bitmap.compress(Bitmap.java:980)
at com.example.pretest.services.PostDispatchWorker.postRandomData(PostDispatchWorker.java:82)
at com.example.pretest.services.PostDispatchWorker.doWork(PostDispatchWorker.java:64)
at androidx.work.Worker$1.run(Worker.java:86)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
at java.lang.Thread.run(Thread.java:841)
D/skia: ------- write threw an exception
D/dalvikvm: GC_FOR_ALLOC freed 13K, 20% free 52195K/64592K, paused 47ms, total 47ms
I/dalvikvm-heap: Forcing collection of SoftReferences for 4177154-byte allocation
D/dalvikvm: GC_BEFORE_OOM freed <1K, 20% free 52194K/64592K, paused 200ms, total 200ms
E/dalvikvm-heap: Out of memory on a 4177154-byte allocation.
I/dalvikvm: "pool-2-thread-1" prio=5 tid=36 RUNNABLE
| group="main" sCount=0 dsCount=0 obj=0x430b9d30 self=0x63f73198
| sysTid=18149 nice=0 sched=0/0 cgrp=apps handle=1677137248
| state=R schedstat=( 12900202522 616312889 1206 ) utm=1187 stm=102 core=5
at java.io.ByteArrayOutputStream.toByteArray(ByteArrayOutputStream.java:~122)
at com.example.pretest.services.PostDispatchWorker.postRandomData(PostDispatchWorker.java:83)
at com.example.pretest.services.PostDispatchWorker.doWork(PostDispatchWorker.java:64)
at androidx.work.Worker$1.run(Worker.java:86)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
at java.lang.Thread.run(Thread.java:841)
E/WM-WorkerWrapper: Work [ id=ddc8ccb7-cb28-4bd9-99d4-911e8533c171, tags={ com.example.pretest.services.PostDispatchWorker } ] failed because it threw an exception/error
java.util.concurrent.ExecutionException: java.lang.OutOfMemoryError
at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:516)
at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:475)
at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:300)
at androidx.work.impl.utils.SerialExecutor$Task.run(SerialExecutor.java:91)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
at java.lang.Thread.run(Thread.java:841)
Caused by: java.lang.OutOfMemoryError
at java.io.ByteArrayOutputStream.toByteArray(ByteArrayOutputStream.java:122)
at com.example.pretest.services.PostDispatchWorker.postRandomData(PostDispatchWorker.java:83)
at com.example.pretest.services.PostDispatchWorker.doWork(PostDispatchWorker.java:64)
at androidx.work.Worker$1.run(Worker.java:86)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
at java.lang.Thread.run(Thread.java:841)
Upvotes: 0
Views: 165
Reputation: 6620
How about something like this?
InputStream inputStream = getApplicationContext().getContentResolver().openInputStream(pictureUri);
byte[] data = is.readAllBytes();
maybe that does the trick?
EDIT: however, this won't solve the problem if the picture (bitmap's bytes actually) is too big. If it exceeds 10MB for example.
Rescaling it to 1280x720 should be sufficient, however you'll better calculate the max width*height yourself, knowing that each pixel takes about 3bytes.
Here's some code you will have to adapt, but it will get you started at least:
public static final int MAX_WIDTH = 1280;
public static final int MAX_HEIGHT = 720;
private FocusParams calculateRectToCropPicture() {
View decorView = getWindow().getDecorView();
int decorWidth = decorView.getWidth();
int decorHeight = decorView.getHeight();
Rect screenRect = new Rect(decorView.getLeft(), decorView.getTop(), decorView.getRight(), decorView.getBottom());
// Get the "camera focus" view and set it as the cropping rect
Rect cropRect = null;
if (getCroppingViewId() != 0) {
cropRect = getCroppingRect(decorView.findViewById(getCroppingViewId()));
}
float widthRatio = (float) cameraFocusView.getWidth() / decorWidth;
float heightRatio = (float) cameraFocusView.getHeight() / decorHeight;
return new FocusParams(widthRatio, heightRatio, cropRect, screenRect);
}
private Rect getCroppingRect(View croppingView) {
if (croppingView != null) {
// In case the "cropping view" is wrapped with a different view or a viewgroup
// (such as FrameLayout) we can't just rely on getLeft / getTop because they return
// relative positions; so we have to take the parent's position into account as well.
// In case we're located at the root layout, these positions will remain 0 regardless.
int parentLeft = 0, parentTop = 0;
if (croppingView.getParent() instanceof View) {
View parent = (View) croppingView.getParent();
parentLeft = parent.getLeft();
parentTop = parent.getTop();
}
int left = (int) (croppingView.getX() + parentLeft);
int top = (int) (croppingView.getY() + parentTop);
int right = (int) (left + croppingView.getWidth());
int bottom = (int) (top + croppingView.getHeight());
return new Rect(
left,
top,
right,
bottom
);
}
return null;
}
static class FocusParams {
final float widthRatio;
final float heightRatio;
final Rect cropRect;
final Rect screenRect;
FocusParams(float widthRatio, float heightRatio, Rect cropRect, Rect screenRect) {
this.widthRatio = widthRatio;
this.heightRatio = heightRatio;
this.cropRect = cropRect;
this.screenRect = screenRect;
}
}
private File cropPicture(File originalImage, FocusParams focusParams)
throws IOException {
Bitmap originalBitmap =
BitmapFactory.decodeStream(new BufferedInputStream(new FileInputStream(originalImage)));
double pictureWidth = originalBitmap.getWidth();
double pictureHeight = originalBitmap.getHeight();
double screenWidth = focusParams.screenRect.width();
double screenHeight = focusParams.screenRect.height();
Bitmap croppedPart;
Rect previewRect = getCameraFragment().getCameraViewRect();
boolean isFullscreenPreview = (
(previewRect.width() == screenWidth)
&& (previewRect.height() == screenHeight)
);
// For fullscreen preview (if the camera supports it) the procedure is straightforward:
// 1. we check whether the screen or the bitmap is bigger
// 2. if the bitmap is bigger, we scale up the cropping rect and cut out that part of the picture
// 3. if the screen is bigger, we probably lost some "picture space" due to implicit camera cropping
// so we just adjust for that and crop out the relevant part of the picture
if (isFullscreenPreview) {
// This scenario tells us that the device has a decent camera that can take pictures
// much bigger than the screen, or will implicitly crop pictures when told to take
// smaller pictures
if (pictureWidth > screenWidth || pictureHeight > screenHeight) {
// If the bitmap width/height are bigger than the screen width/height, we need to scale UP the cropping rect
double cropWidthRatio = pictureWidth / focusParams.screenRect.width();
double cropHeightRatio = pictureHeight / focusParams.screenRect.height();
// Scale crop rect
Rect scaledCropRect = new Rect(
(int) (focusParams.cropRect.left * cropWidthRatio),
(int) (focusParams.cropRect.top * cropHeightRatio),
(int) (focusParams.cropRect.right * cropWidthRatio),
(int) (focusParams.cropRect.bottom * cropHeightRatio)
);
croppedPart = Bitmap.createBitmap(
scaledCropRect.width(),
scaledCropRect.height(),
originalBitmap.getConfig()
);
Rect destRect = new Rect(0, 0, croppedPart.getWidth(), croppedPart.getHeight());
// Draw using canvas
Canvas canvas = new Canvas(croppedPart);
canvas.drawBitmap(originalBitmap, scaledCropRect, destRect, null);
} else {
// In the "downscale" scenario, we can't simply use scaling because the camera will do
// implicit cropping. Scaling in that case will produce senseless positions.
//
// Ex: If we have a 50x50px square in the center of a 100x100px area,
// it will have 25px of room on each side.
//
// When we want to transform that square onto a 200x200 area, it's going to turn
// into a 100x100 square centered in 200x200 area, with 50px of room on each side.
// (which is fine, and follows scaling rules - it's still 2x larger) - which is
// the "upscale scenario"
//
// However, transforming a 50x50 square centered in a 200x200 area (with 75px room on both sides)
// onto a 100x100 area, should still result in a 50x50px square centered, with 25px on both sides
// Meaning the "room on the sides" shrinks down in a non-linear way and the "interesting area"
// doesn't change size at all - which is where the scaling approach breaks down, since the camera
// will first do implicit cropping of it's own
double lostWidth = screenWidth - pictureWidth;
double cropWidthRatio = focusParams.screenRect.width() / pictureWidth;
// Adjust points individually
// If we "lost width" by camera doing implicit cropping, we need to adjust our points accordingly
// otherwise, we can just use our original points scaled to the new area
// We won't care about the "lost height" due to the ID being either in the bottom or the middle
// of the picture, taken it was centered in the "view finder" aka "the cropping view"
int adjustedLeft = (int) (
(lostWidth > 0)
? (focusParams.cropRect.left - lostWidth / 2) * cropWidthRatio
: focusParams.cropRect.left * cropWidthRatio
);
adjustedLeft = clamp(adjustedLeft, 0, (int) pictureWidth);
int adjustedTop = focusParams.cropRect.top;
adjustedTop = clamp(adjustedTop, 0, (int) pictureHeight);
int adjustedRight = (int) (
(lostWidth > 0)
? (focusParams.cropRect.right - lostWidth / 2) * cropWidthRatio
: focusParams.cropRect.right * cropWidthRatio
);
adjustedRight = adjustedRight - focusParams.cropRect.left;
adjustedRight = clamp(adjustedRight, 0, (int) pictureWidth);
int adjustedBottom = focusParams.cropRect.bottom;
adjustedBottom = clamp(adjustedBottom, 0, (int) pictureHeight);
Rect adjustedRect = new Rect(
adjustedLeft,
adjustedTop,
adjustedRight,
adjustedBottom
);
croppedPart = Bitmap.createBitmap(
adjustedRect.width(),
adjustedRect.height(),
originalBitmap.getConfig()
);
Rect destRect = new Rect(0, 0, croppedPart.getWidth(), croppedPart.getHeight());
Canvas canvas = new Canvas(croppedPart);
canvas.drawBitmap(originalBitmap, adjustedRect, destRect, null);
}
} else {
// If we don't have fullscreen preview; chances are the camera doesn't support taking pictures
// bigger than the screen either. So let's scale down the cropping rect, and cut that part out
//
// The cropping rect is in screen-size dimension (e.g. [1080, 1920]) and we need to bring it to
// the preview-size dimension first
// This case covers both the "crappy camera" and "implicit scaling" cases
double cropWidthRatio = pictureWidth / focusParams.screenRect.width();
double cropHeightRatio = pictureHeight / focusParams.screenRect.height();
double cropRatio = Math.max(cropWidthRatio, cropHeightRatio);
// Scale crop rect
Rect scaledCropRect = new Rect(
(int) (focusParams.cropRect.left * cropWidthRatio),
(int) (focusParams.cropRect.top * cropHeightRatio),
(int) (focusParams.cropRect.right * cropWidthRatio),
// While this may seem counter-intuitive; the ratio isn't excellent and may cut out
// the bottom of the ID on some devices; so just use the bigger ratio to ensure
// the whole ID fits in the picture with the possibility of some empty padding
// at the bottom
(int) (focusParams.cropRect.bottom * cropRatio)
);
croppedPart = Bitmap.createBitmap(
scaledCropRect.width(),
scaledCropRect.height(),
originalBitmap.getConfig()
);
Rect destRect = new Rect(0, 0, croppedPart.getWidth(), croppedPart.getHeight());
// Draw using canvas
Canvas canvas = new Canvas(croppedPart);
canvas.drawBitmap(originalBitmap, scaledCropRect, destRect, null);
}
// Since the cropped-out ID is probably different than the GCV recommended sizes, upscale or downscale it
// But don't just blindly rescale the cropped part, preserve the aspect ratio
double croppedWidth = croppedPart.getWidth();
double croppedHeight = croppedPart.getHeight();
int scaledWidth;
int scaledHeight;
// Lets re-scale again only if our size is smaller than recommended, or it's bigger than
// twice the recommended
Bitmap scaledCroppedIdPart;
if ((croppedWidth < MAX_WIDTH) || (croppedHeight < MAX_HEIGHT) || (croppedWidth > 2 * MAX_WIDTH) || (croppedHeight > 2 * MAX_HEIGHT)) {
if (croppedWidth > croppedHeight) {
scaledWidth = MAX_WIDTH;
scaledHeight = (int) (scaledWidth * (croppedHeight / croppedWidth));
} else {
scaledHeight = MAX_HEIGHT;
scaledWidth = (int) (scaledHeight * (croppedWidth / croppedHeight));
}
scaledCroppedIdPart = Bitmap.createScaledBitmap(croppedPart, scaledWidth, scaledHeight, true);
} else {
scaledCroppedIdPart = croppedPart;
}
// Write-out the new image and delete the old one before sending off to GCV
File croppedImageFile = File.createTempFile("croppedImage", "jpeg", getFilesDir());
scaledCroppedIdPart.compress(Bitmap.CompressFormat.JPEG, 90, new BufferedOutputStream(new FileOutputStream(croppedImageFile)));
originalImage.delete();
return croppedImageFile;
}
private int clamp(int valueToClamp, int clampMin, int clampMax) {
if (valueToClamp <= clampMin) return clampMin;
if (valueToClamp >= clampMax) return clampMax;
return valueToClamp;
}
Upvotes: 1