/*
* Copyright 2013, Edmodo, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License.
* You may obtain a copy of the License in the LICENSE file, or 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 com.theartofdev.edmodo.cropper.util;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.net.Uri;
import android.provider.MediaStore;
import android.view.View;
import android.widget.ImageView;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* Utility class that deals with operations with an ImageView.
*/
public class ImageViewUtil {
/**
* Gets the rectangular position of a Bitmap if it were placed inside a View.
*
* @param bitmap the Bitmap
* @param view the parent View of the Bitmap
* @param scaleType the desired scale type
* @return the rectangular position of the Bitmap
*/
public static Rect getBitmapRect(Bitmap bitmap, View view, ImageView.ScaleType scaleType) {
final int bitmapWidth = bitmap.getWidth();
final int bitmapHeight = bitmap.getHeight();
final int viewWidth = view.getWidth();
final int viewHeight = view.getHeight();
switch (scaleType) {
default:
case CENTER_INSIDE:
return getBitmapRectCenterInsideHelper(bitmapWidth, bitmapHeight, viewWidth, viewHeight);
case FIT_CENTER:
return getBitmapRectFitCenterHelper(bitmapWidth, bitmapHeight, viewWidth, viewHeight);
}
}
/**
* Gets the rectangular position of a Bitmap if it were placed inside a View.
*
* @param bitmapWidth the Bitmap's width
* @param bitmapHeight the Bitmap's height
* @param viewWidth the parent View's width
* @param viewHeight the parent View's height
* @param scaleType the desired scale type
* @return the rectangular position of the Bitmap
*/
public static Rect getBitmapRect(int bitmapWidth,
int bitmapHeight,
int viewWidth,
int viewHeight, ImageView.ScaleType scaleType) {
switch (scaleType) {
default:
case CENTER_INSIDE:
return getBitmapRectCenterInsideHelper(bitmapWidth, bitmapHeight, viewWidth, viewHeight);
case FIT_CENTER:
return getBitmapRectFitCenterHelper(bitmapWidth, bitmapHeight, viewWidth, viewHeight);
}
}
/**
* Rotate the given image by reading the Exif value of the image (uri).<br>
* If no rotation is required the image will not be rotated.<br>
* New bitmap is created and the old one is recycled.
*/
public static RotateBitmapResult rotateBitmapByExif(Context context, Bitmap bitmap, Uri uri) {
try {
File file = getFileFromUri(context, uri);
if (file.exists()) {
ExifInterface ei = new ExifInterface(file.getAbsolutePath());
return rotateBitmapByExif(bitmap, ei);
}
} catch (Exception ignored) {
}
return new RotateBitmapResult(bitmap, 0);
}
/**
* Rotate the given image by given Exif value.<br>
* If no rotation is required the image will not be rotated.<br>
* New bitmap is created and the old one is recycled.
*/
public static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, ExifInterface exif) {
int degrees = 0;
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degrees = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degrees = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degrees = 270;
break;
}
if (degrees > 0) {
bitmap = rotateBitmap(bitmap, degrees);
}
return new RotateBitmapResult(bitmap, degrees);
}
/**
* Decode bitmap from stream using sampling to get bitmap with the requested limit.
*/
public static DecodeBitmapResult decodeSampledBitmap(Context context, Uri uri, int reqWidth, int reqHeight) {
InputStream stream = null;
try {
ContentResolver resolver = context.getContentResolver();
stream = resolver.openInputStream(uri);
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(stream, new Rect(0, 0, 0, 0), options);
options.inJustDecodeBounds = false;
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
closeSafe(stream);
stream = resolver.openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(stream, new Rect(0, 0, 0, 0), options);
return new DecodeBitmapResult(bitmap, options.inSampleSize);
} catch (Exception e) {
throw new RuntimeException("Failed to load sampled bitmap", e);
} finally {
closeSafe(stream);
}
}
/**
* Decode specific rectangle bitmap from stream using sampling to get bitmap with the requested limit.
*/
public static DecodeBitmapResult decodeSampledBitmapRegion(Context context, Uri uri, Rect rect, int reqWidth, int reqHeight) {
InputStream stream = null;
try {
ContentResolver resolver = context.getContentResolver();
stream = resolver.openInputStream(uri);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calculateInSampleSize(rect.width(), rect.height(), reqWidth, reqHeight);
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(stream, false);
Bitmap bitmap = decoder.decodeRegion(rect, options);
return new DecodeBitmapResult(bitmap, options.inSampleSize);
} catch (Exception e) {
throw new RuntimeException("Failed to load sampled bitmap", e);
} finally {
closeSafe(stream);
}
}
/**
* Calculate the largest inSampleSize value that is a power of 2 and keeps both
* height and width larger than the requested height and width.
*/
public static int calculateInSampleSize(int width, int height, int reqWidth, int reqHeight) {
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;
}
/**
* Get {@link java.io.File} object for the given Android URI.<br>
* Use content resolver to get real path if direct path doesn't return valid file.
*/
public static File getFileFromUri(Context context, Uri uri) {
// first try by direct path
File file = new File(uri.getPath());
if (file.exists()) {
return file;
}
// try reading real path from content resolver (gallery images)
Cursor cursor = null;
try {
String[] proj = {MediaStore.Images.Media.DATA};
cursor = context.getContentResolver().query(uri, proj, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
String realPath = cursor.getString(column_index);
file = new File(realPath);
} catch (Exception ignored) {
} finally {
if (cursor != null)
cursor.close();
}
return file;
}
/**
* Rotate the given bitmap by the given degrees.<br>
* New bitmap is created and the old one is recycled.
*/
public static Bitmap rotateBitmap(Bitmap bitmap, int degrees) {
Matrix matrix = new Matrix();
matrix.setRotate(degrees);
Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
bitmap.recycle();
return newBitmap;
}
/**
* Close the given closeable object (Stream) in a safe way: check if it is null and catch-log
* exception thrown.
*
* @param closeable the closable object to close
*/
public static void closeSafe(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException ignored) {
}
}
}
/**
* Helper that does the work of the above functions. Gets the rectangular
* position of a Bitmap if it were placed inside a View with scale type set
* to {@link android.widget.ImageView.ScaleType #CENTER_INSIDE}.
*
* @param bitmapWidth the Bitmap's width
* @param bitmapHeight the Bitmap's height
* @param viewWidth the parent View's width
* @param viewHeight the parent View's height
* @return the rectangular position of the Bitmap
*/
private static Rect getBitmapRectCenterInsideHelper(int bitmapWidth,
int bitmapHeight,
int viewWidth,
int viewHeight) {
double resultWidth;
double resultHeight;
int resultX;
int resultY;
double viewToBitmapWidthRatio = Double.POSITIVE_INFINITY;
double viewToBitmapHeightRatio = Double.POSITIVE_INFINITY;
// Checks if either width or height needs to be fixed
if (viewWidth < bitmapWidth) {
viewToBitmapWidthRatio = (double) viewWidth / (double) bitmapWidth;
}
if (viewHeight < bitmapHeight) {
viewToBitmapHeightRatio = (double) viewHeight / (double) bitmapHeight;
}
// If either needs to be fixed, choose smallest ratio and calculate from
// there
if (viewToBitmapWidthRatio != Double.POSITIVE_INFINITY || viewToBitmapHeightRatio != Double.POSITIVE_INFINITY) {
if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) {
resultWidth = viewWidth;
resultHeight = (bitmapHeight * resultWidth / bitmapWidth);
} else {
resultHeight = viewHeight;
resultWidth = (bitmapWidth * resultHeight / bitmapHeight);
}
}
// Otherwise, the picture is within frame layout bounds. Desired width
// is simply picture size
else {
resultHeight = bitmapHeight;
resultWidth = bitmapWidth;
}
// Calculate the position of the bitmap inside the ImageView.
if (resultWidth == viewWidth) {
resultX = 0;
resultY = (int) Math.round((viewHeight - resultHeight) / 2);
} else if (resultHeight == viewHeight) {
resultX = (int) Math.round((viewWidth - resultWidth) / 2);
resultY = 0;
} else {
resultX = (int) Math.round((viewWidth - resultWidth) / 2);
resultY = (int) Math.round((viewHeight - resultHeight) / 2);
}
final Rect result = new Rect(resultX,
resultY,
resultX + (int) Math.ceil(resultWidth),
resultY + (int) Math.ceil(resultHeight));
return result;
}
/**
* Helper that does the work of the above functions. Gets the rectangular
* position of a Bitmap if it were placed inside a View with scale type set
* to {@link ImageView.ScaleType#FIT_CENTER}.
*
* @param bitmapWidth the Bitmap's width
* @param bitmapHeight the Bitmap's height
* @param viewWidth the parent View's width
* @param viewHeight the parent View's height
* @return the rectangular position of the Bitmap
*/
private static Rect getBitmapRectFitCenterHelper(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight) {
double resultWidth;
double resultHeight;
int resultX;
int resultY;
double viewToBitmapWidthRatio = (double) viewWidth / bitmapWidth;
double viewToBitmapHeightRatio = (double) viewHeight / bitmapHeight;
// If either needs to be fixed, choose smallest ratio and calculate from
// there
if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) {
resultWidth = viewWidth;
resultHeight = (bitmapHeight * resultWidth / bitmapWidth);
} else {
resultHeight = viewHeight;
resultWidth = (bitmapWidth * resultHeight / bitmapHeight);
}
// Calculate the position of the bitmap inside the ImageView.
if (resultWidth == viewWidth) {
resultX = 0;
resultY = (int) Math.round((viewHeight - resultHeight) / 2);
} else if (resultHeight == viewHeight) {
resultX = (int) Math.round((viewWidth - resultWidth) / 2);
resultY = 0;
} else {
resultX = (int) Math.round((viewWidth - resultWidth) / 2);
resultY = (int) Math.round((viewHeight - resultHeight) / 2);
}
final Rect result = new Rect(resultX,
resultY,
resultX + (int) Math.ceil(resultWidth),
resultY + (int) Math.ceil(resultHeight));
return result;
}
//region: Inner class: DecodeBitmapResult
/**
* The result of {@link #decodeSampledBitmap(android.content.Context, android.net.Uri, int, int)}.
*/
public static final class DecodeBitmapResult {
/**
* The loaded bitmap
*/
public final Bitmap bitmap;
/**
* The sample size used to load the given bitmap
*/
public final int sampleSize;
DecodeBitmapResult(Bitmap bitmap, int sampleSize) {
this.sampleSize = sampleSize;
this.bitmap = bitmap;
}
}
//endregion
//region: Inner class: RotateBitmapResult
/**
* The result of {@link #rotateBitmapByExif(android.graphics.Bitmap, android.media.ExifInterface)}.
*/
public static final class RotateBitmapResult {
/**
* The loaded bitmap
*/
public final Bitmap bitmap;
/**
* The degrees the image was rotated
*/
public final int degrees;
RotateBitmapResult(Bitmap bitmap, int degrees) {
this.bitmap = bitmap;
this.degrees = degrees;
}
}
//endregion
}