/*
* Copyright 2014 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.androidsdk.util;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ExifInterface;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;
import org.matrix.androidsdk.util.Log;
import org.matrix.androidsdk.db.MXMediasCache;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ImageUtils {
private static final String LOG_TAG = "ImageUtils";
/**
* Gets the bitmap rotation angle from the {@link android.media.ExifInterface}.
* @param context Application context for the content resolver.
* @param uri The URI to find the orientation for. Must be local.
* @return The orientation value, which may be {@link android.media.ExifInterface#ORIENTATION_UNDEFINED}.
*/
public static int getRotationAngleForBitmap(Context context, Uri uri) {
int orientation = getOrientationForBitmap(context, uri);
int rotationAngle = 0;
if (ExifInterface.ORIENTATION_ROTATE_90 == orientation) {
rotationAngle = 90;
} else if (ExifInterface.ORIENTATION_ROTATE_180 == orientation) {
rotationAngle = 180 ;
} else if (ExifInterface.ORIENTATION_ROTATE_270 == orientation) {
rotationAngle = 270;
}
return rotationAngle;
}
/**
* Gets the {@link ExifInterface} value for the orientation for this local bitmap Uri.
* @param context Application context for the content resolver.
* @param uri The URI to find the orientation for. Must be local.
* @return The orientation value, which may be {@link ExifInterface#ORIENTATION_UNDEFINED}.
*/
public static int getOrientationForBitmap(Context context, Uri uri) {
int orientation = ExifInterface.ORIENTATION_UNDEFINED;
if (uri == null) {
return orientation;
}
if (TextUtils.equals(uri.getScheme(), "content")) {
String [] proj= {MediaStore.Images.Media.DATA};
Cursor cursor = null;
try {
cursor = context.getContentResolver().query( uri, proj, null, null, null);
if (cursor != null && cursor.getCount() > 0) {
cursor.moveToFirst();
int idxData = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
String path = cursor.getString(idxData);
if (TextUtils.isEmpty(path)) {
Log.w(LOG_TAG, "Cannot find path in media db for uri " + uri);
return orientation;
}
ExifInterface exif = new ExifInterface(path);
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
}
}
catch (Exception e) {
// eg SecurityException from com.google.android.apps.photos.content.GooglePhotosImageProvider URIs
// eg IOException from trying to parse the returned path as a file when it is an http uri.
Log.e(LOG_TAG, "Cannot get orientation for bitmap: " + e.getLocalizedMessage());
}
finally {
if (cursor != null) {
cursor.close();
}
}
}
else if (TextUtils.equals(uri.getScheme(), "file")) {
try {
ExifInterface exif = new ExifInterface(uri.getPath());
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
}
catch (Exception e) {
Log.e(LOG_TAG, "Cannot get EXIF for file uri "+uri+" because " + e.getLocalizedMessage());
}
}
return orientation;
}
public static BitmapFactory.Options decodeBitmapDimensions(InputStream stream) {
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeStream(stream, null, o);
if (o.outHeight == -1 || o.outWidth == -1) {
// this doesn't look like an image...
Log.e(LOG_TAG, "Cannot resize input stream, failed to get w/h.");
return null;
}
return o;
}
public static int getSampleSize(int w, int h, int maxSize) {
int highestDimensionSize = (h > w) ? h : w;
double ratio = (highestDimensionSize > maxSize) ? (highestDimensionSize / maxSize) : 1.0;
int sampleSize = Integer.highestOneBit((int) Math.floor(ratio));
if (sampleSize == 0) {
sampleSize = 1;
}
return sampleSize;
}
/**
* Resize an image from its stream.
*
* @param fullImageStream the image stream
* @param maxSize the square side to draw the image in. -1 to ignore.
* @param aSampleSize the image dimension divider.
* @param quality the image quality (0 -> 100)
* @return a stream of the resized imaged
* @throws IOException
*/
public static InputStream resizeImage(InputStream fullImageStream, int maxSize, int aSampleSize, int quality) throws IOException {
/*
* This is all a bit of a mess because android doesn't ship with sensible bitmap streaming libraries.
*
* General structure here is: (N = size of file, M = decompressed size)
* - Copy inputstream to outstream (Usage: 2N)
* - Release inputstream (Usage: N)
* - Copy outstream to instream (Usage: 2N) --- This is done to make sure we can .reset() the stream else we would potentially
* have to re-download the file once we knew the dimensions of the image (!!!)
* - Release outstream (Usage: N)
* - Decode image dimensions, if the size is good, just return instream, else:
* - Decode the full image with the new sample size (Usage: N + M)
* - Release instream (Usage: M)
* - Bitmap compress to JPEG output stream (Usage: N + M)
* - Release bitmap (Usage: N)
* - Return input stream of output stream (Usage: N)
* Usages assume immediate GC, which is no guarantee. If it didn't, the total usage is 5N + M. In an extreme scenario
* of a full 8 MP image roughly 1.85MB file (3264x2448), this equates to roughly 25 MB of memory. On average, it will
* maybe not immediately release the streams but will probably in the future, so maybe 3N which is ~5.55MB - either
* way this isn't cool.
*/
ByteArrayOutputStream outstream = new ByteArrayOutputStream();
// copy the bytes we just got to the byte array output stream so we can resize....
byte[] buffer = new byte[2048];
int l;
while ((l = fullImageStream.read(buffer)) != -1) {
outstream.write(buffer, 0, l);
}
// we're done with the input stream now so get rid of it (bearing in mind this could be several MB..)
fullImageStream.close();
// get the width/height of the image without decoding ALL THE THINGS (though this still makes a copy of the compressed image :/)
ByteArrayInputStream bais = new ByteArrayInputStream(outstream.toByteArray());
// allow it to GC..
outstream.close();
BitmapFactory.Options o = decodeBitmapDimensions(bais);
if (o == null) {
return null;
}
int w = o.outWidth;
int h = o.outHeight;
bais.reset(); // yay no need to re-read the stream (which is why we dumped to another stream)
int sampleSize = (maxSize == -1) ? aSampleSize : getSampleSize(w, h, maxSize);
if (sampleSize == 1) {
// small optimisation
return bais;
} else {
// yucky, we have to decompress the entire (albeit subsampled) bitmap into memory then dump it back into a stream
o = new BitmapFactory.Options();
o.inSampleSize = sampleSize;
Bitmap bitmap = BitmapFactory.decodeStream(bais, null, o);
if (bitmap == null) {
return null;
}
bais.close();
// recopy it back into an input stream :/
outstream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outstream);
// cleanup
bitmap.recycle();
return new ByteArrayInputStream(outstream.toByteArray());
}
}
/**
* Apply rotation to the cached image (stored at imageURL).
* The rotated image replaces the genuine one.
* @param context the application
* @param imageURL the genuine image URL.
* @param rotationAngle angle in degrees
* @param mediasCache the used media cache
* @return the rotated bitmap
*/
public static Bitmap rotateImage(Context context, String imageURL, int rotationAngle, MXMediasCache mediasCache) {
Bitmap rotatedBitmap = null;
try
{
Uri imageUri = Uri.parse(imageURL);
// there is one
if (0 != rotationAngle) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
options.outWidth = -1;
options.outHeight = -1;
// decode the bitmap
Bitmap bitmap = null;
try {
final String filename = imageUri.getPath();
FileInputStream imageStream = new FileInputStream(new File(filename));
bitmap = BitmapFactory.decodeStream(imageStream, null, options);
imageStream.close();
} catch (OutOfMemoryError e) {
Log.e(LOG_TAG, "applyExifRotation BitmapFactory.decodeStream : " + e.getLocalizedMessage());
} catch (Exception e) {
Log.e(LOG_TAG, "applyExifRotation " + e.getLocalizedMessage());
}
android.graphics.Matrix bitmapMatrix = new android.graphics.Matrix();
bitmapMatrix.postRotate(rotationAngle);
Bitmap transformedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), bitmapMatrix, false);
bitmap.recycle();
if (null != mediasCache) {
mediasCache.saveBitmap(transformedBitmap, imageURL);
}
rotatedBitmap = transformedBitmap;
}
} catch (OutOfMemoryError e) {
Log.e(LOG_TAG, "applyExifRotation " + e.getLocalizedMessage());
} catch (Exception e) {
Log.e(LOG_TAG, "applyExifRotation " + e.getLocalizedMessage());
}
return rotatedBitmap;
}
/**
* Apply exif rotation to the cached image (stored at imageURL).
* The rotated image replaces the genuine one.
* @param context the application
* @param imageURL the genuine image URL.
* @param mediasCache the used media cache
* @return the rotated bitmap if the operation succeeds.
*/
public static Bitmap applyExifRotation(Context context, String imageURL, MXMediasCache mediasCache) {
Bitmap rotatedBitmap = null;
try
{
Uri imageUri = Uri.parse(imageURL);
// get the exif rotation angle
final int rotationAngle = ImageUtils.getRotationAngleForBitmap(context, imageUri);
if (0 != rotationAngle) {
rotatedBitmap = rotateImage(context, imageURL, rotationAngle, mediasCache);
}
} catch (Exception e) {
Log.e(LOG_TAG, "applyExifRotation " + e.getLocalizedMessage());
}
return rotatedBitmap;
}
/**
* Scale and apply exif rotation to an image defines by its stream.
* @param context the context.
* @param maxSide reduce the image to this square side.
* @param mediasCache the media cache.
* @return the media url
*/
public static String scaleAndRotateImage(Context context, InputStream stream, String mimeType, int maxSide, int rotationAngle, MXMediasCache mediasCache) {
String url = null;
// sanity checks
if ((null != context) && (null != stream) && (null != mediasCache)) {
try {
InputStream scaledStream = ImageUtils.resizeImage(stream, maxSide, 0, 75);
url = mediasCache.saveMedia(scaledStream, null, mimeType);
rotateImage(context, url, rotationAngle, mediasCache);
} catch (Exception e) {
Log.e(LOG_TAG, "rotateAndScale " + e.getLocalizedMessage());
}
}
return url;
}
}