Compress image file to specified size - android

I have a requirement to make sure that all pictures my users take from ACTION_IMAGE_CAPTURE Intent in my app are under 4 MB due to server restrictions. What would be the best way to go about doing this? The files are JPEG if that matters.
So far I'm using a brute force method below but this doesn't work. (Edit: It works now with suggestions given below, but takes a while) I'm wondering if there is some more efficient method, perhaps using BitmapFactory.Options.inJustDecodeBounds, that could maybe map pixel count to number of bytes so I wouldn't need so many IO operations.
private final int MAX_IMAGE_SIZE = 4194304;
private void shrinkImageFile(File file) throws IOException
{
Bitmap bitmap = BitmapFactory.decodeFile(file.getPath());
int quality = 90;
while (file.length() > MAX_IMAGE_SIZE )
{
FileOutputStream out = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, out);
quality -= 10;
}
}

Related

How can I get the device orientation when a photo was taken

I've never been struggling this much with images as I am today and I'd appreciate some help :D
So, thing is, I'm using the built-in camera to capture a picture and then send it to the back end to save it but the orientation is messed up for Kit kat and specially Samsung devices. I tried to use the exif interface everyone suggests, but I just can't get the photo orientation.
A few minutes ago I found an answer somewhat related to this, saying that maybe a good solution is to save the device orientation when the picture is taken, which sounds pretty nice, however, I don't know how to do that with the built in camera, since I don't have full control when opening the camera with an Intent, like this:
mPathToTakenImage = ImageProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider",
newFile);
openCamera.putExtra(MediaStore.EXTRA_OUTPUT, mPathToTakenImage);
openCamera.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(openCamera, AlgebraNationConstants.TAKE_PHOTO_REQUEST_CODE);
So, how can I get the device orientation while the image was being taken in order to rotate the image correctly?
This is the code to rotate the image, however, I'm getting always zero:
final Bitmap finalImg;
final StringBuilder base64Image = new StringBuilder("data:image/jpeg;base64,");
final ExifInterface exifInterface;
try {
final String imagePath = params[0].getPath();
exifInterface = new ExifInterface(imagePath);
final int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED);
final Bitmap takenPhoto = MediaStore.Images.Media.getBitmap(mRelatedContext.getContentResolver(),
params[0]);
if (null == takenPhoto) {
base64Image.setLength(0);
} else {
finalImg = rotateBitmap(takenPhoto, orientation);
if (null == finalImg) {
base64Image.setLength(0);
} else {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
finalImg.compress(Bitmap.CompressFormat.JPEG, 75, byteArrayOutputStream);
final byte[] byteArray = byteArrayOutputStream.toByteArray();
base64Image.append(Base64.encodeToString(byteArray, Base64.DEFAULT));
}
}
} catch (IOException e) {
e.printStackTrace();
}
return base64Image.length() == 0 ? null : base64Image.toString();
I'm going crazy with this, any help will be deeply appreciated.
EDIT:
A Uri is not a file. Unless the scheme of the Uri is file, getPath() is meaningless. In your case, the scheme is mostly going to be content, not file. As it stands, you are not getting EXIF headers, because ExifInterface cannot find that file.
Use a ContentResolver and openInputStream() to open an InputStream on the content identified by the Uri. Pass that InputStream to the android.support.media.ExifInterface constructor.
Also, bear in mind that you will crash much of the time with an OutOfMemoryError, as you will not have heap space to hold a base64-encoded photo.
So, I finally resolved my problem making use of these two answers:
https://stackoverflow.com/a/17426328
https://stackoverflow.com/a/40865982
In general, the issue was due to the image allocation in disk since I don't know why Android didn't like the fact that I was giving my own path to save the taken image. At the end, the Intent to open the camera is looking like this:
final Intent openCamera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(openCamera, AlgebraNationConstants.TAKE_PHOTO_REQUEST_CODE);
And Android itself is resolving where to put the image. Nevertheless, #CommonsWare thanks for the enlightenment and answers.

Android EXIF data always 0, how to change it?

I have an app that captures photos using the native Camera and then uploads them to a server. My problem is that all the photos have an EXIF orientation value of 0, and this messes up the display elsewhere.
How can I change the EXIF orientation? I'm not looking for a way to correct it for every circumstance, just change it to a different value.
I'm using a Samsung Galaxy Note 4
I tried this solution that sets the camera orientation before taking photos: Setting Android Photo EXIF Orientation
Camera c = Camera.open();
c.setDisplayOrientation(90);
Camera.Parameters params = mCamera.getParameters();
params.setRotation(0); // tried 0, 90, 180
c.setParameters(params);
but it doesn't influence the resulting EXIF data, its still always 0
I also tried these solutions where the image is rotated after it is taken: EXIF orientation tag value always 0 for image taken with portrait camera app android
and while this rotates the photo, the EXIF orientation is still always 0.
I also tried setting the EXIF data directly: How to save Exif data after bitmap compression in Android
private Camera.PictureCallback mPicture = new Camera.PictureCallback() {
#Override
public void onPictureTaken(byte[] data, Camera camera) {
final File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE, "");
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
ExifInterface exif = new ExifInterface(pictureFile.toString());
exif.setAttribute(ExifInterface.TAG_ORIENTATION, "3");
exif.saveAttributes();
fos.write(data);
fos.close();
//upload photo..
}
}
}
but EXIF Orientation is still 0 after uploading.
I have also looked at these solutions:
Exif data TAG_ORIENTATION always 0
How to write exif data to image in Android?
How to get the Correct orientation of the image selected from the Default Image gallery
how to set camera Image orientation?
but they all involve correcting the orientation by rotating, which doesn't influence the EXIF data, or setting the EXIF data directly which doesn't seem to work.
How can I change the file's EXIF orientation data from 0 to 3?
UPDATE:
here is my upload code:
Bitmap sBitmap = null;
final File sResizedFile = getOutputMediaFile(MEDIA_TYPE_IMAGE, "_2");
try {
sBitmap = BitmapFactory.decodeStream(new FileInputStream(pictureFile), null, options);
} catch (FileNotFoundException e) {
Log.e("App", "[MainActivity] unable to convert pictureFile to bitmap");
e.printStackTrace();
return;
}
// ... compute sw and sh int values
Bitmap sOut = Bitmap.createScaledBitmap(sBitmap, sw, sh, false);
Bitmap rotatedBitmap = rotateBitmap(sOut, 3);
FileOutputStream sfOut;
try {
sfOut = new FileOutputStream(sResizedFile);
rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 70, sfOut);
sfOut.flush();
sfOut.close();
sBitmap.recycle();
sOut.recycle();
rotatedBitmap.recycle();
} catch (Exception e) {
Log.e("App", "[MainActivity] unable to save thumbnail");
e.printStackTrace();
return;
}
// upload small thumbnail
TransferObserver sObserver = transferUtility.upload(
"stills/small", /* The bucket to upload to */
filename + ".jpg", /* The key for the uploaded object */
sResizedFile /* The file where the data to upload exists */
);
As you can see, the The EXIF information is not reliable on Android (especially Samsung devices).
However the phone SQL database holding the references to Media object is reliable. I would propose going this way.
Getting the orientation from the Uri:
private static int getOrientation(Context context, Uri photoUri) {
Cursor cursor = context.getContentResolver().query(photoUri,
new String[]{MediaStore.Images.ImageColumns.ORIENTATION}, null, null, null);
if (cursor.getCount() != 1) {
cursor.close();
return -1;
}
cursor.moveToFirst();
int orientation = cursor.getInt(0);
cursor.close();
cursor = null;
return orientation;
}
Then initialize rotated Bitmap:
public static Bitmap rotateBitmap(Context context, Uri photoUri, Bitmap bitmap) {
int orientation = getOrientation(context, photoUri);
if (orientation <= 0) {
return bitmap;
}
Matrix matrix = new Matrix();
matrix.postRotate(orientation);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
return bitmap;
}
If you want to change the orientation of the image, try the following snippet:
public static boolean setOrientation(Context context, Uri fileUri, int orientation) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.ORIENTATION, orientation);
int rowsUpdated = context.getContentResolver().update(fileUri, values, null, null);
return rowsUpdated > 0;
}
If you set the orientation of the image, later it will be constantly set at the correct orientation. There is need to make use of ExifInterface later, because the image is already rotated in proper way.
If this method is not satisfactory, then you could try this method
You have accepted your own answer as solution. My rant is just useful side-info, anyways...
The "However..." in your Answer suggests while you now know the cause, you don't have a fix.
Turns out my code was able to set the EXIF data, but there is a
discrepancy between how Android interprets this data and how iOS...
interprets it.
This could be an endianness issue. You can try manually changing the endianness setting of Exif by opening your jpeg in a hex editor and finding...
The bytes 45 78 69 66 (makes "Exif" text) followed by two zero bytes 00 00.
Then it should be 49 49 (makes "II" text) which means read data as little endian format.
If you replace it with 4D 4D (or "MM" text) then the reading side
will consider data as big endian.
Test this in iOS to see if numbers are now correct.
and regarding this...
However, setting 3 shows up as0 on iOS and the image is sideways in
Chrome.
Setting 6 shows up as 3 on iOS and the image looks right in Chrome.
Only thing I can add is that iOS Mac is Big Endian* and Android/PC is Little Endian. Essentially Little Endian reads/writes bytes as right-to-left whilst Big Endian is opposite.
In binary : 011 means 3 and 110 means 6. The difference between 3 and 6 is simply the reading order of those bits of ones & zeroes. So a system that reads as zero-one-one gets a result of 3 but the other Endian system will read a byte with same bits as one-one-zero and tell you result is a 6. I can't explain why "3 shows up as 0" without a test file to analyse bytes but it's a strange result to me.
</end rant>
<sleep>
*note: While Macs are Big Endian, double-checking says iOS uses Little Endian system after all. Your numbers still suggest a Big vs Little Endian issue though.
Turns out my code was able to set the EXIF data, but there is a discrepancy between how Android interprets this data and how iOS and Chrome on a Mac (where I was checking the resulting file) interprets it.
This is the only code needed to set EXIF orientation:
ExifInterface exif = new ExifInterface(pictureFile.toString());
exif.setAttribute(ExifInterface.TAG_ORIENTATION, "3");
exif.saveAttributes();
However, setting 3 shows up as 0 on iOS and the image is sideways in Chrome.
setting 6 shows up as 3 on iOS and the image looks right in Chrome.
Refer this GitHub project https://github.com/pandiaraj44/Camera. It has the custom camera activity where EXIF TAG_ORIENTATION was handled correctly. You can clone the project and check. For code details please refer https://github.com/pandiaraj44/Camera/blob/master/app/src/main/java/com/pansapp/cameraview/CameraFragment.java
There is one class to read and update these information of images.
To update the attributes you can use like this
ExifInterface ef = new ExifInterface(filePath);
ef.setAttribute(MAKE_TAG, MAKE_TAG);
ef.setAttribute(ExifInterface.TAG_ORIENTATION, orientation+"");
ef.saveAttributes();
and for reading you can use like this
ExifInterface exif = null;
try {
exif = new ExifInterface(absolutePath+path);
} catch (IOException e) {
e.printStackTrace();
}
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED);
I hope it will help you

How can I read from a read-only RandomAccessFile while a MediaPlayer is playing from it in a different position

I have a custom file format (similar to a zip file) that packs small files (small images, mp3 files) into 1 physical file. My android app downloads this file, and it displays one image from it. The user can touch the image and it'll start to play one of the small mp3 "files" inside the packed file. He can also swipe left or right, and the app displays the previous or next image.
In order to make things smoother I am holding 3 "cards" in the memory: the one currently displayed, and the prevous and the next one. This way when it's swiped, I can immediatelly show the next image. In order to do this, I am preloading the images and the mp3 into the MediaPlayer. The problem is that because of this it is multi threaded, as the preloading is done in the background. I have a bug: when I start to play the mp3, and during it's playing I swipe, the image I preaload is cut in the middle. After lots of debugging, I found the reason: while I load the image, the MediaPlayer is moving the file pointer in the file descriptor, and that causes the next read to read from the middle of the mp3 instead of the image.
Here's the code:
InputStream imageStream = myPackedFile.getBaseStream("cat.jpg"); // this returns an InputStream representing "cat.jpg" from my packed file (which is based on a RandomAccessFile)
Drawable image = Drawable.createFromStream(imageStream, imagePath);
FileDescriptor fd = myPackedFile.getFD();
long pos = myPackedFile.getPos("cat.mp3");
long len = myPackedFile.getLength("cat.mp3");
player.setDataSource(fd, pos, len);
player.prepare();
This is what worked for me: Instead of creating a RandomAccessFile and holding to it, I create a File, and every time I need to access it as a RandomAccessFile I create a new one:
public class PackagedFile {
private File file;
PackagedFile(String filename) {
file = new File(filename);
}
public RandomAccessFile getRandomAccessFile() {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return raf;
}
}
and the above code became:
InputStream imageStream = myPackedFile.getBaseStream("cat.jpg"); // this returns an InputStream representing "cat.jpg" from my packed file (which is based on a RandomAccessFile)
Drawable image = Drawable.createFromStream(imageStream, imagePath);
FileDescriptor fd = myPackedFile.getRandomAccessFile().getFD();
long pos = myPackedFile.getPos("cat.mp3");
long len = myPackedFile.getLength("cat.mp3");
player.setDataSource(fd, pos, len);
player.prepare();
For API Level 13 and above, one can consider ParcelFileDescriptor.dup to duplicate the file descriptors. For more information, please refer to this link: http://androidxref.com/4.2.2_r1/xref/frameworks/base/core/java/android/app/ActivityThread.java#864

Bitmap compress PNG -> JPEG and vice versa in Android

I have problem with size of picture when I'm doing conversion from PNG to JPEG, and then JPEG to PNG.
public void onClick(View v) {
String imageFileName = "/sdcard/Penguins2.png";
File imageFile = new File(imageFileName);
if (imageFile.exists()) {
// Load the image from file
myBitmap = BitmapFactory.decodeFile(imageFileName);
// Display the image in the image viewer
myImageView = (ImageView) findViewById(R.id.my_image_view);
if (myImageView != null) {
myImageView.setImageBitmap(myBitmap);
}
}
}
Conversion:
private void processImage() {
try {
String outputPath = "/sdcard/Penguins2.jpg";
int quality = 100;
FileOutputStream fileOutStr = new FileOutputStream(outputPath);
BufferedOutputStream bufOutStr = new BufferedOutputStream(
fileOutStr);
myBitmap.compress(CompressFormat.JPEG, quality, bufOutStr);
bufOutStr.flush();
bufOutStr.close();
} catch (FileNotFoundException exception) {
Log.e("debug_log", exception.toString());
} catch (IOException exception) {
Log.e("debug_log", exception.toString());
}
myImageView.setImageBitmap(myBitmap);
After processing this operation I just change these lines:
String imageFileName = "/sdcard/Penguins2.png";
to
String imageFileName = "/sdcard/Penguins2.jpg";
and
String outputPath = "/sdcard/Penguins2.jpg";
(...)
myBitmap.compress(CompressFormat.JPEG, quality, bufOutStr);
to
String outputPath = "/sdcard/Penguins2.png";
(...)
myBitmap.compress(CompressFormat.PNG, quality, bufOutStr);
Size of image changed from 585847 to 531409 (in DDMS)
I want to do such thing because I'd like to use PNG which is lossless for some image processing.
Then converse image to jpeg and send as MMS, I'm not sure but I think JPEG is only format which is support by all devices in MMS. Receiver would open image and converse it back to png without losing data.
In addition to the #Sherif elKhatib answer, if you check the documentation: http://developer.android.com/reference/android/graphics/Bitmap.html#compress%28android.graphics.Bitmap.CompressFormat,%20int,%20java.io.OutputStream%29
You can see that the PNG images don't use the quality paremeter:
quality: Hint to the compressor, 0-100. 0 meaning compress for small size, 100 meaning compress for max quality. Some formats, like PNG which is lossless, will ignore the quality setting
This is not doable! Once you convert to JPG you lost the "lossless state of PNG".
Anyway png is supported by everyone.
+In your case, you want the receiver to change it back to PNG to retrieve the lossless image. This means that the receiver also supports PNG. What is the point of changing it to JPG before sending and then changing it back to PNG when received. Just some extra computations?

Downsample image below size limit before upload

There's a 2MB limit on the server where I need to upload an image.
I'm using this method to downsample a Bitmap Strange out of memory issue while loading an image to a Bitmap object
within this method
public InputStream getPhotoStream(int imageSizeBytes) throws IOException {
int targetLength = 1500;
ByteArrayOutputStream photoStream;
byte[] photo;
Bitmap pic;
final int MAX_QUALITY = 100;
int actualSize = -1;
do {
photo = null;
pic = null;
photoStream = null;
//this calls the downsampling method
pic = getPhoto(targetLength);
photoStream = new ByteArrayOutputStream();
pic.compress(CompressFormat.JPEG, MAX_QUALITY, photoStream);
photo = photoStream.toByteArray();
actualSize = photo.length;
targetLength /= 2;
} while (actualSize > imageSizeBytes);
return new ByteArrayInputStream(photo);
}
This throws OutOfMemoryError on the second iteration. How could I downsample the image below a certain size limit?
I think the problem is happening because you are compressing the image to a in memory representation, you need to free up that memory before you try to compress again.
You need call close() in the photoStream before trying again in order to free up resources.
Also toByteArray() makes a copy of the stream in memory that you have to free up later, why don't you just use the photoStream.size() to check for the file size?
I can post some code if you need.
Instead of this:
pic = null;
Do this:
if (pic!=null)
pic.recycle();
pic = null
If you simply set the bitmap object to null the memory it occupied isn't released immediately. In the second case you are explicitly telling the OS you are done with the bitmap and its okay to release its memory.
Also consider using a compression quality of 90 instead of 100, I believe that will reduce the resulting file size quite a bit.

Categories

Resources