/* * 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.image; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import java.io.Closeable; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import android.util.Pair; import com.facebook.cache.common.CacheKey; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.Supplier; import com.facebook.common.internal.VisibleForTesting; import com.facebook.common.memory.PooledByteBuffer; import com.facebook.common.memory.PooledByteBufferInputStream; import com.facebook.common.references.CloseableReference; import com.facebook.common.references.SharedReference; import com.facebook.imageformat.DefaultImageFormats; import com.facebook.imageformat.ImageFormat; import com.facebook.imageformat.ImageFormatChecker; import com.facebook.imageutils.BitmapUtil; import com.facebook.imageutils.JfifUtil; import com.facebook.imageutils.WebpUtil; /** * Class that contains all the information for an encoded image, both the image bytes (held on * a byte buffer or a supplier of input streams) and the extracted meta data that is useful for * image transforms. * * <p>Only one of the input stream supplier or the byte buffer can be set. If using an input stream * supplier, the methods that return a byte buffer will simply return null. However, getInputStream * will always be supported, either from the supplier or an input stream created from the byte * buffer held. * * <p>Currently the data is useful for rotation and resize. */ @Immutable public class EncodedImage implements Closeable { public static final int UNKNOWN_ROTATION_ANGLE = -1; public static final int UNKNOWN_WIDTH = -1; public static final int UNKNOWN_HEIGHT = -1; public static final int UNKNOWN_STREAM_SIZE = -1; public static final int DEFAULT_SAMPLE_SIZE = 1; // Only one of this will be set. The EncodedImage can either be backed by a ByteBuffer or a // Supplier of InputStream, but not both. private final @Nullable CloseableReference<PooledByteBuffer> mPooledByteBufferRef; private final @Nullable Supplier<FileInputStream> mInputStreamSupplier; private ImageFormat mImageFormat = ImageFormat.UNKNOWN; private int mRotationAngle = UNKNOWN_ROTATION_ANGLE; private int mWidth = UNKNOWN_WIDTH; private int mHeight = UNKNOWN_HEIGHT; private int mSampleSize = DEFAULT_SAMPLE_SIZE; private int mStreamSize = UNKNOWN_STREAM_SIZE; private @Nullable CacheKey mEncodedCacheKey; public EncodedImage(CloseableReference<PooledByteBuffer> pooledByteBufferRef) { Preconditions.checkArgument(CloseableReference.isValid(pooledByteBufferRef)); this.mPooledByteBufferRef = pooledByteBufferRef.clone(); this.mInputStreamSupplier = null; } public EncodedImage(Supplier<FileInputStream> inputStreamSupplier) { Preconditions.checkNotNull(inputStreamSupplier); this.mPooledByteBufferRef = null; this.mInputStreamSupplier = inputStreamSupplier; } public EncodedImage(Supplier<FileInputStream> inputStreamSupplier, int streamSize) { this(inputStreamSupplier); this.mStreamSize = streamSize; } /** * Returns the cloned encoded image if the parameter received is not null, null otherwise. * * @param encodedImage the EncodedImage to clone */ public static EncodedImage cloneOrNull(EncodedImage encodedImage) { return encodedImage != null ? encodedImage.cloneOrNull() : null; } public EncodedImage cloneOrNull() { EncodedImage encodedImage; if (mInputStreamSupplier != null) { encodedImage = new EncodedImage(mInputStreamSupplier, mStreamSize); } else { CloseableReference<PooledByteBuffer> pooledByteBufferRef = CloseableReference.cloneOrNull(mPooledByteBufferRef); try { encodedImage = (pooledByteBufferRef == null) ? null : new EncodedImage(pooledByteBufferRef); } finally { // Close the recently created reference since it will be cloned again in the constructor. CloseableReference.closeSafely(pooledByteBufferRef); } } if (encodedImage != null) { encodedImage.copyMetaDataFrom(this); } return encodedImage; } /** * Closes the buffer enclosed by this class. */ @Override public void close() { CloseableReference.closeSafely(mPooledByteBufferRef); } /** * Returns true if the internal buffer reference is valid or the InputStream Supplier is not null, * false otherwise. */ public synchronized boolean isValid() { return CloseableReference.isValid(mPooledByteBufferRef) || mInputStreamSupplier != null; } /** * Returns a cloned reference to the stored encoded bytes. * * <p>The caller has to close the reference once it has finished using it. */ public CloseableReference<PooledByteBuffer> getByteBufferRef() { return CloseableReference.cloneOrNull(mPooledByteBufferRef); } /** * Returns an InputStream from the internal InputStream Supplier if it's not null. Otherwise * returns an InputStream for the internal buffer reference if valid and null otherwise. * * <p>The caller has to close the InputStream after using it. */ public InputStream getInputStream() { if (mInputStreamSupplier != null) { return mInputStreamSupplier.get(); } CloseableReference<PooledByteBuffer> pooledByteBufferRef = CloseableReference.cloneOrNull(mPooledByteBufferRef); if (pooledByteBufferRef != null) { try { return new PooledByteBufferInputStream(pooledByteBufferRef.get()); } finally { CloseableReference.closeSafely(pooledByteBufferRef); } } return null; } /** * Sets the image format */ public void setImageFormat(ImageFormat imageFormat) { this.mImageFormat = imageFormat; } /** * Sets the image height */ public void setHeight(int height) { this.mHeight = height; } /** * Sets the image width */ public void setWidth(int width) { this.mWidth = width; } /** * Sets the image rotation angle */ public void setRotationAngle(int rotationAngle) { this.mRotationAngle = rotationAngle; } /** * Sets the image sample size */ public void setSampleSize(int sampleSize) { this.mSampleSize = sampleSize; } /** * Sets the size of an image if backed by an InputStream * * <p> Ignored if backed by a ByteBuffer */ public void setStreamSize(int streamSize) { this.mStreamSize = streamSize; } /** * Sets the key related to this image for encoded caches * @param encodedCacheKey */ @Deprecated public void setEncodedCacheKey(@Nullable CacheKey encodedCacheKey) { mEncodedCacheKey = encodedCacheKey; } /** * Returns the image format if known, otherwise ImageFormat.UNKNOWN. */ public ImageFormat getImageFormat() { return mImageFormat; } /** * Only valid if the image format is JPEG. * @return the rotation angle if the rotation angle is known, else -1. The rotation angle may not * be known if the image is incomplete (e.g. for progressive JPEGs). */ public int getRotationAngle() { return mRotationAngle; } /** * Only valid if the image format is JPEG. * @return width if the width is known, else -1. */ public int getWidth() { return mWidth; } /** * Only valid if the image format is JPEG. * @return height if the height is known, else -1. */ public int getHeight() { return mHeight; } /** * Only valid if the image format is JPEG. * @return sample size of the image. */ public int getSampleSize() { return mSampleSize; } /** * Gets the key to use when storing this image in encoded caches * @return the encoded cache key */ @Nullable @Deprecated public CacheKey getEncodedCacheKey() { return mEncodedCacheKey; } /** * Returns true if the image is a JPEG and its data is already complete at the specified length, * false otherwise. */ public boolean isCompleteAt(int length) { if (mImageFormat != DefaultImageFormats.JPEG) { return true; } // If the image is backed by FileInputStreams return true since they will always be complete. if (mInputStreamSupplier != null) { return true; } // The image should be backed by a ByteBuffer Preconditions.checkNotNull(mPooledByteBufferRef); PooledByteBuffer buf = mPooledByteBufferRef.get(); return (buf.read(length - 2) == (byte) JfifUtil.MARKER_FIRST_BYTE) && (buf.read(length - 1) == (byte) JfifUtil.MARKER_EOI); } /** * Returns the size of the backing structure. * * <p> If it's a PooledByteBuffer returns its size if its not null, -1 otherwise. If it's an * InputStream, return the size if it was set, -1 otherwise. */ public int getSize() { if (mPooledByteBufferRef != null && mPooledByteBufferRef.get() != null) { return mPooledByteBufferRef.get().size(); } return mStreamSize; } /** * Sets the encoded image meta data. */ public void parseMetaData() { final ImageFormat imageFormat = ImageFormatChecker.getImageFormat_WrapIOException( getInputStream()); mImageFormat = imageFormat; // BitmapUtil.decodeDimensions has a bug where it will return 100x100 for some WebPs even though // those are not its actual dimensions final Pair<Integer, Integer> dimensions; if (DefaultImageFormats.isWebpFormat(imageFormat)) { dimensions = readWebPImageSize(); } else { dimensions = readImageSize(); } if (imageFormat == DefaultImageFormats.JPEG && mRotationAngle == UNKNOWN_ROTATION_ANGLE) { // Load the JPEG rotation angle only if we have the dimensions if (dimensions != null) { mRotationAngle = JfifUtil.getAutoRotateAngleFromOrientation( JfifUtil.getOrientation(getInputStream())); } } else { mRotationAngle = 0; } } /** * We get the size from a WebP image */ private Pair<Integer, Integer> readWebPImageSize() { final Pair<Integer, Integer> dimensions = WebpUtil.getSize(getInputStream()); if (dimensions != null) { mWidth = dimensions.first; mHeight = dimensions.second; } return dimensions; } /** * We get the size from a generic image */ private Pair<Integer, Integer> readImageSize() { InputStream inputStream = null; Pair<Integer, Integer> dimensions = null; try { inputStream = getInputStream(); dimensions = BitmapUtil.decodeDimensions(inputStream); if (dimensions != null) { mWidth = dimensions.first; mHeight = dimensions.second; } }finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { // Head in the sand } } } return dimensions; } /** * Copy the meta data from another EncodedImage. * * @param encodedImage the EncodedImage to copy the meta data from. */ public void copyMetaDataFrom(EncodedImage encodedImage) { mImageFormat = encodedImage.getImageFormat(); mWidth = encodedImage.getWidth(); mHeight = encodedImage.getHeight(); mRotationAngle = encodedImage.getRotationAngle(); mSampleSize = encodedImage.getSampleSize(); mStreamSize = encodedImage.getSize(); mEncodedCacheKey = encodedImage.getEncodedCacheKey(); } /** * Returns true if all the image information has loaded, false otherwise. */ public static boolean isMetaDataAvailable(EncodedImage encodedImage) { return encodedImage.mRotationAngle >= 0 && encodedImage.mWidth >= 0 && encodedImage.mHeight >= 0; } /** * Closes the encoded image handling null. * * @param encodedImage the encoded image to close. */ public static void closeSafely(@Nullable EncodedImage encodedImage) { if (encodedImage != null) { encodedImage.close(); } } /** * Checks if the encoded image is valid i.e. is not null, and is not closed. * @return true if the encoded image is valid */ public static boolean isValid(@Nullable EncodedImage encodedImage) { return encodedImage != null && encodedImage.isValid(); } /** * A test-only method to get the underlying references. * * <p><b>DO NOT USE in application code.</b> */ @VisibleForTesting public synchronized SharedReference<PooledByteBuffer> getUnderlyingReferenceTestOnly() { return (mPooledByteBufferRef != null) ? mPooledByteBufferRef.getUnderlyingReferenceTestOnly() : null; } }