blob: 02112507997c4d23ca687dfd465e05a0e25aa1aa [file] [log] [blame]
/*
* Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jp.tomorrowkey.android.gifplayer;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
/**
* A base GIF Drawable with support for animations.
*
* Inspired by http://code.google.com/p/android-gifview/
*/
public class BaseGifDrawable extends Drawable implements Runnable, Animatable,
android.os.Handler.Callback {
private static final String TAG = "GifDrawable";
// Max decoder pixel stack size
private static final int MAX_STACK_SIZE = 4096;
private static final int MAX_BITS = 4097;
// Frame disposal methods
private static final int DISPOSAL_METHOD_UNKNOWN = 0;
private static final int DISPOSAL_METHOD_LEAVE = 1;
private static final int DISPOSAL_METHOD_BACKGROUND = 2;
private static final int DISPOSAL_METHOD_RESTORE = 3;
// Message types
private static final int READ_FRAME_REQ = 10;
private static final int READ_FRAME_RESP = 11;
private static final int RESET_DECODER = 12;
// Specifies the minimum amount of time before a subsequent frame will be rendered.
private static final int MIN_FRAME_SCHEDULE_DELAY_MS = 5;
private static final byte[] NETSCAPE2_0 = "NETSCAPE2.0".getBytes();
private static Paint sPaint;
private static Paint sScalePaint;
protected final BaseGifImage mGifImage;
private final byte[] mData;
private int mPosition;
protected int mIntrinsicWidth;
protected int mIntrinsicHeight;
private int mWidth;
private int mHeight;
protected Bitmap mBitmap;
protected int[] mColors;
private boolean mScale;
private float mScaleFactor;
// The following are marked volatile because they are read/written in the background decoder
// thread and read from the UI thread. No further synchronization is needed because their
// values will only ever change from at most once, and it is safe to lazily detect the change
// in the UI thread.
private volatile boolean mError;
private volatile boolean mDone;
private volatile boolean mAnimateOnLoad = true;
private int mBackgroundColor;
private boolean mLocalColorTableUsed;
private int mLocalColorTableSize;
private int[] mLocalColorTable;
private int[] mActiveColorTable;
private boolean mInterlace;
// Each frame specifies a sub-region of the image that should be updated. The values are
// clamped to the GIF dimensions if they exceed the intrinsic dimensions.
private int mFrameX, mFrameY, mFrameWidth, mFrameHeight;
// This specifies the width of the actual data within a GIF frame. It will be equal to
// mFrameWidth unless the frame sub-region was clamped to prevent exceeding the intrinsic
// dimensions.
private int mFrameStep;
private byte[] mBlock = new byte[256];
private int mDisposalMethod = DISPOSAL_METHOD_BACKGROUND;
private boolean mTransparency;
private int mTransparentColorIndex;
// LZW decoder working arrays
private short[] mPrefix = new short[MAX_STACK_SIZE];
private byte[] mSuffix = new byte[MAX_STACK_SIZE];
private byte[] mPixelStack = new byte[MAX_STACK_SIZE + 1];
private byte[] mPixels;
private boolean mBackupSaved;
private int[] mBackup;
private int mFrameCount;
private long mLastFrameTime;
private boolean mRunning;
protected int mFrameDelay;
private int mNextFrameDelay;
protected boolean mScheduled;
private boolean mAnimationEnabled = true;
private final Handler mHandler = new Handler(Looper.getMainLooper(), this);
private static DecoderThread sDecoderThread;
private static Handler sDecoderHandler;
private boolean mRecycled;
protected boolean mFirstFrameReady;
private boolean mEndOfFile;
private int mLoopCount = 0; // 0 to repeat endlessly.
private int mLoopIndex = 0;
private final Bitmap.Config mBitmapConfig;
private boolean mFirstFrame = true;
public BaseGifDrawable(BaseGifImage gifImage, Bitmap.Config bitmapConfig) {
this.mBitmapConfig = bitmapConfig;
// Create the background decoder thread, if necessary.
if (sDecoderThread == null) {
sDecoderThread = new DecoderThread();
sDecoderThread.start();
sDecoderHandler = new Handler(sDecoderThread.getLooper(), sDecoderThread);
}
if (sPaint == null) {
sPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
sScalePaint = new Paint(Paint.FILTER_BITMAP_FLAG);
sScalePaint.setFilterBitmap(true);
}
mGifImage = gifImage;
mData = gifImage.getData();
mPosition = mGifImage.mHeaderSize;
mFrameWidth = mFrameStep = mIntrinsicWidth = gifImage.getWidth();
mFrameHeight = mIntrinsicHeight = gifImage.getHeight();
mBackgroundColor = mGifImage.mBackgroundColor;
mError = mGifImage.mError;
if (!mError) {
try {
mBitmap = Bitmap.createBitmap(mIntrinsicWidth, mIntrinsicHeight, mBitmapConfig);
if (mBitmap == null) {
throw new OutOfMemoryError("Cannot allocate bitmap");
}
int pixelCount = mIntrinsicWidth * mIntrinsicHeight;
mColors = new int[pixelCount];
mPixels = new byte[pixelCount];
mWidth = mIntrinsicHeight;
mHeight = mIntrinsicHeight;
// Read the first frame
sDecoderHandler.sendMessage(sDecoderHandler.obtainMessage(READ_FRAME_REQ, this));
} catch (OutOfMemoryError e) {
mError = true;
}
}
}
/**
* Sets the loop count for multi-frame animation.
*/
public void setLoopCount(int loopCount) {
mLoopCount = loopCount;
}
/**
* Returns the loop count for multi-frame animation.
*/
public int getLoopCount() {
return mLoopCount;
}
/**
* Sets whether to start animation on load or not.
*/
public void setAnimateOnLoad(boolean animateOnLoad) {
mAnimateOnLoad = animateOnLoad;
}
/**
* Returns {@code true} if the GIF is valid and {@code false} otherwise.
*/
public boolean isValid() {
return !mError && mFirstFrameReady;
}
public void onRecycle() {
if (mBitmap != null) {
mBitmap.recycle();
}
mBitmap = null;
mRecycled = true;
}
/**
* Enables or disables the GIF from animating. GIF animations are enabled by default.
*/
public void setAnimationEnabled(boolean animationEnabled) {
if (mAnimationEnabled == animationEnabled) {
return;
}
mAnimationEnabled = animationEnabled;
if (mAnimationEnabled) {
start();
} else {
stop();
}
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mWidth = bounds.width();
mHeight = bounds.height();
mScale = mWidth != mIntrinsicWidth && mHeight != mIntrinsicHeight;
if (mScale) {
mScaleFactor = Math.max((float) mWidth / mIntrinsicWidth,
(float) mHeight / mIntrinsicHeight);
}
if (!mError && !mRecycled) {
// Request that the decoder reset itself
sDecoderHandler.sendMessage(sDecoderHandler.obtainMessage(RESET_DECODER, this));
}
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean changed = super.setVisible(visible, restart);
if (visible) {
if (changed || restart) {
start();
}
} else {
stop();
}
return changed;
}
@Override
public void draw(Canvas canvas) {
if (mError || mWidth == 0 || mHeight == 0 || mRecycled || !mFirstFrameReady) {
return;
}
if (mScale) {
canvas.save();
canvas.scale(mScaleFactor, mScaleFactor, 0, 0);
canvas.drawBitmap(mBitmap, 0, 0, sScalePaint);
canvas.restore();
} else {
canvas.drawBitmap(mBitmap, 0, 0, sPaint);
}
if (mRunning) {
if (!mScheduled) {
// Schedule the next frame at mFrameDelay milliseconds from the previous frame or
// the minimum sceduling delay from now, whichever is later.
mLastFrameTime = Math.max(
mLastFrameTime + mFrameDelay,
SystemClock.uptimeMillis() + MIN_FRAME_SCHEDULE_DELAY_MS);
scheduleSelf(this, mLastFrameTime);
}
} else if (!mDone) {
start();
} else {
unscheduleSelf(this);
}
}
@Override
public int getIntrinsicWidth() {
return mIntrinsicWidth;
}
@Override
public int getIntrinsicHeight() {
return mIntrinsicHeight;
}
@Override
public int getOpacity() {
return PixelFormat.UNKNOWN;
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter cf) {
}
@Override
public boolean isRunning() {
return mRunning;
}
@Override
public void start() {
if (!isRunning()) {
mRunning = true;
if (!mAnimateOnLoad) {
mDone = true;
}
mLastFrameTime = SystemClock.uptimeMillis();
run();
}
}
@Override
public void stop() {
if (isRunning()) {
unscheduleSelf(this);
}
}
@Override
public void scheduleSelf(Runnable what, long when) {
if (mAnimationEnabled) {
super.scheduleSelf(what, when);
mScheduled = true;
}
}
@Override
public void unscheduleSelf(Runnable what) {
super.unscheduleSelf(what);
mRunning = false;
}
/**
* Moves to the next frame.
*/
@Override
public void run() {
if (mRecycled) {
return;
}
// Send request to decoder to read the next frame
if (!mDone) {
sDecoderHandler.sendMessage(sDecoderHandler.obtainMessage(READ_FRAME_REQ, this));
}
}
/**
* Restarts decoding the image from the beginning. Called from the background thread.
*/
private void reset() {
// Return to the position of the first image frame in the stream.
mPosition = mGifImage.mHeaderSize;
mBackupSaved = false;
mFrameCount = 0;
mDisposalMethod = DISPOSAL_METHOD_UNKNOWN;
}
/**
* Restarts animation if a limited number of loops of animation have been previously done.
*/
public void restartAnimation() {
if (mDone && mLoopCount > 0) {
reset();
mDone = false;
mLoopIndex = 0;
run();
}
}
/**
* Reads color table as 256 RGB integer values. Called from the background thread.
*
* @param ncolors int number of colors to read
*/
private void readColorTable(int[] colorTable, int ncolors) {
for (int i = 0; i < ncolors; i++) {
int r = mData[mPosition++] & 0xff;
int g = mData[mPosition++] & 0xff;
int b = mData[mPosition++] & 0xff;
colorTable[i] = 0xff000000 | (r << 16) | (g << 8) | b;
}
}
/**
* Reads GIF content blocks. Called from the background thread.
*
* @return true if the next frame has been parsed successfully, false if EOF
* has been reached
*/
private void readNextFrame() {
// Don't clear the image if it is a terminator.
if ((mData[mPosition] & 0xff) == 0x3b) {
mEndOfFile = true;
return;
}
disposeOfLastFrame();
mDisposalMethod = DISPOSAL_METHOD_UNKNOWN;
mTransparency = false;
mEndOfFile = false;
mNextFrameDelay = 100;
mLocalColorTable = null;
while (true) {
int code = mData[mPosition++] & 0xff;
switch (code) {
case 0: // Empty block, ignore
break;
case 0x21: // Extension. Extensions precede the corresponding image.
code = mData[mPosition++] & 0xff;
switch (code) {
case 0xf9: // graphics control extension
readGraphicControlExt();
break;
case 0xff: // application extension
readBlock();
boolean netscape = true;
for (int i = 0; i < NETSCAPE2_0.length; i++) {
if (mBlock[i] != NETSCAPE2_0[i]) {
netscape = false;
break;
}
}
if (netscape) {
readNetscapeExtension();
} else {
skip(); // don't care
}
break;
case 0xfe:// comment extension
skip();
break;
case 0x01:// plain text extension
skip();
break;
default: // uninteresting extension
skip();
}
break;
case 0x2C: // Image separator
readBitmap();
return;
case 0x3b: // Terminator
mEndOfFile = true;
return;
default: // We don't know what this is. Just skip it.
break;
}
}
}
/**
* Disposes of the previous frame. Called from the background thread.
*/
private void disposeOfLastFrame() {
if (mFirstFrame) {
mFirstFrame = false;
return;
}
switch (mDisposalMethod) {
case DISPOSAL_METHOD_UNKNOWN:
case DISPOSAL_METHOD_LEAVE: {
mBackupSaved = false;
break;
}
case DISPOSAL_METHOD_RESTORE: {
if (mBackupSaved) {
System.arraycopy(mBackup, 0, mColors, 0, mBackup.length);
}
break;
}
case DISPOSAL_METHOD_BACKGROUND: {
mBackupSaved = false;
// Fill last image rect area with background color
int color = 0;
if (!mTransparency) {
color = mBackgroundColor;
}
for (int i = 0; i < mFrameHeight; i++) {
int n1 = (mFrameY + i) * mIntrinsicWidth + mFrameX;
int n2 = n1 + mFrameWidth;
for (int k = n1; k < n2; k++) {
mColors[k] = color;
}
}
break;
}
}
}
/**
* Reads Graphics Control Extension values. Called from the background thread.
*/
private void readGraphicControlExt() {
mPosition++; // Block size, fixed
int packed = mData[mPosition++] & 0xff; // Packed fields
mDisposalMethod = (packed & 0x1c) >> 2; // Disposal method
mTransparency = (packed & 1) != 0;
mNextFrameDelay = readShort() * 10; // Delay in milliseconds
// It seems that there are broken tools out there that set a 0ms or 10ms
// timeout when they really want a "default" one.
// Following WebKit's lead (http://trac.webkit.org/changeset/73295)
// we use 10 frames per second as the default frame rate.
if (mNextFrameDelay <= 10) {
mNextFrameDelay = 100;
}
mTransparentColorIndex = mData[mPosition++] & 0xff;
mPosition++; // Block terminator - ignore
}
/**
* Reads Netscape extension to obtain iteration count. Called from the background thread.
*/
private void readNetscapeExtension() {
int count;
do {
count = readBlock();
} while ((count > 0) && !mError);
}
/**
* Reads next frame image. Called from the background thread.
*/
private void readBitmap() {
mFrameX = readShort(); // (sub)image position & size
mFrameY = readShort();
int width = readShort();
int height = readShort();
// Clamp the frame dimensions to the intrinsic dimensions.
mFrameWidth = Math.min(width, mIntrinsicWidth - mFrameX);
mFrameHeight = Math.min(height, mIntrinsicHeight - mFrameY);
// The frame step is set to the specfied frame width before clamping.
mFrameStep = width;
// Increase the size of the decoding buffer if necessary.
int framePixelCount = width * height;
if (framePixelCount > mPixels.length) {
mPixels = new byte[framePixelCount];
}
int packed = mData[mPosition++] & 0xff;
// 3 - sort flag
// 4-5 - reserved lctSize = 2 << (packed & 7);
// 6-8 - local color table size
mInterlace = (packed & 0x40) != 0;
mLocalColorTableUsed = (packed & 0x80) != 0; // 1 - local color table flag interlace
mLocalColorTableSize = (int) Math.pow(2, (packed & 0x07) + 1);
if (mLocalColorTableUsed) {
if (mLocalColorTable == null) {
mLocalColorTable = new int[256];
}
readColorTable(mLocalColorTable, mLocalColorTableSize);
mActiveColorTable = mLocalColorTable;
} else {
mActiveColorTable = mGifImage.mGlobalColorTable;
if (mGifImage.mBackgroundIndex == mTransparentColorIndex) {
mBackgroundColor = 0;
}
}
int savedColor = 0;
if (mTransparency) {
savedColor = mActiveColorTable[mTransparentColorIndex];
mActiveColorTable[mTransparentColorIndex] = 0;
}
if (mActiveColorTable == null) {
mError = true;
}
if (mError) {
return;
}
decodeBitmapData();
skip();
if (mError) {
return;
}
if (mDisposalMethod == DISPOSAL_METHOD_RESTORE) {
backupFrame();
}
populateImageData();
if (mTransparency) {
mActiveColorTable[mTransparentColorIndex] = savedColor;
}
mFrameCount++;
}
/**
* Stores the relevant portion of the current frame so that it can be restored
* before the next frame is rendered. Called from the background thread.
*/
private void backupFrame() {
if (mBackupSaved) {
return;
}
if (mBackup == null) {
mBackup = null;
try {
mBackup = new int[mColors.length];
} catch (OutOfMemoryError e) {
Log.e(TAG, "GifDrawable.backupFrame threw an OOME", e);
}
}
if (mBackup != null) {
System.arraycopy(mColors, 0, mBackup, 0, mColors.length);
mBackupSaved = true;
}
}
/**
* Decodes LZW image data into pixel array. Called from the background thread.
*/
private void decodeBitmapData() {
int npix = mFrameWidth * mFrameHeight;
// Initialize GIF data stream decoder.
int dataSize = mData[mPosition++] & 0xff;
int clear = 1 << dataSize;
int endOfInformation = clear + 1;
int available = clear + 2;
int oldCode = -1;
int codeSize = dataSize + 1;
int codeMask = (1 << codeSize) - 1;
for (int code = 0; code < clear; code++) {
mPrefix[code] = 0; // XXX ArrayIndexOutOfBoundsException
mSuffix[code] = (byte) code;
}
// Decode GIF pixel stream.
int datum = 0;
int bits = 0;
int first = 0;
int top = 0;
int pi = 0;
while (pi < npix) {
int blockSize = mData[mPosition++] & 0xff;
if (blockSize == 0) {
break;
}
int blockEnd = mPosition + blockSize;
while (mPosition < blockEnd) {
datum += (mData[mPosition++] & 0xff) << bits;
bits += 8;
while (bits >= codeSize) {
// Get the next code.
int code = datum & codeMask;
datum >>= codeSize;
bits -= codeSize;
// Interpret the code
if (code == clear) {
// Reset decoder.
codeSize = dataSize + 1;
codeMask = (1 << codeSize) - 1;
available = clear + 2;
oldCode = -1;
continue;
}
// Check for explicit end-of-stream
if (code == endOfInformation) {
mPosition = blockEnd;
return;
}
if (oldCode == -1) {
mPixels[pi++] = mSuffix[code];
oldCode = code;
first = code;
continue;
}
int inCode = code;
if (code >= available) {
mPixelStack[top++] = (byte) first;
code = oldCode;
if (top == MAX_BITS) {
mError = true;
return;
}
}
while (code >= clear) {
if (code >= MAX_BITS || code == mPrefix[code]) {
mError = true;
return;
}
mPixelStack[top++] = mSuffix[code];
code = mPrefix[code];
if (top == MAX_BITS) {
mError = true;
return;
}
}
first = mSuffix[code];
mPixelStack[top++] = (byte) first;
// Add new code to the dictionary
if (available < MAX_STACK_SIZE) {
mPrefix[available] = (short) oldCode;
mSuffix[available] = (byte) first;
available++;
if (((available & codeMask) == 0) && (available < MAX_STACK_SIZE)) {
codeSize++;
codeMask += available;
}
}
oldCode = inCode;
// Drain the pixel stack.
do {
mPixels[pi++] = mPixelStack[--top];
} while (top > 0);
}
}
}
while (pi < npix) {
mPixels[pi++] = 0; // clear missing pixels
}
}
/**
* Populates the color array with pixels for the next frame.
*/
private void populateImageData() {
// Copy each source line to the appropriate place in the destination
int pass = 1;
int inc = 8;
int iline = 0;
for (int i = 0; i < mFrameHeight; i++) {
int line = i;
if (mInterlace) {
if (iline >= mFrameHeight) {
pass++;
switch (pass) {
case 2:
iline = 4;
break;
case 3:
iline = 2;
inc = 4;
break;
case 4:
iline = 1;
inc = 2;
break;
default:
break;
}
}
line = iline;
iline += inc;
}
line += mFrameY;
if (line < mIntrinsicHeight) {
int k = line * mIntrinsicWidth;
int dx = k + mFrameX; // start of line in dest
int dlim = dx + mFrameWidth; // end of dest line
// It is unnecesary to test if dlim is beyond the edge of the destination line,
// since mFrameWidth is clamped to a maximum of mIntrinsicWidth - mFrameX.
int sx = i * mFrameStep; // start of line in source
while (dx < dlim) {
// map color and insert in destination
int index = mPixels[sx++] & 0xff;
int c = mActiveColorTable[index];
if (c != 0) {
mColors[dx] = c;
}
dx++;
}
}
}
}
/**
* Reads next variable length block from input. Called from the background thread.
*
* @return number of bytes stored in "buffer"
*/
private int readBlock() {
int blockSize = mData[mPosition++] & 0xff;
if (blockSize > 0) {
System.arraycopy(mData, mPosition, mBlock, 0, blockSize);
mPosition += blockSize;
}
return blockSize;
}
/**
* Reads next 16-bit value, LSB first. Called from the background thread.
*/
private int readShort() {
// read 16-bit value, LSB first
int byte1 = mData[mPosition++] & 0xff;
int byte2 = mData[mPosition++] & 0xff;
return byte1 | (byte2 << 8);
}
/**
* Skips variable length blocks up to and including next zero length block.
* Called from the background thread.
*/
private void skip() {
int blockSize;
do {
blockSize = mData[mPosition++] & 0xff;
mPosition += blockSize;
} while (blockSize > 0);
}
@Override
public boolean handleMessage(Message msg) {
if (msg.what == BaseGifDrawable.READ_FRAME_RESP) {
mFrameDelay = msg.arg1;
if (mBitmap != null) {
mBitmap.setPixels(mColors, 0, mIntrinsicWidth,
0, 0, mIntrinsicWidth, mIntrinsicHeight);
postProcessFrame(mBitmap);
mFirstFrameReady = true;
mScheduled = false;
invalidateSelf();
}
return true;
}
return false;
}
/**
* Gives a subclass a chance to apply changes to the mutable bitmap
* before showing the frame.
*/
protected void postProcessFrame(Bitmap bitmap) {
}
/**
* Background thread that handles reading and decoding frames of GIF images.
*/
private static class DecoderThread extends HandlerThread
implements android.os.Handler.Callback {
private static final String DECODER_THREAD_NAME = "GifDecoder";
public DecoderThread() {
super(DECODER_THREAD_NAME);
}
@Override
public boolean handleMessage(Message msg) {
BaseGifDrawable gif = (BaseGifDrawable) msg.obj;
if (gif == null || gif.mBitmap == null || gif.mRecycled) {
return true;
}
switch (msg.what) {
case READ_FRAME_REQ:
// Processed on background thread
do {
try {
gif.readNextFrame();
} catch (ArrayIndexOutOfBoundsException e) {
gif.mEndOfFile = true;
}
// Check for EOF
if (gif.mEndOfFile) {
if (gif.mFrameCount == 0) {
// could not read first frame
gif.mError = true;
} else if (gif.mFrameCount > 1) {
if (gif.mLoopCount == 0 || ++gif.mLoopIndex < gif.mLoopCount) {
// Repeat the animation
gif.reset();
} else {
gif.mDone = true;
}
} else {
// Only one frame. Mark as done.
gif.mDone = true;
}
}
} while (gif.mEndOfFile && !gif.mError && !gif.mDone);
gif.mHandler.sendMessage(gif.mHandler.obtainMessage(READ_FRAME_RESP,
gif.mNextFrameDelay, 0));
return true;
case RESET_DECODER:
gif.reset();
return true;
}
return false;
}
}
}