/* * 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 java.io.InputStream; import java.util.concurrent.Executor; import com.facebook.common.internal.Preconditions; import com.facebook.common.references.CloseableReference; import com.facebook.common.util.TriState; import com.facebook.imageformat.ImageFormat; import com.facebook.imageformat.ImageFormatChecker; import com.facebook.imagepipeline.image.EncodedImage; import com.facebook.imagepipeline.memory.PooledByteBuffer; import com.facebook.imagepipeline.memory.PooledByteBufferFactory; import com.facebook.imagepipeline.memory.PooledByteBufferOutputStream; import com.facebook.imagepipeline.nativecode.WebpTranscoder; /** * Transcodes WebP to JPEG / PNG. * * <p> If processed image is one of VP8, VP8X or VP8L non-animated WebPs then it is transcoded to * jpeg if the decoder on the running version of Android does not support this format. This was the * case prior to version 4.2.1. * <p> If the image is not WebP, no transformation is applied. */ public class WebpTranscodeProducer implements Producer<EncodedImage> { private static final String PRODUCER_NAME = "WebpTranscodeProducer"; private static final int DEFAULT_JPEG_QUALITY = 80; private final Executor mExecutor; private final PooledByteBufferFactory mPooledByteBufferFactory; private final Producer<EncodedImage> mInputProducer; public WebpTranscodeProducer( Executor executor, PooledByteBufferFactory pooledByteBufferFactory, Producer<EncodedImage> inputProducer) { mExecutor = Preconditions.checkNotNull(executor); mPooledByteBufferFactory = Preconditions.checkNotNull(pooledByteBufferFactory); mInputProducer = Preconditions.checkNotNull(inputProducer); } @Override public void produceResults(final Consumer<EncodedImage> consumer, final ProducerContext context) { mInputProducer.produceResults(new WebpTranscodeConsumer(consumer, context), context); } private class WebpTranscodeConsumer extends DelegatingConsumer<EncodedImage, EncodedImage> { private final ProducerContext mContext; private TriState mShouldTranscodeWhenFinished; public WebpTranscodeConsumer( final Consumer<EncodedImage> consumer, final ProducerContext context) { super(consumer); mContext = context; mShouldTranscodeWhenFinished = TriState.UNSET; } @Override protected void onNewResultImpl(@Nullable EncodedImage newResult, boolean isLast) { // try to determine if the last result should be transformed if (mShouldTranscodeWhenFinished == TriState.UNSET && newResult != null) { mShouldTranscodeWhenFinished = shouldTranscode(newResult); } // just propagate result if it shouldn't be transformed if (mShouldTranscodeWhenFinished == TriState.NO) { getConsumer().onNewResult(newResult, isLast); return; } if (isLast) { if (mShouldTranscodeWhenFinished == TriState.YES && newResult != null) { transcodeLastResult(newResult, getConsumer(), mContext); } else { getConsumer().onNewResult(newResult, isLast); } } } } private void transcodeLastResult( final EncodedImage originalResult, final Consumer<EncodedImage> consumer, final ProducerContext producerContext) { Preconditions.checkNotNull(originalResult); final EncodedImage encodedImageCopy = EncodedImage.cloneOrNull(originalResult); final StatefulProducerRunnable<EncodedImage> runnable = new StatefulProducerRunnable<EncodedImage>( consumer, producerContext.getListener(), PRODUCER_NAME, producerContext.getId()) { @Override protected EncodedImage getResult() throws Exception { PooledByteBufferOutputStream outputStream = mPooledByteBufferFactory.newOutputStream(); try { doTranscode(encodedImageCopy, outputStream); CloseableReference<PooledByteBuffer> ref = CloseableReference.of(outputStream.toByteBuffer()); try { EncodedImage encodedImage = new EncodedImage(ref); encodedImage.copyMetaDataFrom(encodedImageCopy); return encodedImage; } finally { CloseableReference.closeSafely(ref); } } finally { outputStream.close(); } } @Override protected void disposeResult(EncodedImage result) { EncodedImage.closeSafely(result); } @Override protected void onSuccess(EncodedImage result) { EncodedImage.closeSafely(encodedImageCopy); super.onSuccess(result); } @Override protected void onFailure(Exception e) { EncodedImage.closeSafely(encodedImageCopy); super.onFailure(e); } @Override protected void onCancellation() { EncodedImage.closeSafely(encodedImageCopy); super.onCancellation(); } }; mExecutor.execute(runnable); } private static TriState shouldTranscode(final EncodedImage encodedImage) { Preconditions.checkNotNull(encodedImage); ImageFormat imageFormat = ImageFormatChecker.getImageFormat_WrapIOException( encodedImage.getInputStream()); switch (imageFormat) { case WEBP_SIMPLE: case WEBP_LOSSLESS: case WEBP_EXTENDED: case WEBP_EXTENDED_WITH_ALPHA: return TriState.valueOf(!WebpTranscoder.isWebpNativelySupported(imageFormat)); case UNKNOWN: // the image format might be unknown because we haven't fetched the whole header yet, // in which case the decision whether to transcode or not cannot be made yet return TriState.UNSET; default: // if the image format is known, but it is not WebP, then the image shouldn't be transcoded return TriState.NO; } } private static void doTranscode( final EncodedImage encodedImage, final PooledByteBufferOutputStream outputStream) throws Exception { InputStream imageInputStream = encodedImage.getInputStream(); ImageFormat imageFormat = ImageFormatChecker.getImageFormat_WrapIOException(imageInputStream); switch (imageFormat) { case WEBP_SIMPLE: case WEBP_EXTENDED: WebpTranscoder.transcodeWebpToJpeg(imageInputStream, outputStream, DEFAULT_JPEG_QUALITY); break; case WEBP_LOSSLESS: case WEBP_EXTENDED_WITH_ALPHA: WebpTranscoder.transcodeWebpToPng(imageInputStream, outputStream); break; default: throw new IllegalArgumentException("Wrong image format"); } } }