blob: 904a7c45b8539b5ec0d976f402f978cad9b3194e [file] [log] [blame]
package com.bumptech.glide.load.resource.bitmap;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.util.Log;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.util.ByteArrayPool;
import com.bumptech.glide.util.ExceptionCatchingInputStream;
import com.bumptech.glide.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.util.EnumSet;
import java.util.Queue;
import java.util.Set;
/**
* A base class with methods for loading and decoding images from InputStreams.
*/
public abstract class Downsampler implements BitmapDecoder<InputStream> {
private static final String TAG = "Downsampler";
private static final Set<ImageHeaderParser.ImageType> TYPES_THAT_USE_POOL = EnumSet.of(
ImageHeaderParser.ImageType.JPEG, ImageHeaderParser.ImageType.PNG_A, ImageHeaderParser.ImageType.PNG);
private static final Queue<BitmapFactory.Options> OPTIONS_QUEUE = Util.createQueue(0);
/**
* Load and scale the image uniformly (maintaining the image's aspect ratio) so that the dimensions of the image
* will be greater than or equal to the given width and height.
*/
public static final Downsampler AT_LEAST = new Downsampler() {
@Override
protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
return Math.min(inHeight / outHeight, inWidth / outWidth);
}
@Override
public String getId() {
return "AT_LEAST.com.bumptech.glide.load.data.bitmap";
}
};
/**
* Load and scale the image uniformly (maintaining the image's aspect ratio) so that the dimensions of the image
* will be less than or equal to the given width and height.
*
*/
public static final Downsampler AT_MOST = new Downsampler() {
@Override
protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
return Math.max(inHeight / outHeight, inWidth / outWidth);
}
@Override
public String getId() {
return "AT_MOST.com.bumptech.glide.load.data.bitmap";
}
};
/**
* Load the image at its original size.
*/
public static final Downsampler NONE = new Downsampler() {
@Override
protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
return 0;
}
@Override
public String getId() {
return "NONE.com.bumptech.glide.load.data.bitmap";
}
};
// 5MB. This is the max image header size we can handle, we preallocate a much smaller buffer but will resize up to
// this amount if necessary.
private static final int MARK_POSITION = 5 * 1024 * 1024;
/**
* Load the image for the given InputStream. If a recycled Bitmap whose dimensions exactly match those of the image
* for the given InputStream is available, the operation is much less expensive in terms of memory.
*
* <p>
* Note - this method will throw an exception of a Bitmap with dimensions not matching
* those of the image for the given InputStream is provided.
* </p>
*
* @param is An {@link InputStream} to the data for the image.
* @param pool A pool of recycled bitmaps.
* @param outWidth The width the final image should be close to.
* @param outHeight The height the final image should be close to.
* @return A new bitmap containing the image from the given InputStream, or recycle if recycle is not null.
*/
@SuppressWarnings("resource")
// see BitmapDecoder.decode
@Override
public Bitmap decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat) {
final ByteArrayPool byteArrayPool = ByteArrayPool.get();
final byte[] bytesForOptions = byteArrayPool.getBytes();
final byte[] bytesForStream = byteArrayPool.getBytes();
final BitmapFactory.Options options = getDefaultOptions();
// TODO(#126): when the framework handles exceptions better, consider removing.
final ExceptionCatchingInputStream stream =
ExceptionCatchingInputStream.obtain(new RecyclableBufferedInputStream(is, bytesForStream));
try {
stream.mark(MARK_POSITION);
int orientation = 0;
try {
orientation = new ImageHeaderParser(stream).getOrientation();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Cannot determine the image orientation from header", e);
}
} finally {
try {
stream.reset();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Cannot reset the input stream", e);
}
}
}
options.inTempStorage = bytesForOptions;
final int[] inDimens = getDimensions(stream, options);
final int inWidth = inDimens[0];
final int inHeight = inDimens[1];
final int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
final int sampleSize = getRoundedSampleSize(degreesToRotate, inWidth, inHeight, outWidth, outHeight);
final Bitmap downsampled =
downsampleWithSize(stream, options, pool, inWidth, inHeight, sampleSize,
decodeFormat);
// BitmapFactory swallows exceptions during decodes and in some cases when inBitmap is non null, may catch
// and log a stack trace but still return a non null bitmap. To avoid displaying partially decoded bitmaps,
// we catch exceptions reading from the stream in our ExceptionCatchingInputStream and throw them here.
final Exception streamException = stream.getException();
if (streamException != null) {
throw new RuntimeException(streamException);
}
Bitmap rotated = null;
if (downsampled != null) {
rotated = TransformationUtils.rotateImageExif(downsampled, pool, orientation);
if (!downsampled.equals(rotated) && !pool.put(downsampled)) {
downsampled.recycle();
}
}
return rotated;
} finally {
byteArrayPool.releaseBytes(bytesForOptions);
byteArrayPool.releaseBytes(bytesForStream);
stream.release();
releaseOptions(options);
}
}
private int getRoundedSampleSize(int degreesToRotate, int inWidth, int inHeight, int outWidth, int outHeight) {
final int exactSampleSize;
if (degreesToRotate == 90 || degreesToRotate == 270) {
// If we're rotating the image +-90 degrees, we need to downsample accordingly so the image width is
// decreased to near our target's height and the image height is decreased to near our target width.
exactSampleSize = getSampleSize(inHeight, inWidth, outWidth, outHeight);
} else {
exactSampleSize = getSampleSize(inWidth, inHeight, outWidth, outHeight);
}
// BitmapFactory only accepts powers of 2, so it will round down to the nearest power of two that is less than
// or equal to the sample size we provide. Because we need to estimate the final image width and height to
// re-use Bitmaps, we mirror BitmapFactory's calculation here. For bug, see issue #224. For algorithm see
// http://stackoverflow.com/a/17379704/800716.
final int powerOfTwoSampleSize = exactSampleSize == 0 ? 0 : Integer.highestOneBit(exactSampleSize - 1);
// Although functionally equivalent to 0 for BitmapFactory, 1 is a safer default for our code than 0.
return Math.max(1, powerOfTwoSampleSize);
}
private Bitmap downsampleWithSize(ExceptionCatchingInputStream is, BitmapFactory.Options options, BitmapPool pool,
int inWidth, int inHeight, int sampleSize, DecodeFormat decodeFormat) {
// Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding.
Bitmap.Config config = getConfig(is, decodeFormat);
options.inSampleSize = sampleSize;
options.inPreferredConfig = config;
if ((options.inSampleSize == 1 || Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT) && shouldUsePool(is)) {
int targetWidth = (int) Math.ceil(inWidth / (double) sampleSize);
int targetHeight = (int) Math.ceil(inHeight / (double) sampleSize);
// BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
setInBitmap(options, pool.getDirty(targetWidth, targetHeight, config));
}
return decodeStream(is, options);
}
private static boolean shouldUsePool(InputStream is) {
// On KitKat+, any bitmap can be used to decode any other bitmap.
if (Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT) {
return true;
}
is.mark(1024);
try {
final ImageHeaderParser.ImageType type = new ImageHeaderParser(is).getType();
// cannot reuse bitmaps when decoding images that are not PNG or JPG.
// look at : https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ
return TYPES_THAT_USE_POOL.contains(type);
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Cannot determine the image type from header", e);
}
} finally {
try {
is.reset();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Cannot reset the input stream", e);
}
}
}
return false;
}
private static Bitmap.Config getConfig(InputStream is, DecodeFormat format) {
// Changing configs can cause skewing on 4.1, see issue #128.
if (format == DecodeFormat.ALWAYS_ARGB_8888 || Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
return Bitmap.Config.ARGB_8888;
}
boolean hasAlpha = false;
// We probably only need 25, but this is safer (particularly since the buffer size is > 1024).
is.mark(1024);
try {
hasAlpha = new ImageHeaderParser(is).hasAlpha();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Cannot determine whether the image has alpha or not from header for format " + format, e);
}
} finally {
try {
is.reset();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Cannot reset the input stream", e);
}
}
}
return hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
}
/**
* Determine the amount of downsampling to use for a load given the dimensions of the image to be downsampled and
* the dimensions of the view/target the image will be displayed in.
*
* @see android.graphics.BitmapFactory.Options#inSampleSize
*
* @param inWidth The width of the image to be downsampled.
* @param inHeight The height of the image to be downsampled.
* @param outWidth The width of the view/target the image will be displayed in.
* @param outHeight The height of the view/target the imag will be displayed in.
* @return An integer to pass in to {@link BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect,
* android.graphics.BitmapFactory.Options)}.
*/
protected abstract int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight);
/**
* A method for getting the dimensions of an image from the given InputStream.
*
* @param is The InputStream representing the image.
* @param options The options to pass to
* {@link BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect,
* android.graphics.BitmapFactory.Options)}.
* @return an array containing the dimensions of the image in the form {width, height}.
*/
public int[] getDimensions(ExceptionCatchingInputStream is, BitmapFactory.Options options) {
options.inJustDecodeBounds = true;
decodeStream(is, options);
options.inJustDecodeBounds = false;
return new int[] { options.outWidth, options.outHeight };
}
private static Bitmap decodeStream(ExceptionCatchingInputStream is, BitmapFactory.Options options) {
if (options.inJustDecodeBounds) {
// This is large, but jpeg headers are not size bounded so we need something large enough to minimize
// the possibility of not being able to fit enough of the header in the buffer to get the image size so
// that we don't fail to load images. The BufferedInputStream will create a new buffer of 2x the
// original size each time we use up the buffer space without passing the mark so this is a maximum
// bound on the buffer size, not a default. Most of the time we won't go past our pre-allocated 16kb.
is.mark(MARK_POSITION);
} else {
// Once we've read the image header, we no longer need to allow the buffer to expand in size. To avoid
// unnecessary allocations reading image data, we fix the mark limit so that it is no larger than our
// current buffer size here. See issue #225.
is.fixMarkLimit();
}
final Bitmap result = BitmapFactory.decodeStream(is, null, options);
try {
if (options.inJustDecodeBounds) {
is.reset();
}
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.ERROR)) {
Log.e(TAG, "Exception loading inDecodeBounds=" + options.inJustDecodeBounds
+ " sample=" + options.inSampleSize, e);
}
}
return result;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void setInBitmap(BitmapFactory.Options options, Bitmap recycled) {
if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) {
options.inBitmap = recycled;
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static synchronized BitmapFactory.Options getDefaultOptions() {
BitmapFactory.Options decodeBitmapOptions;
synchronized (OPTIONS_QUEUE) {
decodeBitmapOptions = OPTIONS_QUEUE.poll();
}
if (decodeBitmapOptions == null) {
decodeBitmapOptions = new BitmapFactory.Options();
resetOptions(decodeBitmapOptions);
}
return decodeBitmapOptions;
}
private static void releaseOptions(BitmapFactory.Options decodeBitmapOptions) {
resetOptions(decodeBitmapOptions);
synchronized (OPTIONS_QUEUE) {
OPTIONS_QUEUE.offer(decodeBitmapOptions);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void resetOptions(BitmapFactory.Options decodeBitmapOptions) {
decodeBitmapOptions.inTempStorage = null;
decodeBitmapOptions.inDither = false;
decodeBitmapOptions.inScaled = false;
decodeBitmapOptions.inSampleSize = 1;
decodeBitmapOptions.inPreferredConfig = null;
decodeBitmapOptions.inJustDecodeBounds = false;
if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) {
decodeBitmapOptions.inBitmap = null;
decodeBitmapOptions.inMutable = true;
}
}
}