/*
* 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.Map;
import java.util.concurrent.Executor;
import android.os.SystemClock;
import com.facebook.common.internal.ImmutableMap;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.util.UriUtil;
import com.facebook.imagepipeline.common.ImageDecodeOptions;
import com.facebook.imagepipeline.decoder.ProgressiveJpegConfig;
import com.facebook.imagepipeline.decoder.ImageDecoder;
import com.facebook.imagepipeline.decoder.ProgressiveJpegParser;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.image.ImmutableQualityInfo;
import com.facebook.imagepipeline.image.QualityInfo;
import com.facebook.imagepipeline.memory.ByteArrayPool;
import com.facebook.imagepipeline.memory.PooledByteBuffer;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imageformat.ImageFormat;
/**
* Decodes images. Progressive JPEGs are decoded progressively as new data arrives
*
* TODO 5416926: use more elaborate algorithm for throttling decoded scans
*/
public class DecodeProducer implements Producer<CloseableReference<CloseableImage>> {
@VisibleForTesting
static final String PRODUCER_NAME = "DecodeProducer";
// keys for extra map
private static final String QUEUE_TIME_KEY = "queueTime";
private static final String HAS_GOOD_QUALITY_KEY = "hasGoodQuality";
private static final String IS_FINAL_KEY = "isFinal";
private final ByteArrayPool mByteArrayPool;
private final Executor mExecutor;
private final ImageDecoder mImageDecoder;
private final ProgressiveJpegConfig mProgressiveJpegConfig;
private final Producer<CloseableReference<PooledByteBuffer>> mNextProducer;
public DecodeProducer(
final ByteArrayPool byteArrayPool,
final Executor executor,
final ImageDecoder imageDecoder,
final ProgressiveJpegConfig progressiveJpegConfig,
final Producer<CloseableReference<PooledByteBuffer>> nextProducer) {
mByteArrayPool = Preconditions.checkNotNull(byteArrayPool);
mExecutor = Preconditions.checkNotNull(executor);
mImageDecoder = Preconditions.checkNotNull(imageDecoder);
mProgressiveJpegConfig = Preconditions.checkNotNull(progressiveJpegConfig);
mNextProducer = Preconditions.checkNotNull(nextProducer);
}
@Override
public void produceResults(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext context) {
final ImageRequest imageRequest = context.getImageRequest();
ProgressiveDecoder progressiveDecoder;
if (!UriUtil.isNetworkUri(imageRequest.getSourceUri())) {
progressiveDecoder = new LocalImagesProgressiveDecoder(consumer, context);
} else {
ProgressiveJpegParser jpegParser = new ProgressiveJpegParser(mByteArrayPool);
progressiveDecoder = new NetworkImagesProgressiveDecoder(
consumer,
context,
jpegParser,
mProgressiveJpegConfig);
}
mNextProducer.produceResults(progressiveDecoder, context);
}
@VisibleForTesting
abstract class ProgressiveDecoder extends BaseConsumer<CloseableReference<PooledByteBuffer>> {
private final Consumer<CloseableReference<CloseableImage>> mConsumer;
protected final ProducerContext mProducerContext;
private final ProducerListener mProducerListener;
private final ImageDecodeOptions mImageDecodeOptions;
@GuardedBy("this")
private boolean mIsFinished;
@GuardedBy("ProgressiveDecoder.this")
@VisibleForTesting CloseableReference<PooledByteBuffer> mStoredIntermediateImageBytesRef;
@GuardedBy("ProgressiveDecoder.this")
@VisibleForTesting int mStoredIntermediateImageBestScanEnd;
@GuardedBy("ProgressiveDecoder.this")
@VisibleForTesting ImageFormat mStoredIntermediateImageFormat;
@GuardedBy("ProgressiveDecoder.this")
@VisibleForTesting QualityInfo mStoredIntermediateImageQualityInfo;
public ProgressiveDecoder(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext) {
mConsumer = consumer;
mProducerContext = producerContext;
mProducerListener = producerContext.getListener();
mImageDecodeOptions = producerContext.getImageRequest().getImageDecodeOptions();
mIsFinished = false;
mProducerContext.addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onIsIntermediateResultExpectedChanged() {
if (mProducerContext.isIntermediateResultExpected()) {
maybeScheduleStoredIntermediateImageDecode();
}
}
});
}
private synchronized void maybeScheduleStoredIntermediateImageDecode() {
if (mStoredIntermediateImageBytesRef != null) {
scheduleImageDecode(
mStoredIntermediateImageBytesRef,
mStoredIntermediateImageBestScanEnd,
mStoredIntermediateImageFormat,
mStoredIntermediateImageQualityInfo,
false);
closeStoredIntermediateImageBytes();
}
}
protected synchronized void updateStoredIntermediateImage(
CloseableReference<PooledByteBuffer> intermediateImageBytesRef,
int intermediateImageBestScanEnd,
ImageFormat intermediateImageFormat,
QualityInfo intermediateImageQualityInfo) {
closeStoredIntermediateImageBytes();
mStoredIntermediateImageBytesRef = intermediateImageBytesRef.clone();
mStoredIntermediateImageBestScanEnd = intermediateImageBestScanEnd;
mStoredIntermediateImageFormat = intermediateImageFormat;
mStoredIntermediateImageQualityInfo = intermediateImageQualityInfo;
}
protected synchronized void closeStoredIntermediateImageBytes() {
if (mStoredIntermediateImageBytesRef != null) {
mStoredIntermediateImageBytesRef.close();
mStoredIntermediateImageBytesRef = null;
}
}
@Override
public void onNewResultImpl(CloseableReference<PooledByteBuffer> newResult, boolean isLast) {
if (isLast) {
decodeFinalImage(newResult);
} else {
maybeDecodeIntermediateImage(newResult);
}
}
@Override
public void onFailureImpl(Throwable t) {
handleError(t);
}
@Override
public void onCancellationImpl() {
handleCancellation();
}
private void decodeFinalImage(final CloseableReference<PooledByteBuffer> imageBytesRef) {
closeStoredIntermediateImageBytes();
if (imageBytesRef == null) {
handleResult(null, true);
} else {
scheduleImageDecode(
imageBytesRef,
imageBytesRef.get().size(),
getImageFormat(imageBytesRef),
ImmutableQualityInfo.FULL_QUALITY,
/* isFinal */ true);
}
}
protected void maybeDecodeIntermediateImage(
CloseableReference<PooledByteBuffer> imageBytesRef) {
Preconditions.checkNotNull(imageBytesRef);
final int bestScanEnd = getIntermediateImageEndOffset(imageBytesRef);
final ImageFormat imageFormat = getImageFormat(imageBytesRef);
final QualityInfo qualityInfo = getQualityInfo(imageBytesRef);
synchronized (ProgressiveDecoder.this) {
if (!mProducerContext.isIntermediateResultExpected()) {
// do not schedule decode as the result is not expected.
// however, keep the result in case the client should need it in the future.
updateStoredIntermediateImage(
imageBytesRef,
bestScanEnd,
imageFormat,
qualityInfo);
} else {
closeStoredIntermediateImageBytes();
scheduleImageDecode(
imageBytesRef,
bestScanEnd,
imageFormat,
qualityInfo,
false);
}
}
}
protected void scheduleImageDecode(
final CloseableReference<PooledByteBuffer> imageBytesRef,
final int length,
@Nullable final ImageFormat imageFormat,
final QualityInfo qualityInfo,
final boolean isFinal) {
final CloseableReference<PooledByteBuffer> imageBytesRefCopy = imageBytesRef.clone();
final long startTime = SystemClock.elapsedRealtime();
mExecutor.execute(
new Runnable() {
@Override
public void run() {
final long queueTime = SystemClock.elapsedRealtime() - startTime;
try {
if (isFinished()) {
return;
}
mProducerListener.onProducerStart(mProducerContext.getId(), PRODUCER_NAME);
CloseableImage decodedImage = mImageDecoder.decodeImage(
imageBytesRefCopy,
imageFormat,
length,
qualityInfo,
mImageDecodeOptions);
mProducerListener.onProducerFinishWithSuccess(
mProducerContext.getId(),
PRODUCER_NAME,
getExtraMap(queueTime, qualityInfo, isFinal));
handleResult(decodedImage, isFinal);
} catch (Exception e) {
mProducerListener.onProducerFinishWithFailure(
mProducerContext.getId(),
PRODUCER_NAME,
e,
getExtraMap(queueTime, qualityInfo, isFinal));
handleError(e);
} finally {
imageBytesRefCopy.close();
}
}
});
}
private Map<String, String> getExtraMap(
final long queueTime,
final QualityInfo qualityInfo,
final boolean isFinal) {
if (!mProducerListener.requiresExtraMap(mProducerContext.getId())) {
return null;
}
return ImmutableMap.of(
QUEUE_TIME_KEY,
String.valueOf(queueTime),
HAS_GOOD_QUALITY_KEY,
String.valueOf(qualityInfo.isOfGoodEnoughQuality()),
IS_FINAL_KEY,
String.valueOf(isFinal));
}
/**
* @return true if producer is finished
*/
private synchronized boolean isFinished() {
return mIsFinished;
}
/**
* Finishes if not already finished and {@code finish} is specified.
* <p> If just finished, the intermediate image gets released.
*/
private synchronized void maybeFinish(boolean finish) {
if (mIsFinished) {
return;
}
mIsFinished = finish;
if (finish) {
closeStoredIntermediateImageBytes();
}
}
/**
* Notifies consumer of new result and finishes if the result is final.
*/
private void handleResult(final CloseableImage decodedImage, final boolean isFinal) {
CloseableReference<CloseableImage> decodedImageRef = CloseableReference.of(decodedImage);
try {
maybeFinish(isFinal);
mConsumer.onNewResult(decodedImageRef, isFinal);
} finally {
CloseableReference.closeSafely(decodedImageRef);
}
}
/**
* Notifies consumer about the failure and finishes.
*/
private void handleError(Throwable t) {
maybeFinish(true);
mConsumer.onFailure(t);
}
/**
* Notifies consumer about the cancellation and finishes.
*/
private void handleCancellation() {
maybeFinish(true);
mConsumer.onCancellation();
}
/**
* All these abstract methods are thread-safe.
*/
@Nullable protected abstract ImageFormat getImageFormat(
CloseableReference<PooledByteBuffer> imageBytesRef);
protected abstract int getIntermediateImageEndOffset(
CloseableReference<PooledByteBuffer> imageBytesRef);
protected abstract QualityInfo getQualityInfo(
CloseableReference<PooledByteBuffer> imageBytesRef);
}
class LocalImagesProgressiveDecoder extends ProgressiveDecoder {
@VisibleForTesting LocalImagesProgressiveDecoder(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext) {
super(consumer, producerContext);
}
@Override
@Nullable protected ImageFormat getImageFormat(
CloseableReference<PooledByteBuffer> imageBytesRef) {
return null;
}
@Override
protected int getIntermediateImageEndOffset(
CloseableReference<PooledByteBuffer> imageBytesRef) {
return imageBytesRef.get().size();
}
@Override
protected QualityInfo getQualityInfo(CloseableReference<PooledByteBuffer> imageBytesRef) {
return ImmutableQualityInfo.of(0, false, false);
}
}
class NetworkImagesProgressiveDecoder extends ProgressiveDecoder {
private final ProgressiveJpegParser mProgressiveJpegParser;
private final ProgressiveJpegConfig mProgressiveJpegConfig;
private int mLastDecodedScanNumber;
NetworkImagesProgressiveDecoder(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext,
final ProgressiveJpegParser progressiveJpegParser,
final ProgressiveJpegConfig progressiveJpegConfig) {
super(consumer, producerContext);
mProgressiveJpegParser = Preconditions.checkNotNull(progressiveJpegParser);
mProgressiveJpegConfig = Preconditions.checkNotNull(progressiveJpegConfig);
mLastDecodedScanNumber = 0;
}
@Override
protected void maybeDecodeIntermediateImage(
CloseableReference<PooledByteBuffer> imageBytesRef) {
Preconditions.checkNotNull(imageBytesRef);
if (mProgressiveJpegParser.parseMoreData(imageBytesRef) &&
mProgressiveJpegParser.getBestScanNumber() >=
mProgressiveJpegConfig.getNextScanNumberToDecode(mLastDecodedScanNumber)) {
mLastDecodedScanNumber = mProgressiveJpegParser.getBestScanNumber();
super.maybeDecodeIntermediateImage(imageBytesRef);
}
}
@Override
@Nullable protected ImageFormat getImageFormat(
CloseableReference<PooledByteBuffer> imageBytesRef) {
return mProgressiveJpegParser.isJpeg() ? ImageFormat.JPEG : ImageFormat.UNKNOWN;
}
@Override
protected int getIntermediateImageEndOffset(
CloseableReference<PooledByteBuffer> imageBytesRef) {
return mProgressiveJpegParser.getBestScanEndOffset();
}
@Override
protected QualityInfo getQualityInfo(CloseableReference<PooledByteBuffer> imageBytesRef) {
return mProgressiveJpegConfig.getQualityInfo(mProgressiveJpegParser.getBestScanNumber());
}
}
}