
com.bumptech.glide.load.resource.bitmap.Downsampler Maven / Gradle / Ivy
package com.bumptech.glide.load.resource.bitmap;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.ColorSpace;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.DisplayMetrics;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
import com.bumptech.glide.load.Option;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.PreferredColorSpace;
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.SampleSizeRounding;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.util.LogTime;
import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
/**
* Downsamples, decodes, and rotates images according to their exif orientation using {@link
* BitmapFactory}.
*/
public final class Downsampler {
static final String TAG = "Downsampler";
/**
* Indicates the {@link com.bumptech.glide.load.DecodeFormat} that will be used in conjunction
* with the image format to determine the {@link android.graphics.Bitmap.Config} to provide to
* {@link android.graphics.BitmapFactory.Options#inPreferredConfig} when decoding the image.
*/
public static final Option DECODE_FORMAT =
Option.memory(
"com.bumptech.glide.load.resource.bitmap.Downsampler.DecodeFormat", DecodeFormat.DEFAULT);
/**
* Sets the {@link PreferredColorSpace} that will be used along with the version of Android and
* color space of the requested image to determine the final color space used to decode the image.
*
* Refer to {@link PreferredColorSpace} for details on how this option works and its various
* limitations.
*/
public static final Option PREFERRED_COLOR_SPACE =
Option.memory("com.bumptech.glide.load.resource.bitmap.Downsampler.PreferredColorSpace");
/**
* Indicates the {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option that
* will be used to calculate the sample size to use to downsample an image given the original and
* target dimensions of the image.
*
* @deprecated Use {@link DownsampleStrategy#OPTION} directly instead.
*/
@Deprecated
public static final Option DOWNSAMPLE_STRATEGY = DownsampleStrategy.OPTION;
/**
* Ensure that the size of the bitmap is fixed to the requested width and height of the resource
* from the caller. The final resource dimensions may differ from the requested width and height,
* and thus setting this to true may result in the bitmap size differing from the resource
* dimensions.
*
* This can be used as a performance optimization for KitKat and above by fixing the size of
* the bitmap for a collection of requested resources so that the bitmap pool will not need to
* allocate new bitmaps for images of different sizes.
*/
// Public API
@SuppressWarnings("WeakerAccess")
public static final Option FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS =
Option.memory("com.bumptech.glide.load.resource.bitmap.Downsampler.FixBitmapSize", false);
/**
* Indicates that it's safe or unsafe to decode {@link Bitmap}s with {@link
* Bitmap.Config#HARDWARE}.
*
* Callers should almost never set this value to {@code true} manually. Glide will already do
* so when Glide believes it's safe to do (when no transformations are applied). Instead, callers
* can set this value to {@code false} to prevent Glide from decoding hardware bitmaps if Glide is
* unable to detect that hardware bitmaps are unsafe. For example, you should set this to {@code
* false} if you plan to draw it to a software {@link android.graphics.Canvas} or if you plan to
* inspect the {@link Bitmap}s pixels with {@link Bitmap#getPixel(int, int)} or {@link
* Bitmap#getPixels(int[], int, int, int, int, int, int)}.
*
*
Callers can disable hardware {@link Bitmap}s for all loads using {@link
* com.bumptech.glide.GlideBuilder#setDefaultRequestOptions(RequestOptions)}.
*
*
This option is ignored unless we're on Android O+.
*/
public static final Option ALLOW_HARDWARE_CONFIG =
Option.memory(
"com.bumptech.glide.load.resource.bitmap.Downsampler.AllowHardwareDecode", false);
private static final String WBMP_MIME_TYPE = "image/vnd.wap.wbmp";
private static final String ICO_MIME_TYPE = "image/x-ico";
private static final Set NO_DOWNSAMPLE_PRE_N_MIME_TYPES =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(WBMP_MIME_TYPE, ICO_MIME_TYPE)));
private static final DecodeCallbacks EMPTY_CALLBACKS =
new DecodeCallbacks() {
@Override
public void onObtainBounds() {
// Do nothing.
}
@Override
public void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) {
// Do nothing.
}
};
private static final Set TYPES_THAT_USE_POOL_PRE_KITKAT =
Collections.unmodifiableSet(
EnumSet.of(
ImageHeaderParser.ImageType.JPEG,
ImageHeaderParser.ImageType.PNG_A,
ImageHeaderParser.ImageType.PNG));
private static final Queue OPTIONS_QUEUE = Util.createQueue(0);
private final BitmapPool bitmapPool;
private final DisplayMetrics displayMetrics;
private final ArrayPool byteArrayPool;
private final List parsers;
private final HardwareConfigState hardwareConfigState = HardwareConfigState.getInstance();
public Downsampler(
List parsers,
DisplayMetrics displayMetrics,
BitmapPool bitmapPool,
ArrayPool byteArrayPool) {
this.parsers = parsers;
this.displayMetrics = Preconditions.checkNotNull(displayMetrics);
this.bitmapPool = Preconditions.checkNotNull(bitmapPool);
this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool);
}
public boolean handles(@SuppressWarnings("unused") InputStream is) {
// We expect Downsampler to handle any available type Android supports.
return true;
}
public boolean handles(@SuppressWarnings("unused") ByteBuffer byteBuffer) {
// We expect downsampler to handle any available type Android supports.
return true;
}
public boolean handles(@SuppressWarnings("unused") ParcelFileDescriptor source) {
return ParcelFileDescriptorRewinder.isSupported();
}
/**
* Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF
* data present in the stream and that is downsampled according to the given dimensions and any
* provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option.
*
* @see #decode(InputStream, int, int, Options, DecodeCallbacks)
*/
public Resource decode(InputStream is, int outWidth, int outHeight, Options options)
throws IOException {
return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS);
}
/**
* Identical to {@link #decode(InputStream, int, int, Options)}, except that it accepts a {@link
* ByteBuffer} in place of an {@link InputStream}.
*/
public Resource decode(
ByteBuffer buffer, int requestedWidth, int requestedHeight, Options options)
throws IOException {
return decode(
new ImageReader.ByteBufferReader(buffer, parsers, byteArrayPool),
requestedWidth,
requestedHeight,
options,
EMPTY_CALLBACKS);
}
/**
* Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF
* data present in the stream and that is downsampled according to the given dimensions and any
* provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option.
*
* If a Bitmap is present in the {@link
* com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} whose dimensions exactly match those
* of the image for the given InputStream is available, the operation is much less expensive in
* terms of memory.
*
* @param is An {@link InputStream} to the data for the image.
* @param requestedWidth The width the final image should be close to.
* @param requestedHeight The height the final image should be close to.
* @param options A set of options that may contain one or more supported options that influence
* how a Bitmap will be decoded from the given stream.
* @param callbacks A set of callbacks allowing callers to optionally respond to various
* significant events during the decode process.
* @return A new bitmap containing the image from the given InputStream, or recycle if recycle is
* not null.
*/
public Resource decode(
InputStream is,
int requestedWidth,
int requestedHeight,
Options options,
DecodeCallbacks callbacks)
throws IOException {
return decode(
new ImageReader.InputStreamImageReader(is, parsers, byteArrayPool),
requestedWidth,
requestedHeight,
options,
callbacks);
}
@VisibleForTesting
void decode(byte[] bytes, int requestedWidth, int requestedHeight, Options options)
throws IOException {
decode(
new ImageReader.ByteArrayReader(bytes, parsers, byteArrayPool),
requestedWidth,
requestedHeight,
options,
EMPTY_CALLBACKS);
}
@VisibleForTesting
void decode(File file, int requestedWidth, int requestedHeight, Options options)
throws IOException {
decode(
new ImageReader.FileReader(file, parsers, byteArrayPool),
requestedWidth,
requestedHeight,
options,
EMPTY_CALLBACKS);
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public Resource decode(
ParcelFileDescriptor parcelFileDescriptor, int outWidth, int outHeight, Options options)
throws IOException {
return decode(
new ImageReader.ParcelFileDescriptorImageReader(
parcelFileDescriptor, parsers, byteArrayPool),
outWidth,
outHeight,
options,
EMPTY_CALLBACKS);
}
private Resource decode(
ImageReader imageReader,
int requestedWidth,
int requestedHeight,
Options options,
DecodeCallbacks callbacks)
throws IOException {
byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
bitmapFactoryOptions.inTempStorage = bytesForOptions;
DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
PreferredColorSpace preferredColorSpace = options.get(PREFERRED_COLOR_SPACE);
DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
boolean isHardwareConfigAllowed =
options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG);
try {
Bitmap result =
decodeFromWrappedStreams(
imageReader,
bitmapFactoryOptions,
downsampleStrategy,
decodeFormat,
preferredColorSpace,
isHardwareConfigAllowed,
requestedWidth,
requestedHeight,
fixBitmapToRequestedDimensions,
callbacks);
return BitmapResource.obtain(result, bitmapPool);
} finally {
releaseOptions(bitmapFactoryOptions);
byteArrayPool.put(bytesForOptions);
}
}
private Bitmap decodeFromWrappedStreams(
ImageReader imageReader,
BitmapFactory.Options options,
DownsampleStrategy downsampleStrategy,
DecodeFormat decodeFormat,
PreferredColorSpace preferredColorSpace,
boolean isHardwareConfigAllowed,
int requestedWidth,
int requestedHeight,
boolean fixBitmapToRequestedDimensions,
DecodeCallbacks callbacks)
throws IOException {
long startTime = LogTime.getLogTime();
int[] sourceDimensions = getDimensions(imageReader, options, callbacks, bitmapPool);
int sourceWidth = sourceDimensions[0];
int sourceHeight = sourceDimensions[1];
String sourceMimeType = options.outMimeType;
// If we failed to obtain the image dimensions, we may end up with an incorrectly sized Bitmap,
// so we want to use a mutable Bitmap type. One way this can happen is if the image header is so
// large (10mb+) that our attempt to use inJustDecodeBounds fails and we're forced to decode the
// full size image.
if (sourceWidth == -1 || sourceHeight == -1) {
isHardwareConfigAllowed = false;
}
int orientation = imageReader.getImageOrientation();
int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);
int targetWidth =
requestedWidth == Target.SIZE_ORIGINAL
? (isRotationRequired(degreesToRotate) ? sourceHeight : sourceWidth)
: requestedWidth;
int targetHeight =
requestedHeight == Target.SIZE_ORIGINAL
? (isRotationRequired(degreesToRotate) ? sourceWidth : sourceHeight)
: requestedHeight;
ImageType imageType = imageReader.getImageType();
calculateScaling(
imageType,
imageReader,
callbacks,
bitmapPool,
downsampleStrategy,
degreesToRotate,
sourceWidth,
sourceHeight,
targetWidth,
targetHeight,
options);
calculateConfig(
imageReader,
decodeFormat,
isHardwareConfigAllowed,
isExifOrientationRequired,
options,
targetWidth,
targetHeight);
boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding.
if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
int expectedWidth;
int expectedHeight;
if (sourceWidth >= 0
&& sourceHeight >= 0
&& fixBitmapToRequestedDimensions
&& isKitKatOrGreater) {
expectedWidth = targetWidth;
expectedHeight = targetHeight;
} else {
float densityMultiplier =
isScaling(options) ? (float) options.inTargetDensity / options.inDensity : 1f;
int sampleSize = options.inSampleSize;
int downsampledWidth = (int) Math.ceil(sourceWidth / (float) sampleSize);
int downsampledHeight = (int) Math.ceil(sourceHeight / (float) sampleSize);
expectedWidth = Math.round(downsampledWidth * densityMultiplier);
expectedHeight = Math.round(downsampledHeight * densityMultiplier);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(
TAG,
"Calculated target ["
+ expectedWidth
+ "x"
+ expectedHeight
+ "] for source"
+ " ["
+ sourceWidth
+ "x"
+ sourceHeight
+ "]"
+ ", sampleSize: "
+ sampleSize
+ ", targetDensity: "
+ options.inTargetDensity
+ ", density: "
+ options.inDensity
+ ", density multiplier: "
+ densityMultiplier);
}
}
// If this isn't an image, or BitmapFactory was unable to parse the size, width and height
// will be -1 here.
if (expectedWidth > 0 && expectedHeight > 0) {
setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);
}
}
if (preferredColorSpace != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
boolean isP3Eligible =
preferredColorSpace == PreferredColorSpace.DISPLAY_P3
&& options.outColorSpace != null
&& options.outColorSpace.isWideGamut();
options.inPreferredColorSpace =
ColorSpace.get(isP3Eligible ? ColorSpace.Named.DISPLAY_P3 : ColorSpace.Named.SRGB);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
}
}
Bitmap downsampled = decodeStream(imageReader, options, callbacks, bitmapPool);
callbacks.onDecodeComplete(bitmapPool, downsampled);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logDecode(
sourceWidth,
sourceHeight,
sourceMimeType,
options,
downsampled,
requestedWidth,
requestedHeight,
startTime);
}
Bitmap rotated = null;
if (downsampled != null) {
// If we scaled, the Bitmap density will be our inTargetDensity. Here we correct it back to
// the expected density dpi.
downsampled.setDensity(displayMetrics.densityDpi);
rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation);
if (!downsampled.equals(rotated)) {
bitmapPool.put(downsampled);
}
}
return rotated;
}
private static void calculateScaling(
ImageType imageType,
ImageReader imageReader,
DecodeCallbacks decodeCallbacks,
BitmapPool bitmapPool,
DownsampleStrategy downsampleStrategy,
int degreesToRotate,
int sourceWidth,
int sourceHeight,
int targetWidth,
int targetHeight,
BitmapFactory.Options options)
throws IOException {
// We can't downsample source content if we can't determine its dimensions.
if (sourceWidth <= 0 || sourceHeight <= 0) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(
TAG,
"Unable to determine dimensions for: "
+ imageType
+ " with target ["
+ targetWidth
+ "x"
+ targetHeight
+ "]");
}
return;
}
int orientedSourceWidth = sourceWidth;
int orientedSourceHeight = sourceHeight;
// 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.
//noinspection SuspiciousNameCombination
if (isRotationRequired(degreesToRotate)) {
orientedSourceWidth = sourceHeight;
orientedSourceHeight = sourceWidth;
}
final float exactScaleFactor =
downsampleStrategy.getScaleFactor(
orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);
if (exactScaleFactor <= 0f) {
throw new IllegalArgumentException(
"Cannot scale with factor: "
+ exactScaleFactor
+ " from: "
+ downsampleStrategy
+ ", source: ["
+ sourceWidth
+ "x"
+ sourceHeight
+ "]"
+ ", target: ["
+ targetWidth
+ "x"
+ targetHeight
+ "]");
}
SampleSizeRounding rounding =
downsampleStrategy.getSampleSizeRounding(
orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);
if (rounding == null) {
throw new IllegalArgumentException("Cannot round with null rounding");
}
int outWidth = round(exactScaleFactor * orientedSourceWidth);
int outHeight = round(exactScaleFactor * orientedSourceHeight);
int widthScaleFactor = orientedSourceWidth / outWidth;
int heightScaleFactor = orientedSourceHeight / outHeight;
// TODO: This isn't really right for both CenterOutside and CenterInside. Consider allowing
// DownsampleStrategy to pick, or trying to do something more sophisticated like picking the
// scale factor that leads to an exact match.
int scaleFactor =
rounding == SampleSizeRounding.MEMORY
? Math.max(widthScaleFactor, heightScaleFactor)
: Math.min(widthScaleFactor, heightScaleFactor);
int powerOfTwoSampleSize;
// BitmapFactory does not support downsampling wbmp files on platforms <= M. See b/27305903.
if (Build.VERSION.SDK_INT <= 23
&& NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) {
powerOfTwoSampleSize = 1;
} else {
powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor));
if (rounding == SampleSizeRounding.MEMORY
&& powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
powerOfTwoSampleSize = powerOfTwoSampleSize << 1;
}
}
// Here we mimic framework logic for determining how inSampleSize division is rounded on various
// versions of Android. The logic here has been tested on emulators for Android versions 15-26.
// PNG - Always uses floor
// JPEG - Always uses ceiling
// Webp - Prior to N, always uses floor. At and after N, always uses round.
options.inSampleSize = powerOfTwoSampleSize;
int powerOfTwoWidth;
int powerOfTwoHeight;
if (imageType == ImageType.JPEG) {
// libjpegturbo can downsample up to a sample size of 8. libjpegturbo uses ceiling to round.
// After libjpegturbo's native rounding, skia does a secondary scale using floor
// (integer division). Here we replicate that logic.
int nativeScaling = Math.min(powerOfTwoSampleSize, 8);
powerOfTwoWidth = (int) Math.ceil(orientedSourceWidth / (float) nativeScaling);
powerOfTwoHeight = (int) Math.ceil(orientedSourceHeight / (float) nativeScaling);
int secondaryScaling = powerOfTwoSampleSize / 8;
if (secondaryScaling > 0) {
powerOfTwoWidth = powerOfTwoWidth / secondaryScaling;
powerOfTwoHeight = powerOfTwoHeight / secondaryScaling;
}
} else if (imageType == ImageType.PNG || imageType == ImageType.PNG_A) {
powerOfTwoWidth = (int) Math.floor(orientedSourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = (int) Math.floor(orientedSourceHeight / (float) powerOfTwoSampleSize);
} else if (imageType.isWebp()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
powerOfTwoWidth = Math.round(orientedSourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = Math.round(orientedSourceHeight / (float) powerOfTwoSampleSize);
} else {
powerOfTwoWidth = (int) Math.floor(orientedSourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = (int) Math.floor(orientedSourceHeight / (float) powerOfTwoSampleSize);
}
} else if (orientedSourceWidth % powerOfTwoSampleSize != 0
|| orientedSourceHeight % powerOfTwoSampleSize != 0) {
// If we're not confident the image is in one of our types, fall back to checking the
// dimensions again. inJustDecodeBounds decodes do obey inSampleSize.
int[] dimensions = getDimensions(imageReader, options, decodeCallbacks, bitmapPool);
// Power of two downsampling in BitmapFactory uses a variety of random factors to determine
// rounding that we can't reliably replicate for all image formats. Use ceiling here to make
// sure that we at least provide a Bitmap that's large enough to fit the content we're going
// to load.
powerOfTwoWidth = dimensions[0];
powerOfTwoHeight = dimensions[1];
} else {
powerOfTwoWidth = orientedSourceWidth / powerOfTwoSampleSize;
powerOfTwoHeight = orientedSourceHeight / powerOfTwoSampleSize;
}
double adjustedScaleFactor =
downsampleStrategy.getScaleFactor(
powerOfTwoWidth, powerOfTwoHeight, targetWidth, targetHeight);
// Density scaling is only supported if inBitmap is null prior to KitKat. Avoid setting
// densities here so we calculate the final Bitmap size correctly.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor);
options.inDensity = getDensityMultiplier(adjustedScaleFactor);
}
if (isScaling(options)) {
options.inScaled = true;
} else {
options.inDensity = options.inTargetDensity = 0;
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(
TAG,
"Calculate scaling"
+ ", source: ["
+ sourceWidth
+ "x"
+ sourceHeight
+ "]"
+ ", degreesToRotate: "
+ degreesToRotate
+ ", target: ["
+ targetWidth
+ "x"
+ targetHeight
+ "]"
+ ", power of two scaled: ["
+ powerOfTwoWidth
+ "x"
+ powerOfTwoHeight
+ "]"
+ ", exact scale factor: "
+ exactScaleFactor
+ ", power of 2 sample size: "
+ powerOfTwoSampleSize
+ ", adjusted scale factor: "
+ adjustedScaleFactor
+ ", target density: "
+ options.inTargetDensity
+ ", density: "
+ options.inDensity);
}
}
/**
* BitmapFactory calculates the density scale factor as a float. This introduces some non-trivial
* error. This method attempts to account for that error by adjusting the inTargetDensity so that
* the final scale factor is as close to our target as possible.
*/
private static int adjustTargetDensityForError(double adjustedScaleFactor) {
int densityMultiplier = getDensityMultiplier(adjustedScaleFactor);
int targetDensity = round(densityMultiplier * adjustedScaleFactor);
float scaleFactorWithError = targetDensity / (float) densityMultiplier;
double difference = adjustedScaleFactor / scaleFactorWithError;
return round(difference * targetDensity);
}
private static int getDensityMultiplier(double adjustedScaleFactor) {
return (int)
Math.round(
Integer.MAX_VALUE
* (adjustedScaleFactor <= 1D ? adjustedScaleFactor : 1 / adjustedScaleFactor));
}
// This is weird, but it matches the logic in a bunch of Android views/framework classes for
// rounding.
private static int round(double value) {
return (int) (value + 0.5d);
}
private boolean shouldUsePool(ImageType imageType) {
// On KitKat+, any bitmap (of a given config) can be used to decode any other bitmap
// (with the same config).
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return true;
}
// We cannot reuse bitmaps when decoding images that are not PNG or JPG prior to KitKat.
// See: https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ
return TYPES_THAT_USE_POOL_PRE_KITKAT.contains(imageType);
}
@SuppressWarnings("deprecation")
private void calculateConfig(
ImageReader imageReader,
DecodeFormat format,
boolean isHardwareConfigAllowed,
boolean isExifOrientationRequired,
BitmapFactory.Options optionsWithScaling,
int targetWidth,
int targetHeight) {
if (hardwareConfigState.setHardwareConfigIfAllowed(
targetWidth,
targetHeight,
optionsWithScaling,
isHardwareConfigAllowed,
isExifOrientationRequired)) {
return;
}
// Changing configs can cause skewing on 4.1, see issue #128.
if (format == DecodeFormat.PREFER_ARGB_8888
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
optionsWithScaling.inPreferredConfig = Bitmap.Config.ARGB_8888;
return;
}
boolean hasAlpha = false;
try {
hasAlpha = imageReader.getImageType().hasAlpha();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(
TAG,
"Cannot determine whether the image has alpha or not from header"
+ ", format "
+ format,
e);
}
}
optionsWithScaling.inPreferredConfig =
hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
if (optionsWithScaling.inPreferredConfig == Config.RGB_565) {
optionsWithScaling.inDither = true;
}
}
/**
* A method for getting the dimensions of an image from the given InputStream.
*
* @param imageReader The {@link ImageReader} 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}.
*/
private static int[] getDimensions(
ImageReader imageReader,
BitmapFactory.Options options,
DecodeCallbacks decodeCallbacks,
BitmapPool bitmapPool)
throws IOException {
options.inJustDecodeBounds = true;
decodeStream(imageReader, options, decodeCallbacks, bitmapPool);
options.inJustDecodeBounds = false;
return new int[] {options.outWidth, options.outHeight};
}
private static Bitmap decodeStream(
ImageReader imageReader,
BitmapFactory.Options options,
DecodeCallbacks callbacks,
BitmapPool bitmapPool)
throws IOException {
if (!options.inJustDecodeBounds) {
// 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. We need to do so immediately before
// decoding the full image to avoid having our mark limit overridden by other calls to
// mark and reset. See issue #225.
callbacks.onObtainBounds();
imageReader.stopGrowingBuffers();
}
// BitmapFactory.Options out* variables are reset by most calls to decodeStream, successful or
// otherwise, so capture here in case we log below.
int sourceWidth = options.outWidth;
int sourceHeight = options.outHeight;
String outMimeType = options.outMimeType;
final Bitmap result;
TransformationUtils.getBitmapDrawableLock().lock();
try {
result = imageReader.decodeBitmap(options);
} catch (IllegalArgumentException e) {
IOException bitmapAssertionException =
newIoExceptionForInBitmapAssertion(e, sourceWidth, sourceHeight, outMimeType, options);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(
TAG,
"Failed to decode with inBitmap, trying again without Bitmap re-use",
bitmapAssertionException);
}
if (options.inBitmap != null) {
try {
bitmapPool.put(options.inBitmap);
options.inBitmap = null;
return decodeStream(imageReader, options, callbacks, bitmapPool);
} catch (IOException resetException) {
throw bitmapAssertionException;
}
}
throw bitmapAssertionException;
} finally {
TransformationUtils.getBitmapDrawableLock().unlock();
}
return result;
}
private static boolean isScaling(BitmapFactory.Options options) {
return options.inTargetDensity > 0
&& options.inDensity > 0
&& options.inTargetDensity != options.inDensity;
}
private static void logDecode(
int sourceWidth,
int sourceHeight,
String outMimeType,
BitmapFactory.Options options,
Bitmap result,
int requestedWidth,
int requestedHeight,
long startTime) {
Log.v(
TAG,
"Decoded "
+ getBitmapString(result)
+ " from ["
+ sourceWidth
+ "x"
+ sourceHeight
+ "] "
+ outMimeType
+ " with inBitmap "
+ getInBitmapString(options)
+ " for ["
+ requestedWidth
+ "x"
+ requestedHeight
+ "]"
+ ", sample size: "
+ options.inSampleSize
+ ", density: "
+ options.inDensity
+ ", target density: "
+ options.inTargetDensity
+ ", thread: "
+ Thread.currentThread().getName()
+ ", duration: "
+ LogTime.getElapsedMillis(startTime));
}
private static String getInBitmapString(BitmapFactory.Options options) {
return getBitmapString(options.inBitmap);
}
@Nullable
@TargetApi(Build.VERSION_CODES.KITKAT)
private static String getBitmapString(Bitmap bitmap) {
if (bitmap == null) {
return null;
}
String sizeString =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
? " (" + bitmap.getAllocationByteCount() + ")"
: "";
return "["
+ bitmap.getWidth()
+ "x"
+ bitmap.getHeight()
+ "] "
+ bitmap.getConfig()
+ sizeString;
}
// BitmapFactory throws an IllegalArgumentException if any error occurs attempting to decode a
// file when inBitmap is non-null, including those caused by partial or corrupt data. We still log
// the error because the IllegalArgumentException is supposed to catch errors reusing Bitmaps, so
// want some useful log output. In most cases this can be safely treated as a normal IOException.
private static IOException newIoExceptionForInBitmapAssertion(
IllegalArgumentException e,
int outWidth,
int outHeight,
String outMimeType,
BitmapFactory.Options options) {
return new IOException(
"Exception decoding bitmap"
+ ", outWidth: "
+ outWidth
+ ", outHeight: "
+ outHeight
+ ", outMimeType: "
+ outMimeType
+ ", inBitmap: "
+ getInBitmapString(options),
e);
}
@SuppressWarnings("PMD.CollapsibleIfStatements")
@TargetApi(Build.VERSION_CODES.O)
private static void setInBitmap(
BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
@Nullable Bitmap.Config expectedConfig = null;
// Avoid short circuiting, it appears to break on some devices.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (options.inPreferredConfig == Config.HARDWARE) {
return;
}
// On API 26 outConfig may be null for some images even if the image is valid, can be decoded
// and outWidth/outHeight/outColorSpace are populated (see b/71513049).
expectedConfig = options.outConfig;
}
if (expectedConfig == null) {
// We're going to guess that BitmapFactory will return us the config we're requesting. This
// isn't always the case, even though our guesses tend to be conservative and prefer configs
// of larger sizes so that the Bitmap will fit our image anyway. If we're wrong here and the
// config we choose is too small, our initial decode will fail, but we will retry with no
// inBitmap which will succeed so if we're wrong here, we're less efficient but still correct.
expectedConfig = options.inPreferredConfig;
}
// BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
}
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);
}
}
@SuppressWarnings("deprecation")
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;
decodeBitmapOptions.inDensity = 0;
decodeBitmapOptions.inTargetDensity = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
decodeBitmapOptions.inPreferredColorSpace = null;
decodeBitmapOptions.outColorSpace = null;
decodeBitmapOptions.outConfig = null;
}
decodeBitmapOptions.outWidth = 0;
decodeBitmapOptions.outHeight = 0;
decodeBitmapOptions.outMimeType = null;
decodeBitmapOptions.inBitmap = null;
decodeBitmapOptions.inMutable = true;
}
/** Callbacks for key points during decodes. */
public interface DecodeCallbacks {
void onObtainBounds();
void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) throws IOException;
}
private static boolean isRotationRequired(int degreesToRotate) {
return degreesToRotate == 90 || degreesToRotate == 270;
}
}