package com.android.volley.misc;
import java.io.ByteArrayInputStream;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Pattern;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
public class ImageUtils {
private static final String TAG = "ImageUtils";
/** Minimum class memory class to use full-res photos */
@SuppressWarnings("unused")
private final static long MIN_NORMAL_CLASS = 32;
/** Minimum class memory class to use small photos */
@SuppressWarnings("unused")
private final static long MIN_SMALL_CLASS = 24;
private static final String BASE64_URI_PREFIX = "base64,";
private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*");
/**
* Returns the largest power-of-two divisor for use in downscaling a bitmap
* that will not result in the scaling past the desired dimensions.
*
* @param actualWidth Actual width of the bitmap
* @param actualHeight Actual height of the bitmap
* @param desiredWidth Desired width of the bitmap
* @param desiredHeight Desired height of the bitmap
*/
// Visible for testing.
public static int findBestSampleSize(
int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
double wr = (double) actualWidth / desiredWidth;
double hr = (double) actualHeight / desiredHeight;
double ratio = Math.min(wr, hr);
float n = 1.0f;
while ((n * 2) <= ratio) {
n *= 2;
}
return (int) n;
}
/**
* Decode and sample down a bitmap from a file input stream to the requested width and height.
*
* @param fileDescriptor The file descriptor to read from
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static Bitmap decodeSampledBitmapFromDescriptor(
FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
}
/**
* Decode and sample down a bitmap from a file input stream to the requested width and height.
*
* @param fileDescriptor The file descriptor to read from
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static Bitmap decodeSampledBitmapFromDescriptor(FileDescriptor fileDescriptor) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
}
/**
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
* the closest inSampleSize that is a power of 2 and will result in the final decoded bitmap
* having a width and height equal to or larger than the requested width and height.
*
* @param options An options object with out* params already populated (run through a decode*
* method with inJustDecodeBounds==true
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return The value to be used for inSampleSize
*/
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// Raw height and width of image
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;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
// This offers some additional logic in case the image has a strange
// aspect ratio. For example, a panorama may have a much larger
// width than height. In these cases the total pixels might still
// end up being too large to fit comfortably in memory, so we should
// be more aggressive with sample down the image (=larger inSampleSize).
long totalPixels = width * height / inSampleSize;
// Anything more than 2x the requested pixels we'll sample down further
final long totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels > totalReqPixelsCap) {
inSampleSize *= 2;
totalPixels /= 2;
}
}
return inSampleSize;
}
/**
* @return true if the MimeType type is image
*/
public static boolean isImageMimeType(String mimeType) {
return mimeType != null && mimeType.startsWith("image/");
}
/**
* Create a bitmap from a local URI
*
* @param resolver The ContentResolver
* @param uri The local URI
* @param maxSize The maximum size (either width or height)
* @return The new bitmap or null
*/
public static Bitmap decodeStream(final ContentResolver resolver, final Uri uri,
final int maxSize) {
Bitmap result = null;
final InputStreamFactory factory = createInputStreamFactory(resolver, uri);
try {
final Point bounds = getImageBounds(factory);
if (bounds == null) {
return result;
}
final BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
result = decodeStream(factory, null, opts);
return result;
} catch (FileNotFoundException exception) {
// Do nothing - the photo will appear to be missing
} catch (IOException exception) {
} catch (IllegalArgumentException exception) {
// Do nothing - the photo will appear to be missing
} catch (SecurityException exception) {
}
return result;
}
/**
* Create a bitmap from a local URI
*
* @param resolver The ContentResolver
* @param uri The local URI
* @param maxSize The maximum size (either width or height)
* @return The new bitmap or null
*/
public static Bitmap decodeStream(final ContentResolver resolver, final Uri uri,
BitmapFactory.Options opts) {
Bitmap result = null;
final InputStreamFactory factory = createInputStreamFactory(resolver, uri);
try {
result = decodeStream(factory, null, opts);
return result;
} catch (FileNotFoundException exception) {
// Do nothing - the photo will appear to be missing
} catch (IllegalArgumentException exception) {
// Do nothing - the photo will appear to be missing
} catch (SecurityException exception) {
}
return result;
}
/**
* Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
* BitmapFactory.Options)} that returns {@code null} on {@link
* OutOfMemoryError}.
*
* @param is The input stream that holds the raw data to be decoded into a
* bitmap.
* @param outPadding If not null, return the padding rect for the bitmap if
* it exists, otherwise set padding to [-1,-1,-1,-1]. If
* no bitmap is returned (null) then padding is
* unchanged.
* @param opts null-ok; Options that control downsampling and whether the
* image should be completely decoded, or just is size returned.
* @return The decoded bitmap, or null if the image data could not be
* decoded, or, if opts is non-null, if opts requested only the
* size be returned (in opts.outWidth and opts.outHeight)
*/
public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
try {
return BitmapFactory.decodeStream(is, outPadding, opts);
} catch (OutOfMemoryError oome) {
Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
return null;
}
}
/**
* Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
* BitmapFactory.Options)} that returns {@code null} on {@link
* OutOfMemoryError}.
*
* @param factory Used to create input streams that holds the raw data to be decoded into a
* bitmap.
* @param outPadding If not null, return the padding rect for the bitmap if
* it exists, otherwise set padding to [-1,-1,-1,-1]. If
* no bitmap is returned (null) then padding is
* unchanged.
* @param opts null-ok; Options that control downsampling and whether the
* image should be completely decoded, or just is size returned.
* @return The decoded bitmap, or null if the image data could not be
* decoded, or, if opts is non-null, if opts requested only the
* size be returned (in opts.outWidth and opts.outHeight)
*/
public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding,
final BitmapFactory.Options opts) throws FileNotFoundException {
InputStream is = null;
try {
// Determine the orientation for this image
is = factory.createInputStream();
final int orientation = Exif.getOrientation(is, -1);
is.close();
// Decode the bitmap
is = factory.createInputStream();
final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts);
if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) {
Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): "
+ "Image bytes cannot be decoded into a Bitmap");
throw new UnsupportedOperationException(
"Image bytes cannot be decoded into a Bitmap.");
}
// Rotate the Bitmap based on the orientation
if (originalBitmap != null && orientation != 0) {
final Matrix matrix = new Matrix();
matrix.postRotate(orientation);
return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
originalBitmap.getHeight(), matrix, true);
}
return originalBitmap;
} catch (OutOfMemoryError oome) {
Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
return null;
} catch (IOException ioe) {
Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
return null;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Do nothing
}
}
}
}
/**
* Gets the image bounds
*
* @param factory Used to create the InputStream.
*
* @return The image bounds
*/
public static Point getImageBounds(final InputStreamFactory factory)
throws IOException {
final BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
decodeStream(factory, null, opts);
return new Point(opts.outWidth, opts.outHeight);
}
public static InputStreamFactory createInputStreamFactory(final ContentResolver resolver,
final Uri uri) {
final String scheme = uri.getScheme();
if ("data".equals(scheme)) {
return new DataInputStreamFactory(resolver, uri);
}
return new BaseInputStreamFactory(resolver, uri);
}
/**
* Utility class for when an InputStream needs to be read multiple times. For example, one pass
* may load EXIF orientation, and the second pass may do the actual Bitmap decode.
*/
public interface InputStreamFactory {
/**
* Create a new InputStream. The caller of this method must be able to read the input
* stream starting from the beginning.
* @return
*/
InputStream createInputStream() throws FileNotFoundException;
}
private static class BaseInputStreamFactory implements InputStreamFactory {
protected final ContentResolver mResolver;
protected final Uri mUri;
public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) {
mResolver = resolver;
mUri = uri;
}
@Override
public InputStream createInputStream() throws FileNotFoundException {
return mResolver.openInputStream(mUri);
}
}
private static class DataInputStreamFactory extends BaseInputStreamFactory {
private byte[] mData;
public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) {
super(resolver, uri);
}
@Override
public InputStream createInputStream() throws FileNotFoundException {
if (mData == null) {
mData = parseDataUri(mUri);
if (mData == null) {
return super.createInputStream();
}
}
return new ByteArrayInputStream(mData);
}
private byte[] parseDataUri(final Uri uri) {
final String ssp = uri.getSchemeSpecificPart();
try {
if (ssp.startsWith(BASE64_URI_PREFIX)) {
final String base64 = ssp.substring(BASE64_URI_PREFIX.length());
return Base64.decode(base64, Base64.URL_SAFE);
} else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){
final String base64 = ssp.substring(
ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length());
return Base64.decode(base64, Base64.DEFAULT);
} else {
return null;
}
} catch (IllegalArgumentException ex) {
Log.e(TAG, "Mailformed data URI: " + ex);
return null;
}
}
}
}