/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.imagepipeline.platform;
import javax.annotation.concurrent.ThreadSafe;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.support.v4.util.Pools.SynchronizedPool;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.streams.LimitedInputStream;
import com.facebook.common.streams.TailAppendingInputStream;
import com.facebook.imagepipeline.image.EncodedImage;
import com.facebook.imagepipeline.memory.BitmapPool;
import com.facebook.imageutils.BitmapUtil;
import com.facebook.imageutils.JfifUtil;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* Bitmap decoder for ART VM (Lollipop and up).
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@ThreadSafe
public class ArtDecoder implements PlatformDecoder {
/**
* Size of temporary array. Value recommended by Android docs for decoding Bitmaps.
*/
private static final int DECODE_BUFFER_SIZE = 16 * 1024;
private final BitmapPool mBitmapPool;
/**
* ArtPlatformImageDecoder decodes images from InputStream - to do so we need to provide
* temporary buffer, otherwise framework will allocate one for us for each decode request
*/
@VisibleForTesting final SynchronizedPool<ByteBuffer> mDecodeBuffers;
// TODO (5884402) - remove dependency on JfifUtil
private static final byte[] EOI_TAIL = new byte[] {
(byte) JfifUtil.MARKER_FIRST_BYTE,
(byte) JfifUtil.MARKER_EOI};
public ArtDecoder(BitmapPool bitmapPool, int maxNumThreads) {
mBitmapPool = bitmapPool;
mDecodeBuffers = new SynchronizedPool<>(maxNumThreads);
for (int i = 0; i < maxNumThreads; i++) {
mDecodeBuffers.release(ByteBuffer.allocate(DECODE_BUFFER_SIZE));
}
}
/**
* Creates a bitmap from encoded bytes.
* @param encodedImage the encoded image with a reference to the encoded bytes
* @param bitmapConfig the {@link android.graphics.Bitmap.Config}
* used to create the decoded Bitmap
* @return the bitmap
* @exception java.lang.OutOfMemoryError if the Bitmap cannot be allocated
*/
@Override
public CloseableReference<Bitmap> decodeFromEncodedImage(
EncodedImage encodedImage,
Bitmap.Config bitmapConfig) {
final BitmapFactory.Options options = getDecodeOptionsForStream(encodedImage, bitmapConfig);
boolean retryOnFail=options.inPreferredConfig != Bitmap.Config.ARGB_8888;
try {
return decodeStaticImageFromStream(encodedImage.getInputStream(), options);
} catch (RuntimeException re) {
if (retryOnFail) {
return decodeFromEncodedImage(encodedImage, Bitmap.Config.ARGB_8888);
}
throw re;
}
}
/**
* Creates a bitmap from encoded JPEG bytes. Supports a partial JPEG image.
* @param encodedImage the encoded image with reference to the encoded bytes
* @param bitmapConfig the {@link android.graphics.Bitmap.Config}
* used to create the decoded Bitmap
* @param length the number of encoded bytes in the buffer
* @return the bitmap
* @exception java.lang.OutOfMemoryError if the Bitmap cannot be allocated
*/
@Override
public CloseableReference<Bitmap> decodeJPEGFromEncodedImage(
EncodedImage encodedImage,
Bitmap.Config bitmapConfig,
int length) {
boolean isJpegComplete = encodedImage.isCompleteAt(length);
final BitmapFactory.Options options = getDecodeOptionsForStream(encodedImage, bitmapConfig);
InputStream jpegDataStream = encodedImage.getInputStream();
// At this point the InputStream from the encoded image should not be null since in the
// pipeline,this comes from a call stack where this was checked before. Also this method needs
// the InputStream to decode the image so this can't be null.
Preconditions.checkNotNull(jpegDataStream);
if (encodedImage.getSize() > length) {
jpegDataStream = new LimitedInputStream(jpegDataStream, length);
}
if (!isJpegComplete) {
jpegDataStream = new TailAppendingInputStream(jpegDataStream, EOI_TAIL);
}
boolean retryOnFail=options.inPreferredConfig != Bitmap.Config.ARGB_8888;
try {
return decodeStaticImageFromStream(jpegDataStream, options);
} catch (RuntimeException re) {
if (retryOnFail) {
return decodeFromEncodedImage(encodedImage, Bitmap.Config.ARGB_8888);
}
throw re;
}
}
private CloseableReference<Bitmap> decodeStaticImageFromStream(
InputStream inputStream,
BitmapFactory.Options options) {
Preconditions.checkNotNull(inputStream);
int sizeInBytes = BitmapUtil.getSizeInByteForBitmap(
options.outWidth,
options.outHeight,
options.inPreferredConfig);
final Bitmap bitmapToReuse = mBitmapPool.get(sizeInBytes);
if (bitmapToReuse == null) {
throw new NullPointerException("BitmapPool.get returned null");
}
options.inBitmap = bitmapToReuse;
Bitmap decodedBitmap;
ByteBuffer byteBuffer = mDecodeBuffers.acquire();
if (byteBuffer == null) {
byteBuffer = ByteBuffer.allocate(DECODE_BUFFER_SIZE);
}
try {
options.inTempStorage = byteBuffer.array();
decodedBitmap = BitmapFactory.decodeStream(inputStream, null, options);
} catch (RuntimeException re) {
mBitmapPool.release(bitmapToReuse);
throw re;
} finally {
mDecodeBuffers.release(byteBuffer);
}
if (bitmapToReuse != decodedBitmap) {
mBitmapPool.release(bitmapToReuse);
decodedBitmap.recycle();
throw new IllegalStateException();
}
return CloseableReference.of(decodedBitmap, mBitmapPool);
}
/**
* Options returned by this method are configured with mDecodeBuffer which is GuardedBy("this")
*/
private static BitmapFactory.Options getDecodeOptionsForStream(
EncodedImage encodedImage,
Bitmap.Config bitmapConfig) {
final BitmapFactory.Options options = new BitmapFactory.Options();
// Sample size should ONLY be different than 1 when downsampling is enabled in the pipeline
options.inSampleSize = encodedImage.getSampleSize();
options.inJustDecodeBounds = true;
// fill outWidth and outHeight
BitmapFactory.decodeStream(encodedImage.getInputStream(), null, options);
if (options.outWidth == -1 || options.outHeight == -1) {
throw new IllegalArgumentException();
}
options.inJustDecodeBounds = false;
options.inDither = true;
options.inPreferredConfig = bitmapConfig;
options.inMutable = true;
return options;
}
}