I have an URI image file, and I want to reduce its size to upload it. Initial image file size depends from mobile to mobile (can be 2MB as can be 500KB), but I want final size to be about 200KB, so that I can upload it.
From what I read, I have (at least) 2 choices:
Using BitmapFactory.Options.inSampleSize, to subsample original image and get a smaller image;
Using Bitmap.compress to compress the image specifying compression quality.
What's the best choice?
I was thinking to initially resize image width/height until width or height is above 1000px (something like 1024x768 or others), then compress image with decreasing quality until file size is above 200KB. Here's an example:
int MAX_IMAGE_SIZE = 200 * 1024; // max final file size
Bitmap bmpPic = BitmapFactory.decodeFile(fileUri.getPath());
if ((bmpPic.getWidth() >= 1024) && (bmpPic.getHeight() >= 1024)) {
BitmapFactory.Options bmpOptions = new BitmapFactory.Options();
bmpOptions.inSampleSize = 1;
while ((bmpPic.getWidth() >= 1024) && (bmpPic.getHeight() >= 1024)) {
bmpOptions.inSampleSize++;
bmpPic = BitmapFactory.decodeFile(fileUri.getPath(), bmpOptions);
}
Log.d(TAG, "Resize: " + bmpOptions.inSampleSize);
}
int compressQuality = 104; // quality decreasing by 5 every loop. (start from 99)
int streamLength = MAX_IMAGE_SIZE;
while (streamLength >= MAX_IMAGE_SIZE) {
ByteArrayOutputStream bmpStream = new ByteArrayOutputStream();
compressQuality -= 5;
Log.d(TAG, "Quality: " + compressQuality);
bmpPic.compress(Bitmap.CompressFormat.JPEG, compressQuality, bmpStream);
byte[] bmpPicByteArray = bmpStream.toByteArray();
streamLength = bmpPicByteArray.length;
Log.d(TAG, "Size: " + streamLength);
}
try {
FileOutputStream bmpFile = new FileOutputStream(finalPath);
bmpPic.compress(Bitmap.CompressFormat.JPEG, compressQuality, bmpFile);
bmpFile.flush();
bmpFile.close();
} catch (Exception e) {
Log.e(TAG, "Error on saving file");
}
Is there a better way to do it? Should I try to keep using all 2 methods or only one? Thanks
Using Bitmap.compress() you just specify compression algorithm and by the way compression operation takes rather big amount of time. If you need to play with sizes for reducing memory allocation for your image, you exactly need to use scaling of your image using Bitmap.Options, computing bitmap bounds at first and then decoding it to your specified size.
The best sample that I found on StackOverflow is this one.
Most of the answers i found were just pieces that i had to put together to get a working code, which is posted below
public void compressBitmap(File file, int sampleSize, int quality) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
FileInputStream inputStream = new FileInputStream(file);
Bitmap selectedBitmap = BitmapFactory.decodeStream(inputStream, null, options);
inputStream.close();
FileOutputStream outputStream = new FileOutputStream("location to save");
selectedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);
outputStream.close();
long lengthInKb = photo.length() / 1024; //in kb
if (lengthInKb > SIZE_LIMIT) {
compressBitmap(file, (sampleSize*2), (quality/4));
}
selectedBitmap.recycle();
} catch (Exception e) {
e.printStackTrace();
}
}
2 parameters sampleSize and quality plays an important role
sampleSize is used to subsample the original image and return a smaller image, ie
SampleSize == 4 returns an image that is 1/4 the width/height of the original.
quality is used to hint the compressor, input range is between 0-100. 0 meaning compress for
small size, 100 meaning compress for max quality
BitmapFactory.Options - Reduces Image Size (In Memory)
Bitmap.compress() - Reduces Image Size (In Disk)
Refer to this link for more information about using both of them:
https://android.jlelse.eu/loading-large-bitmaps-efficiently-in-android-66826cd4ad53
Related
I want to reuse size of bitmap when I send to server as base64. For example, original image size is 1.2 MB so I have to resize it to 50KB (server limit side). The way make image distort sometimes. I have read [1] and [2], but it didn't help.
The problem is some image become distort after resize.
Here is my code:
private String RescaleImage(String bitmap, int size) {
try {
if ((float) bitmap.getBytes().length / 1000 <= Constants.PROFILE_IMAGE_LIMITED_SIZE) {
return bitmap;
} else {
//Rescale
Log.d("msg", "rescale size : " + size);
size -= 1;
bitmap = BitmapBase64Util.encodeToBase64(Bitmap.createScaledBitmap(decodeBase64(bitmap), size, size, false));
return RescaleImage(bitmap, size);
}
} catch (Exception e) {
return bitmap;
}
}
encodingToBase64:
public static String encodeToBase64(Bitmap image) {
Log.d(TAG, "encoding image");
String result = "";
if (image != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] b = baos.toByteArray();
result = Base64.encodeToString(b, Base64.DEFAULT);
Log.d(TAG, result);
return result;
}
return result;
}
Image is cropped before resize. Size after cropped is 300 x 300
My question is:
How to reuse image size to 50KB, keep same ratio and avoid distort?
You pass the the same width and height in Bitmap.createScaledBitmap(decodeBase64(bitmap), size, size, false). Unless your bitmaps are squares you have to specify the right widht and height, otherwise your image gets distorted based on the original aspect ratio. I think something like this would work:
Bitmap scaledBitmap = Bitmap.createScaledBitmap(decodeBase64(bitmap);
bitmap = BitmapBase64Util.encodeToBase64(scaledBitmap, scaledBitmap.getWidth(), size.getHeight(), false);
If you need compression to reduce the size use this [Edit: You have done this]:
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
byte[] byteArray = stream.toByteArray();
I update my code and it work better now.
-- FixED --
Instead of resize the bitmap string continuously, I use the orignal bitmap that is used before resize.
private String RescaleImage(String bitmap, Bitmap origin_bitmap, int size) {
try {
if ((float) bitmap.getBytes().length / 1000 <= Constants.PROFILE_IMAGE_LIMITED_SIZE) {
return bitmap;
} else {
//Rescale
Log.d("msg", "rescale size : " + size);
size -= 1;
bitmap = BitmapBase64Util.encodeToBase64(Bitmap.createScaledBitmap(origin_bitmap, size, size, false));
return RescaleImage(bitmap, origin_bitmap, size);
}
} catch (Exception e) {
return bitmap;
}
}
Also, use this code when decode to reuse distortion. Bad image quality after resizing/scaling bitmap.
If there are better fix, I always welcome in order to improve.
I'm trying to compress a Bitmap which is taken from either the user's gallery or camera and store it as a profile picture in a Parse Server.
The issue is the bitmap will NOT compress. The image saves perfectly fine and is useable in the database, but the file size is massive for just a profile picture.
Here's my current code:
//Compressing
ByteArrayOutputStream stream = new ByteArrayOutputStream();
profilePictureBitmap.compress(Bitmap.CompressFormat.PNG, 20, stream);
byte[] image = stream.toByteArray();
//Saving
String imageName = username + "_profile_picture.png";
final ParseFile file = new ParseFile(imageName, image);
file.saveInBackground(new SaveCallback() {
#Override
public void done(ParseException e) {
if(e == null) {
user.put("profilePicture", file);
user.signUpInBackground();
}
}
}
I'm using a image picker library that gets the path of the image. I then turn it into a bitmap.
Heres my code to retrieve the image:
ArrayList<Image> images = data.getParcelableArrayListExtra(ImagePickerActivity.INTENT_EXTRA_SELECTED_IMAGES);
if(images.size() > 0) {
Image image = images.get(0);
File imgFile = new File(image.getPath());
if(imgFile.exists()){
profilePictureBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath());
profilePictureImage.setImageBitmap(profilePictureBitmap);
}
}
If there is any ideas on how to fix this I would greatly appreciate it.
Thanks :]
Image image = images.get(0);
File imgFile = new File(image.getPath());
if(imgFile.exists()){
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 2; // one quarter of original size
profilePictureBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath(), opts);
profilePictureImage.setImageBitmap(profilePictureBitmap);
}
Docs for inSampleSize:
If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory. The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16 the number of pixels. Any value <= 1 is treated the same as 1. Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.
I'm trying to compress images selected by user from gallery for uploading. I saw that my camera pictures are over 5MB and I would like to compress them(same as facebook if possible). What I've been trying:
I let the user select the photo from gallery,get the uri and use this:
File file = new File(getRealPathFromURI(getActivity(), selectedImageUri));
long length = file.length();
Log.e("Filesize:", "Before: " + length);
if (file.getName().toLowerCase().endsWith("jpg")||file.getName().toLowerCase().endsWith("jpeg")){
Bitmap original;
try {
original = MediaStore.Images.Media.getBitmap(getActivity().getContentResolver(), selectedImageUri);
length = sizeOf(original);
Log.e("Filesize:", "BeforeCompression: " + length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
original.compress(Bitmap.CompressFormat.JPEG, 50, out);
Bitmap decoded = BitmapFactory.decodeStream(new ByteArrayInputStream(out.toByteArray()));
length = sizeOf(decoded);
Log.e("Filesize:", "AfterCompression: " + length);
} catch (IOException e) {
e.printStackTrace();
Log.e("Filesize:", "Error: " + e);
}
I did this to test if it was working first, but what I get in the console is:
/name.company.newapp E/Filesize:: Before: 4970874
/name.company.newapp E/Filesize:: BeforeConversion: 63489024
/name.company.newapp E/Filesize:: AfterConversion: 63489024
The size doesn't change at all. Is this the right approach ?
This happens because you're actually getting the size of memory used by the Bitmap object by calling sizeOf(bitmap) and not the actual file size.
As you should know, a bitmap operates with the number of pixels in an image. Even though you compress the image using a JPEG compression, the image's width and height do not change. Thus the number of pixels do not change and thus the Bitmap's size (in memory) would not change too.
However, if you save the compressed bitmap to a location in your hard disk and use File.length() to calculate the size of the compressed image, then you will notice the difference.
Please check the size before decoding and after compression:
length = sizeOf(original);
Also i would recommend you to flush and close the outputstream:
out.flush();
out.close();
Hope i could help!
Edit:
Please try the following method to decode your bitmap:
public static Bitmap decodeFile(File f,int WIDTH,int HEIGHT){
try {
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeStream(new FileInputStream(f),null,o);
final int REQUIRED_WIDTH=WIDTH;
final int REQUIRED_HEIGHT=HEIGHT;
//Find the correct scale value. It should be the power of 2.
int scale=1;
while(o.outWidth/scale/2>=REQUIRED_WIDTH && o.outHeight/scale/2>=REQUIRED_HEIGHT)
scale*=2;
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize=scale;
return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
} catch (FileNotFoundException e) {}
return null;
}
You can change the width and height of your picture to make it smaller.
I am trying to reduce the size of the bitmap after taking the picture. I am able to reduce the size to max 900kb but I wan to reduce it further as much as possible
First I do this:
public static Bitmap decodeSampledBitmapFromResource(byte[] data,
int reqWidth, int reqHeight) {
//reqWidth is 320
//reqHeight is 480
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeByteArray(data, 0, data.length, options);
}
public static 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;
}
then this
bitmap.compress(Bitmap.CompressFormat.PNG, 10, fos);
How do i compress the bitmap further ? or
Should I compress the byte[] instead?
I just want to send documents, black and white.
I know that the question has some time, but I think this answer could help someone.
There are three methods to compress your picture (the "byte[] data" you have recieved from the Camera):
1) PNG format -> It's the worst choice when the image has a lot of different colors, and in general, this is the case when you take a photo. The PNG format is more useful in images like logotypes and stuff like this.
The code:
String filename = "" //THE PATH WHERE YOU WANT TO PUT YOUR IMAGE
File pictureFile = new File(filename);
try{ //"data" is your "byte[] data"
FileOutputStream fos = new FileOutputStream(pictureFile);
Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
bmp.compress(Bitmap.CompressFormat.PNG, 100, fos); //100-best quality
fos.write(data);
fos.close();
} catch (Exception e){
Log.d("Error", "Image "+filename+" not saved: " + e.getMessage());
}
2) JPG format -> It's a better option with an image with a lot of colors (the algoritm to compress is very different than PNG), but it does not have the 'alpha channel' that could be necesary in some scenarios.
There is only one change from the previous code:
bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos); //100-best quality
3) WebP format -> It's the best format by far. An image of 1.0 MB in PNG could be compress to 100-200KB in JPG, and only to 30-50 KB in WebP. This format has also the 'alpha channel'. But it's not perfect: some browsers are not compatible with it. So you have to be careful about this in your project.
There is only one change from the previous code:
bmp.compress(Bitmap.CompressFormat.WEBP, 100, fos); //100-best quality
NOTES:
1) If this level of compress is not enought, you can play with the quality of the compress (the '100' that you can see in the bmp.compress() method). Be careful with the quality in the JPEG, because in some cases with a quality of 80%, you can see the impact in the image easyly. In PNG and WebP formats the loss percentage has less impact.
2) The other way to reduce the size is to resize the image. The easiest way I found is to simply create a smaller bitmap through Bitmap.createScaledBitmap method (more info).
3) And the last one method is to put the image in black and white. You can do this with this method:
Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
PS 1 - If someone wants to have a deeper knowledge about the image compress methods in Android, I recommend this talk by Colt McAnlis in the Google I/O 2016 (link).
You should use jpg instead of png if the alpha channel is not needed.
Also you can change the color config of your output bitmap:
// Decode bitmap with inSampleSize set
options.inPreferredConfig= Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeByteArray(data, 0, data.length, options);
I have read 100s of article about the OOM problem. Most are in regard to large bitmaps. I am doing a mapping application where we download 256x256 weather overlay tiles. Most are totally transparent and very small. I just got a crash on a bitmap stream that was 442 Bytes long while calling BitmapFactory.decodeByteArray(....).
The Exception states:
java.lang.OutOfMemoryError: bitmap size exceeds VM budget(Heap Size=9415KB, Allocated=5192KB, Bitmap Size=23671KB)
The code is:
protected Bitmap retrieveImageData() throws IOException {
URL url = new URL(imageUrl);
InputStream in = null;
OutputStream out = null;
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// determine the image size and allocate a buffer
int fileSize = connection.getContentLength();
if (fileSize < 0) {
return null;
}
byte[] imageData = new byte[fileSize];
// download the file
//Log.d(LOG_TAG, "fetching image " + imageUrl + " (" + fileSize + ")");
BufferedInputStream istream = new BufferedInputStream(connection.getInputStream());
int bytesRead = 0;
int offset = 0;
while (bytesRead != -1 && offset < fileSize) {
bytesRead = istream.read(imageData, offset, fileSize - offset);
offset += bytesRead;
}
// clean up
istream.close();
connection.disconnect();
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeByteArray(imageData, 0, bytesRead);
} catch (OutOfMemoryError e) {
Log.e("Map", "Tile Loader (241) Out Of Memory Error " + e.getLocalizedMessage());
System.gc();
}
return bitmap;
}
Here is what I see in the debugger:
bytesRead = 442
So the Bitmap data is 442 Bytes. Why would it be trying to create a 23671KB Bitmap and running out of memory?
I have run into problems like this in the past. Android uses Bitmap VM and it is very small. Make sure you dispose your bitmap via bmp.recycle. Later versions of Android have more Bitmap VM but the version that I've been dealing with has a 20MB limit.
This may work. Shrinking the bitmaps to lesser quality. I am not sure, but this may duplicate the image in memory, but could easily be worth a shot.
Bitmap image;
image = BitmapFactory.decodeByteArray(data, 0, data.length);
Bitmap mutableBitmap = image.copy(Bitmap.Config.ARGB_4444, true);
Canvas canvas = new Canvas(mutableBitmap);
My old answer below I don't think will work streaming.
Options options = new BitmapFactory.Options();
Options options2 = new BitmapFactory.Options();
Options options3 = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;///////includes alpha
options2.inPreferredConfig = Bitmap.Config.RGB_565 ;///////no alpha
options3.inPreferredConfig = Bitmap.Config.ARGB_4444 ;/////alpha lesser quality
image=BitmapFactory.decodeResource(getResources(),R.drawable.imagename,options);
image=Bitmap.createScaledBitmap(image, widthx,height, true);
It sounds like you've already done some reading on this subject, so I'll spare you the standard 'this is how you decode a bitmap' comments..
It jumps out at me that you're perhaps holding on to a reference of old bitmaps (perhaps the tile has moved off screen, but you still have a reference in an array somewhere and as a result it isn't being garbage collected?). This has bitten me really badly in the past - memory leaks are hard to debug.
There's a great Google I/O video over here that really helped me when I was having similar problems. It's around an hour, but will hopefully save you days later on.
It covers things like:
Creating heap dumps
Heap usage in DDMS
Using MAT to compare/analyze heap dumps