/*
* 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.producers;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import android.graphics.Bitmap;
import com.facebook.common.internal.ImmutableMap;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.memory.ByteArrayPool;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.util.ExceptionWithNoStacktrace;
import com.facebook.common.util.UriUtil;
import com.facebook.imageformat.DefaultImageFormats;
import com.facebook.imageformat.ImageFormat;
import com.facebook.imagepipeline.common.ImageDecodeOptions;
import com.facebook.imagepipeline.common.ResizeOptions;
import com.facebook.imagepipeline.decoder.ImageDecoder;
import com.facebook.imagepipeline.decoder.ProgressiveJpegConfig;
import com.facebook.imagepipeline.decoder.ProgressiveJpegParser;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.image.CloseableStaticBitmap;
import com.facebook.imagepipeline.image.EncodedImage;
import com.facebook.imagepipeline.image.ImmutableQualityInfo;
import com.facebook.imagepipeline.image.QualityInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import static com.facebook.imagepipeline.producers.JobScheduler.JobRunnable;
/**
* Decodes images.
*
* <p/> Progressive JPEGs are decoded progressively as new data arrives.
*/
public class DecodeProducer implements Producer<CloseableReference<CloseableImage>> {
public static final String PRODUCER_NAME = "DecodeProducer";
// keys for extra map
public static final String EXTRA_BITMAP_SIZE = ProducerConstants.EXTRA_BITMAP_SIZE;
public static final String EXTRA_HAS_GOOD_QUALITY = ProducerConstants.EXTRA_HAS_GOOD_QUALITY;
public static final String EXTRA_IS_FINAL = ProducerConstants.EXTRA_IS_FINAL;
public static final String EXTRA_IMAGE_FORMAT_NAME = ProducerConstants.EXTRA_IMAGE_FORMAT_NAME;
public static final String ENCODED_IMAGE_SIZE = ProducerConstants.ENCODED_IMAGE_SIZE;
public static final String REQUESTED_IMAGE_SIZE = ProducerConstants.REQUESTED_IMAGE_SIZE;
public static final String SAMPLE_SIZE = ProducerConstants.SAMPLE_SIZE;
private final ByteArrayPool mByteArrayPool;
private final Executor mExecutor;
private final ImageDecoder mImageDecoder;
private final ProgressiveJpegConfig mProgressiveJpegConfig;
private final Producer<EncodedImage> mInputProducer;
private final boolean mDownsampleEnabled;
private final boolean mDownsampleEnabledForNetwork;
private final boolean mDecodeCancellationEnabled;
public DecodeProducer(
final ByteArrayPool byteArrayPool,
final Executor executor,
final ImageDecoder imageDecoder,
final ProgressiveJpegConfig progressiveJpegConfig,
final boolean downsampleEnabled,
final boolean downsampleEnabledForNetwork,
final boolean decodeCancellationEnabled,
final Producer<EncodedImage> inputProducer) {
mByteArrayPool = Preconditions.checkNotNull(byteArrayPool);
mExecutor = Preconditions.checkNotNull(executor);
mImageDecoder = Preconditions.checkNotNull(imageDecoder);
mProgressiveJpegConfig = Preconditions.checkNotNull(progressiveJpegConfig);
mDownsampleEnabled = downsampleEnabled;
mDownsampleEnabledForNetwork = downsampleEnabledForNetwork;
mInputProducer = Preconditions.checkNotNull(inputProducer);
mDecodeCancellationEnabled = decodeCancellationEnabled;
}
@Override
public void produceResults(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext) {
final ImageRequest imageRequest = producerContext.getImageRequest();
ProgressiveDecoder progressiveDecoder;
if (!UriUtil.isNetworkUri(imageRequest.getSourceUri())) {
progressiveDecoder = new LocalImagesProgressiveDecoder(
consumer,
producerContext,
mDecodeCancellationEnabled);
} else {
ProgressiveJpegParser jpegParser = new ProgressiveJpegParser(mByteArrayPool);
progressiveDecoder = new NetworkImagesProgressiveDecoder(
consumer,
producerContext,
jpegParser,
mProgressiveJpegConfig,
mDecodeCancellationEnabled);
}
mInputProducer.produceResults(progressiveDecoder, producerContext);
}
private abstract class ProgressiveDecoder extends DelegatingConsumer<
EncodedImage, CloseableReference<CloseableImage>> {
private final ProducerContext mProducerContext;
private final ProducerListener mProducerListener;
private final ImageDecodeOptions mImageDecodeOptions;
@GuardedBy("this")
private boolean mIsFinished;
private final JobScheduler mJobScheduler;
public ProgressiveDecoder(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext,
final boolean decodeCancellationEnabled) {
super(consumer);
mProducerContext = producerContext;
mProducerListener = producerContext.getListener();
mImageDecodeOptions = producerContext.getImageRequest().getImageDecodeOptions();
mIsFinished = false;
JobRunnable job = new JobRunnable() {
@Override
public void run(EncodedImage encodedImage, @Status int status) {
if (encodedImage != null) {
if (mDownsampleEnabled) {
ImageRequest request = producerContext.getImageRequest();
if (mDownsampleEnabledForNetwork ||
!UriUtil.isNetworkUri(request.getSourceUri())) {
encodedImage.setSampleSize(DownsampleUtil.determineSampleSize(
request, encodedImage));
}
}
doDecode(encodedImage, status);
}
}
};
mJobScheduler = new JobScheduler(mExecutor, job, mImageDecodeOptions.minDecodeIntervalMs);
mProducerContext.addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onIsIntermediateResultExpectedChanged() {
if (mProducerContext.isIntermediateResultExpected()) {
mJobScheduler.scheduleJob();
}
}
@Override
public void onCancellationRequested() {
if (decodeCancellationEnabled) {
handleCancellation();
}
}
});
}
@Override
public void onNewResultImpl(EncodedImage newResult, @Status int status) {
final boolean isLast = isLast(status);
if (isLast && !EncodedImage.isValid(newResult)) {
handleError(new ExceptionWithNoStacktrace("Encoded image is not valid."));
return;
}
if (!updateDecodeJob(newResult, status)) {
return;
}
final boolean isPlaceholder = statusHasFlag(status, IS_PLACEHOLDER);
if (isLast || isPlaceholder || mProducerContext.isIntermediateResultExpected()) {
mJobScheduler.scheduleJob();
}
}
@Override
protected void onProgressUpdateImpl(float progress) {
super.onProgressUpdateImpl(progress * 0.99f);
}
@Override
public void onFailureImpl(Throwable t) {
handleError(t);
}
@Override
public void onCancellationImpl() {
handleCancellation();
}
/** Updates the decode job. */
protected boolean updateDecodeJob(EncodedImage ref, @Status int status) {
return mJobScheduler.updateJob(ref, status);
}
/** Performs the decode synchronously. */
private void doDecode(EncodedImage encodedImage, @Status int status) {
if (isFinished() || !EncodedImage.isValid(encodedImage)) {
return;
}
final String imageFormatStr;
ImageFormat imageFormat = encodedImage.getImageFormat();
if (imageFormat != null) {
imageFormatStr = imageFormat.getName();
} else {
imageFormatStr = "unknown";
}
final String encodedImageSize;
final String sampleSize;
final boolean isLast = isLast(status);
final boolean isPlaceholder = statusHasFlag(status, IS_PLACEHOLDER);
if (encodedImage != null) {
encodedImageSize = encodedImage.getWidth() + "x" + encodedImage.getHeight();
sampleSize = String.valueOf(encodedImage.getSampleSize());
} else {
// We should never be here
encodedImageSize = "unknown";
sampleSize = "unknown";
}
final String requestedSizeStr;
final ResizeOptions resizeOptions = mProducerContext.getImageRequest().getResizeOptions();
if (resizeOptions != null) {
requestedSizeStr = resizeOptions.width + "x" + resizeOptions.height;
} else {
requestedSizeStr = "unknown";
}
try {
long queueTime = mJobScheduler.getQueuedTime();
int length = isLast || isPlaceholder ?
encodedImage.getSize() : getIntermediateImageEndOffset(encodedImage);
QualityInfo quality =
isLast || isPlaceholder ? ImmutableQualityInfo.FULL_QUALITY : getQualityInfo();
mProducerListener.onProducerStart(mProducerContext.getId(), PRODUCER_NAME);
CloseableImage image = null;
try {
image = mImageDecoder.decode(encodedImage, length, quality, mImageDecodeOptions);
} catch (Exception e) {
Map<String, String> extraMap = getExtraMap(
image,
queueTime,
quality,
isLast,
imageFormatStr,
encodedImageSize,
requestedSizeStr,
sampleSize);
mProducerListener.
onProducerFinishWithFailure(mProducerContext.getId(), PRODUCER_NAME, e, extraMap);
handleError(e);
return;
}
Map<String, String> extraMap = getExtraMap(
image,
queueTime,
quality,
isLast,
imageFormatStr,
encodedImageSize,
requestedSizeStr,
sampleSize);
mProducerListener.
onProducerFinishWithSuccess(mProducerContext.getId(), PRODUCER_NAME, extraMap);
handleResult(image, status);
} finally {
EncodedImage.closeSafely(encodedImage);
}
}
private Map<String, String> getExtraMap(
@Nullable CloseableImage image,
long queueTime,
QualityInfo quality,
boolean isFinal,
String imageFormatName,
String encodedImageSize,
String requestImageSize,
String sampleSize) {
if (!mProducerListener.requiresExtraMap(mProducerContext.getId())) {
return null;
}
String queueStr = String.valueOf(queueTime);
String qualityStr = String.valueOf(quality.isOfGoodEnoughQuality());
String finalStr = String.valueOf(isFinal);
if (image instanceof CloseableStaticBitmap) {
Bitmap bitmap = ((CloseableStaticBitmap) image).getUnderlyingBitmap();
String sizeStr = bitmap.getWidth() + "x" + bitmap.getHeight();
// We need this because the copyOf() utility method doesn't have a proper overload method
// for all these parameters
final Map<String, String> tmpMap = new HashMap<>(8);
tmpMap.put(EXTRA_BITMAP_SIZE, sizeStr);
tmpMap.put(JobScheduler.QUEUE_TIME_KEY, queueStr);
tmpMap.put(EXTRA_HAS_GOOD_QUALITY, qualityStr);
tmpMap.put(EXTRA_IS_FINAL, finalStr);
tmpMap.put(ENCODED_IMAGE_SIZE, encodedImageSize);
tmpMap.put(EXTRA_IMAGE_FORMAT_NAME, imageFormatName);
tmpMap.put(REQUESTED_IMAGE_SIZE, requestImageSize);
tmpMap.put(SAMPLE_SIZE, sampleSize);
return ImmutableMap.copyOf(tmpMap);
} else {
final Map<String, String> tmpMap = new HashMap<>(7);
tmpMap.put(JobScheduler.QUEUE_TIME_KEY, queueStr);
tmpMap.put(EXTRA_HAS_GOOD_QUALITY, qualityStr);
tmpMap.put(EXTRA_IS_FINAL, finalStr);
tmpMap.put(ENCODED_IMAGE_SIZE, encodedImageSize);
tmpMap.put(EXTRA_IMAGE_FORMAT_NAME, imageFormatName);
tmpMap.put(REQUESTED_IMAGE_SIZE, requestImageSize);
tmpMap.put(SAMPLE_SIZE, sampleSize);
return ImmutableMap.copyOf(tmpMap);
}
}
/**
* @return true if producer is finished
*/
private synchronized boolean isFinished() {
return mIsFinished;
}
/**
* Finishes if not already finished and <code>shouldFinish</code> is specified.
* <p> If just finished, the intermediate image gets released.
*/
private void maybeFinish(boolean shouldFinish) {
synchronized (ProgressiveDecoder.this) {
if (!shouldFinish || mIsFinished) {
return;
}
getConsumer().onProgressUpdate(1.0f);
mIsFinished = true;
}
mJobScheduler.clearJob();
}
/**
* Notifies consumer of new result and finishes if the result is final.
*/
private void handleResult(final CloseableImage decodedImage, final @Status int status) {
CloseableReference<CloseableImage> decodedImageRef = CloseableReference.of(decodedImage);
try {
maybeFinish(isLast(status));
getConsumer().onNewResult(decodedImageRef, status);
} finally {
CloseableReference.closeSafely(decodedImageRef);
}
}
/**
* Notifies consumer about the failure and finishes.
*/
private void handleError(Throwable t) {
maybeFinish(true);
getConsumer().onFailure(t);
}
/**
* Notifies consumer about the cancellation and finishes.
*/
private void handleCancellation() {
maybeFinish(true);
getConsumer().onCancellation();
}
protected abstract int getIntermediateImageEndOffset(EncodedImage encodedImage);
protected abstract QualityInfo getQualityInfo();
}
private class LocalImagesProgressiveDecoder extends ProgressiveDecoder {
public LocalImagesProgressiveDecoder(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext,
final boolean decodeCancellationEnabled) {
super(consumer, producerContext, decodeCancellationEnabled);
}
@Override
protected synchronized boolean updateDecodeJob(EncodedImage encodedImage, @Status int status) {
if (isNotLast(status)) {
return false;
}
return super.updateDecodeJob(encodedImage, status);
}
@Override
protected int getIntermediateImageEndOffset(EncodedImage encodedImage) {
return encodedImage.getSize();
}
@Override
protected QualityInfo getQualityInfo() {
return ImmutableQualityInfo.of(0, false, false);
}
}
private class NetworkImagesProgressiveDecoder extends ProgressiveDecoder {
private final ProgressiveJpegParser mProgressiveJpegParser;
private final ProgressiveJpegConfig mProgressiveJpegConfig;
private int mLastScheduledScanNumber;
public NetworkImagesProgressiveDecoder(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext,
final ProgressiveJpegParser progressiveJpegParser,
final ProgressiveJpegConfig progressiveJpegConfig,
final boolean decodeCancellationEnabled) {
super(consumer, producerContext, decodeCancellationEnabled);
mProgressiveJpegParser = Preconditions.checkNotNull(progressiveJpegParser);
mProgressiveJpegConfig = Preconditions.checkNotNull(progressiveJpegConfig);
mLastScheduledScanNumber = 0;
}
@Override
protected synchronized boolean updateDecodeJob(EncodedImage encodedImage, @Status int status) {
boolean ret = super.updateDecodeJob(encodedImage, status);
if (isNotLast(status)
&& !statusHasFlag(status, IS_PLACEHOLDER)
&& EncodedImage.isValid(encodedImage)
&& encodedImage.getImageFormat() == DefaultImageFormats.JPEG) {
if (!mProgressiveJpegParser.parseMoreData(encodedImage)) {
return false;
}
int scanNum = mProgressiveJpegParser.getBestScanNumber();
if (scanNum <= mLastScheduledScanNumber) {
// We have already decoded this scan, no need to do so again
return false;
}
if (scanNum < mProgressiveJpegConfig.getNextScanNumberToDecode(mLastScheduledScanNumber)
&& !mProgressiveJpegParser.isEndMarkerRead()) {
// We have not reached the minimum scan set by the configuration and there
// are still more scans to be read (the end marker is not reached)
return false;
}
mLastScheduledScanNumber = scanNum;
}
return ret;
}
@Override
protected int getIntermediateImageEndOffset(EncodedImage encodedImage) {
return mProgressiveJpegParser.getBestScanEndOffset();
}
@Override
protected QualityInfo getQualityInfo() {
return mProgressiveJpegConfig.getQualityInfo(mProgressiveJpegParser.getBestScanNumber());
}
}
}