blob: 2a7d5f9ad91b8c5032a406ff81422132695abeb3 [file] [log] [blame]
package com.bumptech.glide.load.resource.bitmap;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.media.ExifInterface;
import android.os.Build;
import android.util.Log;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
/**
* A class with methods to efficiently resize Bitmaps.
*/
public final class TransformationUtils {
private static final String TAG = "TransformationUtils";
public static final int PAINT_FLAGS = Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG;
private TransformationUtils() {
// Utility class.
}
/**
* A potentially expensive operation to crop the given Bitmap so that it fills the given dimensions. This operation
* is significantly less expensive in terms of memory if a mutable Bitmap with the given dimensions is passed in
* as well.
*
* @param recycled A mutable Bitmap with dimensions width and height that we can load the cropped portion of toCrop
* into.
* @param toCrop The Bitmap to resize.
* @param width The width in pixels of the final Bitmap.
* @param height The height in pixels of the final Bitmap.
* @return The resized Bitmap (will be recycled if recycled is not null).
*/
public static Bitmap centerCrop(Bitmap recycled, Bitmap toCrop, int width, int height) {
if (toCrop == null) {
return null;
} else if (toCrop.getWidth() == width && toCrop.getHeight() == height) {
return toCrop;
}
// From ImageView/Bitmap.createScaledBitmap.
final float scale;
float dx = 0, dy = 0;
Matrix m = new Matrix();
if (toCrop.getWidth() * height > width * toCrop.getHeight()) {
scale = (float) height / (float) toCrop.getHeight();
dx = (width - toCrop.getWidth() * scale) * 0.5f;
} else {
scale = (float) width / (float) toCrop.getWidth();
dy = (height - toCrop.getHeight() * scale) * 0.5f;
}
m.setScale(scale, scale);
m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
final Bitmap result;
if (recycled != null) {
result = recycled;
} else {
result = Bitmap.createBitmap(width, height, toCrop.getConfig() == null
? Bitmap.Config.ARGB_8888 : toCrop.getConfig());
}
// We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given.
TransformationUtils.setAlpha(toCrop, result);
Canvas canvas = new Canvas(result);
Paint paint = new Paint(PAINT_FLAGS);
canvas.drawBitmap(toCrop, m, paint);
return result;
}
/**
* An expensive operation to resize the given Bitmap down so that it fits within the given dimensions maintain
* the original proportions.
*
* @param toFit The Bitmap to shrink.
* @param pool The BitmapPool to try to reuse a bitmap from.
* @param width The width in pixels the final image will fit within.
* @param height The height in pixels the final image will fit within.
* @return A new Bitmap shrunk to fit within the given dimensions, or toFit if toFit's width or height matches the
* given dimensions and toFit fits within the given dimensions
*/
public static Bitmap fitCenter(Bitmap toFit, BitmapPool pool, int width, int height) {
if (toFit.getWidth() == width && toFit.getHeight() == height) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "requested target size matches input, returning input");
}
return toFit;
}
final float widthPercentage = width / (float) toFit.getWidth();
final float heightPercentage = height / (float) toFit.getHeight();
final float minPercentage = Math.min(widthPercentage, heightPercentage);
// take the floor of the target width/height, not round. If the matrix
// passed into drawBitmap rounds differently, we want to slightly
// overdraw, not underdraw, to avoid artifacts from bitmap reuse.
final int targetWidth = (int) (minPercentage * toFit.getWidth());
final int targetHeight = (int) (minPercentage * toFit.getHeight());
if (toFit.getWidth() == targetWidth && toFit.getHeight() == targetHeight) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "adjusted target size matches input, returning input");
}
return toFit;
}
Bitmap.Config config = toFit.getConfig() != null ? toFit.getConfig() : Bitmap.Config.ARGB_8888;
Bitmap toReuse = pool.get(targetWidth, targetHeight, config);
if (toReuse == null) {
toReuse = Bitmap.createBitmap(targetWidth, targetHeight, config);
}
// We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given.
TransformationUtils.setAlpha(toFit, toReuse);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "request: " + width + "x" + height);
Log.v(TAG, "toFit: " + toFit.getWidth() + "x" + toFit.getHeight());
Log.v(TAG, "toReuse: " + toReuse.getWidth() + "x" + toReuse.getHeight());
Log.v(TAG, "minPct: " + minPercentage);
}
Canvas canvas = new Canvas(toReuse);
Matrix matrix = new Matrix();
matrix.setScale(minPercentage, minPercentage);
Paint paint = new Paint(PAINT_FLAGS);
canvas.drawBitmap(toFit, matrix, paint);
return toReuse;
}
/**
* Sets the alpha of the Bitmap we're going to re-use to the alpha of the Bitmap we're going to transform. This
* keeps {@link android.graphics.Bitmap#hasAlpha()}} consistent before and after the transformation for
* transformations that don't add or remove transparent pixels.
*
* @param toTransform The {@link android.graphics.Bitmap} that will be transformed.
* @param outBitmap The {@link android.graphics.Bitmap} that will be returned from the transformation.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public static void setAlpha(Bitmap toTransform, Bitmap outBitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 && outBitmap != null) {
outBitmap.setHasAlpha(toTransform.hasAlpha());
}
}
/**
* Returns a matrix with rotation set based on Exif orientation tag.
* If the orientation is undefined or 0 null is returned.
*
* @deprecated No longer used by Glide, scheduled to be removed in Glide 4.0
* @param pathToOriginal Path to original image file that may have exif data.
* @return A rotation in degrees based on exif orientation
*/
@TargetApi(Build.VERSION_CODES.ECLAIR)
@Deprecated
public static int getOrientation(String pathToOriginal) {
int degreesToRotate = 0;
try {
ExifInterface exif = new ExifInterface(pathToOriginal);
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
return getExifOrientationDegrees(orientation);
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.ERROR)) {
Log.e(TAG, "Unable to get orientation for image with path=" + pathToOriginal, e);
}
}
return degreesToRotate;
}
/**
* This is an expensive operation that copies the image in place with the pixels rotated.
* If possible rather use getOrientationMatrix, and set that as the imageMatrix on an ImageView.
*
* @deprecated No longer used by Glide, scheduled to be removed in Glide 4.0
* @param pathToOriginal Path to original image file that may have exif data.
* @param imageToOrient Image Bitmap to orient.
* @return The oriented bitmap. May be the imageToOrient without modification, or a new Bitmap.
*/
@Deprecated
public static Bitmap orientImage(String pathToOriginal, Bitmap imageToOrient) {
int degreesToRotate = getOrientation(pathToOriginal);
return rotateImage(imageToOrient, degreesToRotate);
}
/**
* This is an expensive operation that copies the image in place with the pixels rotated.
* If possible rather use getOrientationMatrix, and set that as the imageMatrix on an ImageView.
*
* @param imageToOrient Image Bitmap to orient.
* @param degreesToRotate number of degrees to rotate the image by. If zero the original image is returned
* unmodified.
* @return The oriented bitmap. May be the imageToOrient without modification, or a new Bitmap.
*/
public static Bitmap rotateImage(Bitmap imageToOrient, int degreesToRotate) {
Bitmap result = imageToOrient;
try {
if (degreesToRotate != 0) {
Matrix matrix = new Matrix();
matrix.setRotate(degreesToRotate);
result = Bitmap.createBitmap(
imageToOrient,
0,
0,
imageToOrient.getWidth(),
imageToOrient.getHeight(),
matrix,
true);
}
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.ERROR)) {
Log.e(TAG, "Exception when trying to orient image", e);
}
}
return result;
}
/**
* Get the # of degrees an image must be rotated to match the given exif orientation.
*
* @param exifOrientation The exif orientation [1-8]
* @return the number of degrees to rotate
*/
public static int getExifOrientationDegrees(int exifOrientation) {
final int degreesToRotate;
switch (exifOrientation) {
case ExifInterface.ORIENTATION_TRANSPOSE:
case ExifInterface.ORIENTATION_ROTATE_90:
degreesToRotate = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
degreesToRotate = 180;
break;
case ExifInterface.ORIENTATION_TRANSVERSE:
case ExifInterface.ORIENTATION_ROTATE_270:
degreesToRotate = 270;
break;
default:
degreesToRotate = 0;
}
return degreesToRotate;
}
/**
* Rotate and/or flip the image to match the given exif orientation.
*
* @param toOrient The bitmap to rotate/flip.
* @param pool A pool that may or may not contain an image of the necessary dimensions.
* @param exifOrientation the exif orientation [1-8].
* @return The rotated and/or flipped image or toOrient if no rotation or flip was necessary.
*/
public static Bitmap rotateImageExif(Bitmap toOrient, BitmapPool pool, int exifOrientation) {
if (exifOrientation == ExifInterface.ORIENTATION_NORMAL
|| exifOrientation == ExifInterface.ORIENTATION_UNDEFINED) {
return toOrient;
}
final Matrix matrix = new Matrix();
initializeMatrixForRotation(exifOrientation, matrix);
// From Bitmap.createBitmap.
final RectF newRect = new RectF(0, 0, toOrient.getWidth(), toOrient.getHeight());
matrix.mapRect(newRect);
final int newWidth = Math.round(newRect.width());
final int newHeight = Math.round(newRect.height());
Bitmap result = pool.get(newWidth, newHeight, toOrient.getConfig());
if (result == null) {
result = Bitmap.createBitmap(newWidth, newHeight, toOrient.getConfig());
}
matrix.postTranslate(-newRect.left, -newRect.top);
final Canvas canvas = new Canvas(result);
final Paint paint = new Paint(PAINT_FLAGS);
canvas.drawBitmap(toOrient, matrix, paint);
return result;
}
// Visible for testing.
static void initializeMatrixForRotation(int exifOrientation, Matrix matrix) {
switch (exifOrientation) {
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
matrix.setScale(-1, 1);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
matrix.setRotate(180);
break;
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
matrix.setRotate(180);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_TRANSPOSE:
matrix.setRotate(90);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_ROTATE_90:
matrix.setRotate(90);
break;
case ExifInterface.ORIENTATION_TRANSVERSE:
matrix.setRotate(-90);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_ROTATE_270:
matrix.setRotate(-90);
break;
default:
// Do nothing.
}
}
}