package pl.droidsonroids.gif;
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RawRes;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.Locale;
import pl.droidsonroids.gif.annotations.Beta;
/**
* Lightweight version of {@link pl.droidsonroids.gif.GifDrawable} used to retrieve metadata of GIF only,
* without having to allocate the memory for its pixels.
*/
public class GifAnimationMetaData implements Serializable, Parcelable {
private static final long serialVersionUID = 5692363926580237325L;
private final int mLoopCount;
private final int mDuration;
private final int mHeight;
private final int mWidth;
private final int mImageCount;
private final long mPixelsBytesCount;
private final long mMetadataBytesCount;
/**
* Retrieves from resource.
*
* @param res Resources to read from
* @param id resource id
* @throws android.content.res.Resources.NotFoundException if the given ID does not exist.
* @throws java.io.IOException when opening failed
* @throws NullPointerException if res is null
*/
public GifAnimationMetaData(@NonNull Resources res, @RawRes @DrawableRes int id) throws Resources.NotFoundException, IOException {
this(res.openRawResourceFd(id));
}
/**
* Retrieves metadata from asset.
*
* @param assets AssetManager to read from
* @param assetName name of the asset
* @throws IOException when opening failed
* @throws NullPointerException if assets or assetName is null
*/
public GifAnimationMetaData(@NonNull AssetManager assets, @NonNull String assetName) throws IOException {
this(assets.openFd(assetName));
}
/**
* Constructs metadata from given file path.<br>
* Only metadata is read, no graphic data is decoded here.
* In practice can be called from main thread. However it will violate
* {@link android.os.StrictMode} policy if disk reads detection is enabled.<br>
*
* @param filePath path to the GIF file
* @throws IOException when opening failed
* @throws NullPointerException if filePath is null
*/
public GifAnimationMetaData(@NonNull String filePath) throws IOException {
this(new GifInfoHandle(filePath));
}
/**
* Equivalent to {@code} GifMetadata(file.getPath())}
*
* @param file the GIF file
* @throws IOException when opening failed
* @throws NullPointerException if file is null
*/
public GifAnimationMetaData(@NonNull File file) throws IOException {
this(file.getPath());
}
/**
* Retrieves metadata from InputStream.
* InputStream must support marking, IllegalArgumentException will be thrown otherwise.
*
* @param stream stream to read from
* @throws IOException when opening failed
* @throws IllegalArgumentException if stream does not support marking
* @throws NullPointerException if stream is null
*/
public GifAnimationMetaData(@NonNull InputStream stream) throws IOException {
this(new GifInfoHandle(stream));
}
/**
* Retrieves metadata from AssetFileDescriptor.
* Convenience wrapper for {@link GifAnimationMetaData#GifAnimationMetaData(FileDescriptor)}
*
* @param afd source
* @throws NullPointerException if afd is null
* @throws IOException when opening failed
*/
public GifAnimationMetaData(@NonNull AssetFileDescriptor afd) throws IOException {
this(new GifInfoHandle(afd));
}
/**
* Retrieves metadata from FileDescriptor
*
* @param fd source
* @throws IOException when opening failed
* @throws NullPointerException if fd is null
*/
public GifAnimationMetaData(@NonNull FileDescriptor fd) throws IOException {
this(new GifInfoHandle(fd));
}
/**
* Retrieves metadata from byte array.<br>
* It can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed.
*
* @param bytes raw GIF bytes
* @throws IOException if bytes does not contain valid GIF data
* @throws NullPointerException if bytes are null
*/
public GifAnimationMetaData(@NonNull byte[] bytes) throws IOException {
this(new GifInfoHandle(bytes));
}
/**
* Retrieves metadata from {@link ByteBuffer}. Only direct buffers are supported.
* Buffer can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed.
*
* @param buffer buffer containing GIF data
* @throws IOException if buffer does not contain valid GIF data or is indirect
* @throws NullPointerException if buffer is null
*/
public GifAnimationMetaData(@NonNull ByteBuffer buffer) throws IOException {
this(new GifInfoHandle(buffer));
}
/**
* Retrieves metadata from {@link android.net.Uri} which is resolved using {@code resolver}.
* {@link android.content.ContentResolver#openAssetFileDescriptor(android.net.Uri, String)}
* is used to open an Uri.
*
* @param uri GIF Uri, cannot be null.
* @param resolver resolver, null is allowed for file:// scheme Uris only
* @throws IOException if resolution fails or destination is not a GIF.
*/
public GifAnimationMetaData(@Nullable ContentResolver resolver, @NonNull Uri uri) throws IOException {
this(GifInfoHandle.openUri(resolver, uri));
}
private GifAnimationMetaData(final GifInfoHandle gifInfoHandle) {
mLoopCount = gifInfoHandle.getLoopCount();
mDuration = gifInfoHandle.getDuration();
mWidth = gifInfoHandle.getWidth();
mHeight = gifInfoHandle.getHeight();
mImageCount = gifInfoHandle.getNumberOfFrames();
mMetadataBytesCount = gifInfoHandle.getMetadataByteCount();
mPixelsBytesCount = gifInfoHandle.getAllocationByteCount();
gifInfoHandle.recycle();
}
/**
* @return width od the GIF canvas in pixels
*/
public int getWidth() {
return mWidth;
}
/**
* @return height od the GIF canvas in pixels
*/
public int getHeight() {
return mHeight;
}
/**
* @return number of frames in GIF, at least one
*/
public int getNumberOfFrames() {
return mImageCount;
}
/**
* See {@link GifDrawable#getLoopCount()}
*
* @return loop count, 0 means that animation is infinite
*/
public int getLoopCount() {
return mLoopCount;
}
/**
* See {@link GifDrawable#getDuration()}
*
* @return duration of of one loop the animation in milliseconds. Result is always multiple of 10.
*/
public int getDuration() {
return mDuration;
}
/**
* @return true if GIF is animated (has at least 2 frames and positive duration), false otherwise
*/
public boolean isAnimated() {
return mImageCount > 1 && mDuration > 0;
}
/**
* Like {@link GifDrawable#getAllocationByteCount()} but does not include memory needed for backing {@link android.graphics.Bitmap}.
* {@code Bitmap} in {@code GifDrawable} may be allocated at the time of creation or existing one may be reused if {@link GifDrawableBuilder#with(GifDrawable)}
* is used.
* This method assumes no subsampling (sample size = 1).<br>
* To calculate allocation byte count of {@link GifDrawable} created from the same input source {@link #getDrawableAllocationByteCount(GifDrawable, int)}
* can be used.
*
* @return possible size of the memory needed to store pixels excluding backing {@link android.graphics.Bitmap} and assuming no subsampling
*/
public long getAllocationByteCount() {
return mPixelsBytesCount;
}
/**
* Like {@link #getAllocationByteCount()} but includes also backing {@link android.graphics.Bitmap} and takes sample size into account.
*
* @param oldDrawable optional old drawable to be reused, pass {@code null} if there is no one
* @param sampleSize sample size, pass {@code 1} if not using subsampling
* @return possible size of the memory needed to store pixels
* @throws IllegalArgumentException if sample size out of range
*/
@Beta
public long getDrawableAllocationByteCount(@Nullable GifDrawable oldDrawable, @IntRange(from = 1, to = Character.MAX_VALUE) int sampleSize) {
if (sampleSize < 1 || sampleSize > Character.MAX_VALUE) {
throw new IllegalStateException("Sample size " + sampleSize + " out of range <1, " + Character.MAX_VALUE + ">");
}
final int sampleSizeFactor = sampleSize * sampleSize;
final long bufferSize;
if (oldDrawable != null && !oldDrawable.mBuffer.isRecycled()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
bufferSize = oldDrawable.mBuffer.getAllocationByteCount();
} else {
bufferSize = oldDrawable.getFrameByteCount();
}
} else {
bufferSize = (mWidth * mHeight * 4) / sampleSizeFactor;
}
return (mPixelsBytesCount / sampleSizeFactor) + bufferSize;
}
/**
* See{@link GifDrawable#getMetadataAllocationByteCount()}
*
* @return maximum possible size of the allocated memory needed to store metadata
*/
public long getMetadataAllocationByteCount() {
return mMetadataBytesCount;
}
@Override
public String toString() {
final String loopCount = mLoopCount == 0 ? "Infinity" : Integer.toString(mLoopCount);
final String suffix = String.format(Locale.ENGLISH, "GIF: size: %dx%d, frames: %d, loops: %s, duration: %d", mWidth, mHeight, mImageCount, loopCount, mDuration);
return isAnimated() ? "Animated " + suffix : suffix;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mLoopCount);
dest.writeInt(mDuration);
dest.writeInt(mHeight);
dest.writeInt(mWidth);
dest.writeInt(mImageCount);
dest.writeLong(mMetadataBytesCount);
dest.writeLong(mPixelsBytesCount);
}
private GifAnimationMetaData(Parcel in) {
mLoopCount = in.readInt();
mDuration = in.readInt();
mHeight = in.readInt();
mWidth = in.readInt();
mImageCount = in.readInt();
mMetadataBytesCount = in.readLong();
mPixelsBytesCount = in.readLong();
}
public static final Parcelable.Creator<GifAnimationMetaData> CREATOR = new Parcelable.Creator<GifAnimationMetaData>() {
public GifAnimationMetaData createFromParcel(Parcel source) {
return new GifAnimationMetaData(source);
}
public GifAnimationMetaData[] newArray(int size) {
return new GifAnimationMetaData[size];
}
};
}