/**
* Copyright (c) 2013 Xcellent Creations, Inc.
* Copyright 2014 Google, Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/package com.lenovo.cava.widget.GifImageView;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
importstatic com.lenovo.cava.utils.Log.TAG_WITH_CLASS_NAME;
importstatic com.lenovo.cava.utils.Log.TAG_CAVA;
/**
* Reads frame data from a GIF image source and decodes it into individual frames
* for animation purposes. Image data can be read from either and InputStream source
* or a byte[].
*
* This class is optimized for running animations with the frames, there
* are no methods to get individual frame images, only to decode the next frame in the
* animation sequence. Instead, it lowers its memory footprint by only housing the minimum
* data necessary to decode the next frame in the animation sequence.
*
* The animation must be manually moved forward using {@link #advance()} before requesting the next
* frame. This method must also be called before you request the first frame or an error will
* occur.
*
* Implementation adapted from sample code published in Lyons. (2004). <em>Java for Programmers</em>,
* republished under the MIT Open Source License
*/
class GifDecoder {
privatestaticfinal String TAG = TAG_WITH_CLASS_NAME ? "GifDecoder" : TAG_CAVA;
/**
* File read status: No errors.
*/staticfinalint STATUS_OK = 0;
/**
* File read status: Error decoding file (may be partially decoded).
*/staticfinalint STATUS_FORMAT_ERROR = 1;
/**
* File read status: Unable to open source.
*/staticfinalint STATUS_OPEN_ERROR = 2;
/**
* Unable to fully decode the current frame.
*/staticfinalint STATUS_PARTIAL_DECODE = 3;
/**
* max decoder pixel stack size.
*/privatestaticfinalint MAX_STACK_SIZE = 4096;
/**
* GIF Disposal Method meaning take no action.
*/privatestaticfinalint DISPOSAL_UNSPECIFIED = 0;
/**
* GIF Disposal Method meaning leave canvas from previous frame.
*/privatestaticfinalint DISPOSAL_NONE = 1;
/**
* GIF Disposal Method meaning clear canvas to background color.
*/privatestaticfinalint DISPOSAL_BACKGROUND = 2;
/**
* GIF Disposal Method meaning clear canvas to frame before last.
*/privatestaticfinalint DISPOSAL_PREVIOUS = 3;
privatestaticfinalint NULL_CODE = -1;
privatestaticfinalint INITIAL_FRAME_POINTER = -1;
staticfinalint LOOP_FOREVER = -1;
privatestaticfinalint BYTES_PER_INTEGER = 4;
// Global File Header values and parsing flags.// Active color table.privateint[] act;
// Private color table that can be modified if needed.privatefinalint[] pct = newint[256];
// Raw GIF data from input source.private ByteBuffer rawData;
// Raw data read working array.privatebyte[] block;
// Temporary buffer for block reading. Reads 16k chunks from the native buffer for processing,// to greatly reduce JNI overhead.privatestaticfinalint WORK_BUFFER_SIZE = 16384;
@Nullableprivatebyte[] workBuffer;
privateint workBufferSize = 0;
privateint workBufferPosition = 0;
private GifHeaderParser parser;
// LZW decoder working arrays.privateshort[] prefix;
privatebyte[] suffix;
privatebyte[] pixelStack;
privatebyte[] mainPixels;
privateint[] mainScratch;
privateint framePointer;
privateint loopIndex;
private GifHeader header;
private BitmapProvider bitmapProvider;
private Bitmap previousImage;
privateboolean savePrevious;
privateint status;
privateint sampleSize;
privateint downsampledHeight;
privateint downsampledWidth;
privateboolean isFirstFrameTransparent;
/**
* An interface that can be used to provide reused {@link Bitmap}s to avoid GCs
* from constantly allocating {@link Bitmap}s for every frame.
*/
interface BitmapProvider {
/**
* Returns an {@link Bitmap} with exactly the given dimensions and config.
*
* @param width The width in pixels of the desired {@link Bitmap}.
* @param height The height in pixels of the desired {@link Bitmap}.
* @param config The {@link Bitmap.Config} of the desired {@link
* Bitmap}.
*/@NonNull
Bitmap obtain(int width, int height, Bitmap.Config config);
/**
* Releases the given Bitmap back to the pool.
*/void release(Bitmap bitmap);
/**
* Returns a byte array used for decoding and generating the frame bitmap.
*
* @param size the size of the byte array to obtain
*/byte[] obtainByteArray(int size);
/**
* Releases the given byte array back to the pool.
*/void release(byte[] bytes);
/**
* Returns an int array used for decoding/generating the frame bitmaps.
* @param size
*/int[] obtainIntArray(int size);
/**
* Release the given array back to the pool.
* @param array
*/void release(int[] array);
}
GifDecoder(BitmapProvider provider, GifHeader gifHeader, ByteBuffer rawData) {
this(provider, gifHeader, rawData, 1/*sampleSize*/);
}
GifDecoder(BitmapProvider provider, GifHeader gifHeader, ByteBuffer rawData,
int sampleSize) {
this(provider);
setData(gifHeader, rawData, sampleSize);
}
GifDecoder(BitmapProvider provider) {
this.bitmapProvider = provider;
header = new GifHeader();
}
GifDecoder() {
this(new SimpleBitmapProvider());
}
int getWidth() {
return header.width;
}
int getHeight() {
return header.height;
}
ByteBuffer getData() {
return rawData;
}
/**
* Returns the current status of the decoder.
*
* <p> Status will update per frame to allow the caller to tell whether or not the current frame
* was decoded successfully and/or completely. Format and open failures persist across frames.
* </p>
*/int getStatus() {
return status;
}
/**
* Move the animation frame counter forward.
*
* @return boolean specifying if animation should continue or if loopCount has been fulfilled.
*/boolean advance() {
if (header.frameCount <= 0) {
returnfalse;
}
if(framePointer == getFrameCount() - 1) {
loopIndex++;
}
if(header.loopCount != LOOP_FOREVER && loopIndex > header.loopCount) {
returnfalse;
}
framePointer = (framePointer + 1) % header.frameCount;
returntrue;
}
/**
* Gets display duration for specified frame.
*
* @param n int index of frame.
* @return delay in milliseconds.
*/int getDelay(int n) {
int delay = -1;
if ((n >= 0) && (n < header.frameCount)) {
delay = header.frames.get(n).delay;
}
return delay;
}
/**
* Gets display duration for the upcoming frame in ms.
*/int getNextDelay() {
if (header.frameCount <= 0 || framePointer < 0) {
return0;
}
return getDelay(framePointer);
}
/**
* Gets the number of frames read from file.
*
* @return frame count.
*/int getFrameCount() {
return header.frameCount;
}
/**
* Gets the current index of the animation frame, or -1 if animation hasn't not yet started.
*
* @return frame index.
*/int getCurrentFrameIndex() {
return framePointer;
}
/**
* Sets the frame pointer to a specific frame
*
* @return boolean true if the move was successful
*/boolean setFrameIndex(int frame) {
if(frame < INITIAL_FRAME_POINTER || frame >= getFrameCount()) {
returnfalse;
}
framePointer = frame;
returntrue;
}
/**
* Resets the frame pointer to before the 0th frame, as if we'd never used this decoder to
* decode any frames.
*/void resetFrameIndex() {
framePointer = INITIAL_FRAME_POINTER;
}
/**
* Resets the loop index to the first loop.
*/void resetLoopIndex() { loopIndex = 0; }
/**
* Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitely.
*
* @return iteration count if one was specified, else 1.
*/int getLoopCount() { return header.loopCount; }
/**
* Gets the number of loops that have been shown.
*
* @return iteration count.
*/int getLoopIndex() {
return loopIndex;
}
/**
* Returns an estimated byte size for this decoder based on the data provided to {@link
* #setData(GifHeader, byte[])}, as well as internal buffers.
*/int getByteSize() {
return rawData.limit() + mainPixels.length + (mainScratch.length * BYTES_PER_INTEGER);
}
/**
* Get the next frame in the animation sequence.
*
* @return Bitmap representation of frame.
*/synchronized Bitmap getNextFrame() {
if (header.frameCount <= 0 || framePointer < 0) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "unable to decode frame, frameCount=" + header.frameCount + " framePointer="
+ framePointer);
}
status = STATUS_FORMAT_ERROR;
}
if (status == STATUS_FORMAT_ERROR || status == STATUS_OPEN_ERROR) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unable to decode frame, status=" + status);
}
returnnull;
}
status = STATUS_OK;
GifFrame currentFrame = header.frames.get(framePointer);
GifFrame previousFrame = null;
int previousIndex = framePointer - 1;
if (previousIndex >= 0) {
previousFrame = header.frames.get(previousIndex);
}
// Set the appropriate color table.
act = currentFrame.lct != null ? currentFrame.lct : header.gct;
if (act == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "No Valid Color Table for frame #" + framePointer);
}
// No color table defined.
status = STATUS_FORMAT_ERROR;
returnnull;
}
// Reset the transparent pixel in the color tableif (currentFrame.transparency) {
// Prepare local copy of color table ("pct = act"), see #1068
System.arraycopy(act, 0, pct, 0, act.length);
// Forget about act reference from shared header object, use copied version
act = pct;
// Set transparent color if specified.
act[currentFrame.transIndex] = 0;
}
// Transfer pixel data to image.return setPixels(currentFrame, previousFrame);
}
/**
* Reads GIF image from stream.
*
* @param is containing GIF file.
* @return read status code (0 = no errors).
*/int read(InputStream is, int contentLength) {
if (is != null) {
try {
int capacity = (contentLength > 0) ? (contentLength + 4096) : 16384;
ByteArrayOutputStream buffer = new ByteArrayOutputStream(capacity);
int nRead;
byte[] data = newbyte[16384];
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
read(buffer.toByteArray());
} catch (IOException e) {
Log.w(TAG, "Error reading data from stream", e);
}
} else {
status = STATUS_OPEN_ERROR;
}
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
Log.w(TAG, "Error closing stream", e);
}
return status;
}
void clear() {
header = null;
if (mainPixels != null) {
bitmapProvider.release(mainPixels);
}
if (mainScratch != null) {
bitmapProvider.release(mainScratch);
}
if (previousImage != null) {
bitmapProvider.release(previousImage);
}
previousImage = null;
rawData = null;
isFirstFrameTransparent = false;
if (block != null) {
bitmapProvider.release(block);
}
if (workBuffer != null) {
bitmapProvider.release(workBuffer);
}
}
synchronizedvoid setData(GifHeader header, byte[] data) {
setData(header, ByteBuffer.wrap(data));
}
synchronizedvoid setData(GifHeader header, ByteBuffer buffer) {
setData(header, buffer, 1);
}
synchronizedvoid setData(GifHeader header, ByteBuffer buffer, int sampleSize) {
if (sampleSize <= 0) {
thrownew IllegalArgumentException("Sample size must be >=0, not: " + sampleSize);
}
// Make sure sample size is a power of 2.
sampleSize = Integer.highestOneBit(sampleSize);
this.status = STATUS_OK;
this.header = header;
isFirstFrameTransparent = false;
framePointer = INITIAL_FRAME_POINTER;
resetLoopIndex();
// Initialize the raw data buffer.
rawData = buffer.asReadOnlyBuffer();
rawData.position(0);
rawData.order(ByteOrder.LITTLE_ENDIAN);
// No point in specially saving an old frame if we're never going to use it.
savePrevious = false;
for (GifFrame frame : header.frames) {
if (frame.dispose == DISPOSAL_PREVIOUS) {
savePrevious = true;
break;
}
}
this.sampleSize = sampleSize;
downsampledWidth = header.width / sampleSize;
downsampledHeight = header.height / sampleSize;
// Now that we know the size, init scratch arrays.// TODO Find a way to avoid this entirely or at least downsample it (either should be possible).
mainPixels = bitmapProvider.obtainByteArray(header.width * header.height);
mainScratch = bitmapProvider.obtainIntArray(downsampledWidth * downsampledHeight);
}
private GifHeaderParser getHeaderParser() {
if (parser == null) {
parser = new GifHeaderParser();
}
return parser;
}
/**
* Reads GIF image from byte array.
*
* @param data containing GIF file.
* @return read status code (0 = no errors).
*/synchronizedint read(byte[] data) {
this.header = getHeaderParser().setData(data).parseHeader();
if (data != null) {
setData(header, data);
}
return status;
}
/**
* Creates new frame image from current data (and previous frames as specified by their
* disposition codes).
*/private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) {
// Final location of blended pixels.finalint[] dest = mainScratch;
// clear all pixels when meet first frameif (previousFrame == null) {
Arrays.fill(dest, 0);
}
// fill in starting image contents based on last image's dispose codeif (previousFrame != null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) {
// We don't need to do anything for DISPOSAL_NONE, if it has the correct pixels so will our// mainScratch and therefore so will our dest array.if (previousFrame.dispose == DISPOSAL_BACKGROUND) {
// Start with a canvas filled with the background colorint c = 0;
if (!currentFrame.transparency) {
c = header.bgColor;
if (currentFrame.lct != null && header.bgIndex == currentFrame.transIndex) {
c = 0;
}
} elseif (framePointer == 0) {
// TODO: We should check and see if all individual pixels are replaced. If they are, the// first frame isn't actually transparent. For now, it's simpler and safer to assume// drawing a transparent background means the GIF contains transparency.
isFirstFrameTransparent = true;
}
fillRect(dest, previousFrame, c);
} elseif (previousFrame.dispose == DISPOSAL_PREVIOUS) {
if (previousImage == null) {
fillRect(dest, previousFrame, 0);
} else {
// Start with the previous frameint downsampledIH = previousFrame.ih / sampleSize;
int downsampledIY = previousFrame.iy / sampleSize;
int downsampledIW = previousFrame.iw / sampleSize;
int downsampledIX = previousFrame.ix / sampleSize;
int topLeft = downsampledIY * downsampledWidth + downsampledIX;
previousImage.getPixels(dest, topLeft, downsampledWidth,
downsampledIX, downsampledIY, downsampledIW, downsampledIH);
}
}
}
// Decode pixels for this frame into the global pixels[] scratch.
decodeBitmapData(currentFrame);
int downsampledIH = currentFrame.ih / sampleSize;
int downsampledIY = currentFrame.iy / sampleSize;
int downsampledIW = currentFrame.iw / sampleSize;
int downsampledIX = currentFrame.ix / sampleSize;
// Copy each source line to the appropriate place in the destination.int pass = 1;
int inc = 8;
int iline = 0;
boolean isFirstFrame = framePointer == 0;
for (int i = 0; i < downsampledIH; i++) {
int line = i;
if (currentFrame.interlace) {
if (iline >= downsampledIH) {
pass++;
switch (pass) {
case2:
iline = 4;
break;
case3:
iline = 2;
inc = 4;
break;
case4:
iline = 1;
inc = 2;
break;
default:
break;
}
}
line = iline;
iline += inc;
}
line += downsampledIY;
if (line < downsampledHeight) {
int k = line * downsampledWidth;
// Start of line in dest.int dx = k + downsampledIX;
// End of dest line.int dlim = dx + downsampledIW;
if (k + downsampledWidth < dlim) {
// Past dest edge.
dlim = k + downsampledWidth;
}
// Start of line in source.int sx = i * sampleSize * currentFrame.iw;
int maxPositionInSource = sx + ((dlim - dx) * sampleSize);
while (dx < dlim) {
// Map color and insert in destination.int averageColor;
if (sampleSize == 1) {
int currentColorIndex = ((int) mainPixels[sx]) & 0x000000ff;
averageColor = act[currentColorIndex];
} else {
// TODO: This is substantially slower (up to 50ms per frame) than just grabbing the// current color index above, even with a sample size of 1.
averageColor = averageColorsNear(sx, maxPositionInSource, currentFrame.iw);
}
if (averageColor != 0) {
dest[dx] = averageColor;
} elseif (!isFirstFrameTransparent && isFirstFrame) {
isFirstFrameTransparent = true;
}
sx += sampleSize;
dx++;
}
}
}
// Copy pixels into previous imageif (savePrevious && (currentFrame.dispose == DISPOSAL_UNSPECIFIED
|| currentFrame.dispose == DISPOSAL_NONE)) {
if (previousImage == null) {
previousImage = getNextBitmap();
}
previousImage.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth,
downsampledHeight);
}
// Set pixels for current image.
Bitmap result = getNextBitmap();
result.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, downsampledHeight);
return result;
}
privatevoidfillRect(int[] dest, GifFrame frame, int bgColor) {
// The area used by the graphic must be restored to the background color.int downsampledIH = frame.ih / sampleSize;
int downsampledIY = frame.iy / sampleSize;
int downsampledIW = frame.iw / sampleSize;
int downsampledIX = frame.ix / sampleSize;
int topLeft = downsampledIY * downsampledWidth + downsampledIX;
int bottomLeft = topLeft + downsampledIH * downsampledWidth;
for (int left = topLeft; left < bottomLeft; left += downsampledWidth) {
int right = left + downsampledIW;
for (int pointer = left; pointer < right; pointer++) {
dest[pointer] = bgColor;
}
}
}
privateintaverageColorsNear(int positionInMainPixels, int maxPositionInMainPixels,
int currentFrameIw) {
int alphaSum = 0;
int redSum = 0;
int greenSum = 0;
int blueSum = 0;
int totalAdded = 0;
// Find the pixels in the current row.for (int i = positionInMainPixels;
i < positionInMainPixels + sampleSize && i < mainPixels.length
&& i < maxPositionInMainPixels; i++) {
int currentColorIndex = ((int) mainPixels[i]) & 0xff;
int currentColor = act[currentColorIndex];
if (currentColor != 0) {
alphaSum += currentColor >> 24 & 0x000000ff;
redSum += currentColor >> 16 & 0x000000ff;
greenSum += currentColor >> 8 & 0x000000ff;
blueSum += currentColor & 0x000000ff;
totalAdded++;
}
}
// Find the pixels in the next row.for (int i = positionInMainPixels + currentFrameIw;
i < positionInMainPixels + currentFrameIw + sampleSize && i < mainPixels.length
&& i < maxPositionInMainPixels; i++) {
int currentColorIndex = ((int) mainPixels[i]) & 0xff;
int currentColor = act[currentColorIndex];
if (currentColor != 0) {
alphaSum += currentColor >> 24 & 0x000000ff;
redSum += currentColor >> 16 & 0x000000ff;
greenSum += currentColor >> 8 & 0x000000ff;
blueSum += currentColor & 0x000000ff;
totalAdded++;
}
}
if (totalAdded == 0) {
return0;
} else {
return ((alphaSum / totalAdded) << 24)
| ((redSum / totalAdded) << 16)
| ((greenSum / totalAdded) << 8)
| (blueSum / totalAdded);
}
}
/**
* Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick.
*/privatevoiddecodeBitmapData(GifFrame frame) {
workBufferSize = 0;
workBufferPosition = 0;
if (frame != null) {
// Jump to the frame start position.
rawData.position(frame.bufferFrameStart);
}
int npix = (frame == null) ? header.width * header.height : frame.iw * frame.ih;
int available, clear, codeMask, codeSize, endOfInformation, inCode, oldCode, bits, code, count,
i, datum,
dataSize, first, top, bi, pi;
if (mainPixels == null || mainPixels.length < npix) {
// Allocate new pixel array.
mainPixels = bitmapProvider.obtainByteArray(npix);
}
if (prefix == null) {
prefix = newshort[MAX_STACK_SIZE];
}
if (suffix == null) {
suffix = newbyte[MAX_STACK_SIZE];
}
if (pixelStack == null) {
pixelStack = newbyte[MAX_STACK_SIZE + 1];
}
// Initialize GIF data stream decoder.
dataSize = readByte();
clear = 1 << dataSize;
endOfInformation = clear + 1;
available = clear + 2;
oldCode = NULL_CODE;
codeSize = dataSize + 1;
codeMask = (1 << codeSize) - 1;
for (code = 0; code < clear; code++) {
// XXX ArrayIndexOutOfBoundsException.
prefix[code] = 0;
suffix[code] = (byte) code;
}
// Decode GIF pixel stream.
datum = bits = count = first = top = pi = bi = 0;
for (i = 0; i < npix; ) {
// Load bytes until there are enough bits for a code.if (count == 0) {
// Read a new data block.
count = readBlock();
if (count <= 0) {
status = STATUS_PARTIAL_DECODE;
break;
}
bi = 0;
}
datum += (((int) block[bi]) & 0xff) << bits;
bits += 8;
bi++;
count--;
while (bits >= codeSize) {
// Get the next code.
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 = NULL_CODE;
continue;
}
if (code > available) {
status = STATUS_PARTIAL_DECODE;
break;
}
if (code == endOfInformation) {
break;
}
if (oldCode == NULL_CODE) {
pixelStack[top++] = suffix[code];
oldCode = code;
first = code;
continue;
}
inCode = code;
if (code >= available) {
pixelStack[top++] = (byte) first;
code = oldCode;
}
while (code >= clear) {
pixelStack[top++] = suffix[code];
code = prefix[code];
}
first = ((int) suffix[code]) & 0xff;
pixelStack[top++] = (byte) first;
// Add a new string to the string table.if (available < MAX_STACK_SIZE) {
prefix[available] = (short) oldCode;
suffix[available] = (byte) first;
available++;
if (((available & codeMask) == 0) && (available < MAX_STACK_SIZE)) {
codeSize++;
codeMask += available;
}
}
oldCode = inCode;
while (top > 0) {
// Pop a pixel off the pixel stack.
mainPixels[pi++] = pixelStack[--top];
i++;
}
}
}
// Clear missing pixels.for (i = pi; i < npix; i++) {
mainPixels[i] = 0;
}
}
/**
* Reads the next chunk for the intermediate work buffer.
*/privatevoidreadChunkIfNeeded() {
if (workBufferSize > workBufferPosition) {
return;
}
if (workBuffer == null) {
workBuffer = bitmapProvider.obtainByteArray(WORK_BUFFER_SIZE);
}
workBufferPosition = 0;
workBufferSize = Math.min(rawData.remaining(), WORK_BUFFER_SIZE);
rawData.get(workBuffer, 0, workBufferSize);
}
/**
* Reads a single byte from the input stream.
*/privateintreadByte() {
try {
readChunkIfNeeded();
return workBuffer[workBufferPosition++] & 0xFF;
} catch (Exception e) {
status = STATUS_FORMAT_ERROR;
return0;
}
}
/**
* Reads next variable length block from input.
*
* @return number of bytes stored in "buffer".
*/privateintreadBlock() {
int blockSize = readByte();
if (blockSize > 0) {
try {
if (block == null) {
block = bitmapProvider.obtainByteArray(255);
}
finalint remaining = workBufferSize - workBufferPosition;
if (remaining >= blockSize) {
// Block can be read from the current work buffer.
System.arraycopy(workBuffer, workBufferPosition, block, 0, blockSize);
workBufferPosition += blockSize;
} elseif (rawData.remaining() + remaining >= blockSize) {
// Block can be read in two passes.
System.arraycopy(workBuffer, workBufferPosition, block, 0, remaining);
workBufferPosition = workBufferSize;
readChunkIfNeeded();
finalint secondHalfRemaining = blockSize - remaining;
System.arraycopy(workBuffer, 0, block, remaining, secondHalfRemaining);
workBufferPosition += secondHalfRemaining;
} else {
status = STATUS_FORMAT_ERROR;
}
} catch (Exception e) {
Log.w(TAG, "Error Reading Block", e);
status = STATUS_FORMAT_ERROR;
}
}
return blockSize;
}
private Bitmap getNextBitmap() {
Bitmap.Config config = isFirstFrameTransparent
? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
Bitmap result = bitmapProvider.obtain(downsampledWidth, downsampledHeight, config);
setAlpha(result);
return result;
}
@TargetApi(12)
privatestaticvoidsetAlpha(Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= 12) {
bitmap.setHasAlpha(true);
}
}
}
GifFrame.java
/**
* Copyright 2014 Google, Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/package com.lenovo.cava.widget.GifImageView;
/**
* Inner model class housing metadata for each frame.
*/
class GifFrame {
int ix, iy, iw, ih;
/**
* Control Flag.
*/boolean interlace;
/**
* Control Flag.
*/boolean transparency;
/**
* Disposal Method.
*/int dispose;
/**
* Transparency Index.
*/int transIndex;
/**
* Delay, in ms, to next frame.
*/int delay;
/**
* Index in the raw buffer where we need to start reading to decode.
*/int bufferFrameStart;
/**
* Local Color Table.
*/int[] lct;
}
GifHeader.java
/**
* Copyright 2014 Google, Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/package com.lenovo.cava.widget.GifImageView;
import java.util.ArrayList;
import java.util.List;
/**
* A header object containing the number of frames in an animated GIF image as well as basic
* metadata like width and height that can be used to decode each individual frame of the GIF. Can
* be shared by one or more {@link GifDecoder}s to play the same animated GIF in multiple views.
*/publicclassGifHeader {int[] gct = null;
int status = GifDecoder.STATUS_OK;
int frameCount = 0;
GifFrame currentFrame;
List<GifFrame> frames = new ArrayList<>();
// Logical screen size.// Full image width.int width;
// Full image height.int height;
// 1 : global color table flag.boolean gctFlag;
// 2-4 : color resolution.// 5 : gct sort flag.// 6-8 : gct size.int gctSize;
// Background color index.int bgIndex;
// Pixel aspect ratio.int pixelAspect;
//TODO: this is set both during reading the header and while decoding frames...int bgColor;
int loopCount = 0;
publicintgetHeight() {
return height;
}
publicintgetWidth() {
return width;
}
publicintgetNumFrames() {
return frameCount;
}
/**
* Global status code of GIF data parsing.
*/publicintgetStatus() {
return status;
}
}
GifHeaderParser.java
/**
* Copyright 2014 Google, Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/package com.lenovo.cava.widget.GifImageView;
import android.util.Log;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
importstatic com.lenovo.cava.utils.Log.TAG_WITH_CLASS_NAME;
importstatic com.lenovo.cava.utils.Log.TAG_CAVA;
/**
* A class responsible for creating {@link GifHeader}s from data
* representing animated gifs.
*/publicclassGifHeaderParser {publicstaticfinal String TAG = TAG_WITH_CLASS_NAME ? "GifHeaderParser" : TAG_CAVA;
// The minimum frame delay in hundredths of a second.staticfinalint MIN_FRAME_DELAY = 2;
// The default frame delay in hundredths of a second for GIFs with frame delays less than the// minimum.staticfinalint DEFAULT_FRAME_DELAY = 10;
privatestaticfinalint MAX_BLOCK_SIZE = 256;
// Raw data read working array.privatefinalbyte[] block = newbyte[MAX_BLOCK_SIZE];
private ByteBuffer rawData;
private GifHeader header;
privateint blockSize = 0;
public GifHeaderParser setData(ByteBuffer data) {
reset();
rawData = data.asReadOnlyBuffer();
rawData.position(0);
rawData.order(ByteOrder.LITTLE_ENDIAN);
returnthis;
}
public GifHeaderParser setData(byte[] data) {
if (data != null) {
setData(ByteBuffer.wrap(data));
} else {
rawData = null;
header.status = GifDecoder.STATUS_OPEN_ERROR;
}
returnthis;
}
publicvoidclear() {
rawData = null;
header = null;
}
privatevoidreset() {
rawData = null;
Arrays.fill(block, (byte) 0);
header = new GifHeader();
blockSize = 0;
}
public GifHeader parseHeader() {
if (rawData == null) {
thrownew IllegalStateException("You must call setData() before parseHeader()");
}
if (err()) {
return header;
}
readHeader();
if (!err()) {
readContents();
if (header.frameCount < 0) {
header.status = GifDecoder.STATUS_FORMAT_ERROR;
}
}
return header;
}
/**
* Determines if the GIF is animated by trying to read in the first 2 frames
* This method reparses the data even if the header has already been read.
*/publicbooleanisAnimated() {
readHeader();
if (!err()) {
readContents(2/* maxFrames */);
}
return header.frameCount > 1;
}
/**
* Main file parser. Reads GIF content blocks.
*/privatevoidreadContents() {
readContents(Integer.MAX_VALUE /* maxFrames */);
}
/**
* Main file parser. Reads GIF content blocks. Stops after reading maxFrames
*/privatevoidreadContents(int maxFrames) {
// Read GIF file content blocks.boolean done = false;
while (!(done || err() || header.frameCount > maxFrames)) {
int code = read();
switch (code) {
// Image separator.case0x2C:
// The graphics control extension is optional, but will always come first if it exists.// If one did// exist, there will be a non-null current frame which we should use. However if one// did not exist,// the current frame will be null and we must create it here. See issue #134.if (header.currentFrame == null) {
header.currentFrame = new GifFrame();
}
readBitmap();
break;
// Extension.case0x21:
code = read();
switch (code) {
// Graphics control extension.case0xf9:
// Start a new frame.
header.currentFrame = new GifFrame();
readGraphicControlExt();
break;
// Application extension.case0xff:
readBlock();
String app = "";
for (int i = 0; i < 11; i++) {
app += (char) block[i];
}
if (app.equals("NETSCAPE2.0")) {
readNetscapeExt();
} else {
// Don't care.
skip();
}
break;
// Comment extension.case0xfe:
skip();
break;
// Plain text extension.case0x01:
skip();
break;
// Uninteresting extension.default:
skip();
}
break;
// Terminator.case0x3b:
done = true;
break;
// Bad byte, but keep going and see what happens break;case0x00:
default:
header.status = GifDecoder.STATUS_FORMAT_ERROR;
}
}
}
/**
* Reads Graphics Control Extension values.
*/privatevoidreadGraphicControlExt() {
// Block size.
read();
// Packed fields.int packed = read();
// Disposal method.
header.currentFrame.dispose = (packed & 0x1c) >> 2;
if (header.currentFrame.dispose == 0) {
// Elect to keep old image if discretionary.
header.currentFrame.dispose = 1;
}
header.currentFrame.transparency = (packed & 1) != 0;
// Delay in milliseconds.int delayInHundredthsOfASecond = readShort();
// TODO: consider allowing -1 to indicate show forever.if (delayInHundredthsOfASecond < MIN_FRAME_DELAY) {
delayInHundredthsOfASecond = DEFAULT_FRAME_DELAY;
}
header.currentFrame.delay = delayInHundredthsOfASecond * 10;
// Transparent color index
header.currentFrame.transIndex = read();
// Block terminator
read();
}
/**
* Reads next frame image.
*/privatevoidreadBitmap() {
// (sub)image position & size.
header.currentFrame.ix = readShort();
header.currentFrame.iy = readShort();
header.currentFrame.iw = readShort();
header.currentFrame.ih = readShort();
int packed = read();
// 1 - local color table flag interlaceboolean lctFlag = (packed & 0x80) != 0;
int lctSize = (int) Math.pow(2, (packed & 0x07) + 1);
// 3 - sort flag// 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color// table size
header.currentFrame.interlace = (packed & 0x40) != 0;
if (lctFlag) {
// Read table.
header.currentFrame.lct = readColorTable(lctSize);
} else {
// No local color table.
header.currentFrame.lct = null;
}
// Save this as the decoding position pointer.
header.currentFrame.bufferFrameStart = rawData.position();
// False decode pixel data to advance buffer.
skipImageData();
if (err()) {
return;
}
header.frameCount++;
// Add image to frame.
header.frames.add(header.currentFrame);
}
/**
* Reads Netscape extension to obtain iteration count.
*/privatevoidreadNetscapeExt() {
do {
readBlock();
if (block[0] == 1) {
// Loop count sub-block.int b1 = ((int) block[1]) & 0xff;
int b2 = ((int) block[2]) & 0xff;
header.loopCount = (b2 << 8) | b1;
if(header.loopCount == 0) {
header.loopCount = GifDecoder.LOOP_FOREVER;
}
}
} while ((blockSize > 0) && !err());
}
/**
* Reads GIF file header information.
*/privatevoidreadHeader() {
String id = "";
for (int i = 0; i < 6; i++) {
id += (char) read();
}
if (!id.startsWith("GIF")) {
header.status = GifDecoder.STATUS_FORMAT_ERROR;
return;
}
readLSD();
if (header.gctFlag && !err()) {
header.gct = readColorTable(header.gctSize);
header.bgColor = header.gct[header.bgIndex];
}
}
/**
* Reads Logical Screen Descriptor.
*/privatevoidreadLSD() {
// Logical screen size.
header.width = readShort();
header.height = readShort();
// Packed fieldsint packed = read();
// 1 : global color table flag.
header.gctFlag = (packed & 0x80) != 0;
// 2-4 : color resolution.// 5 : gct sort flag.// 6-8 : gct size.
header.gctSize = 2 << (packed & 7);
// Background color index.
header.bgIndex = read();
// Pixel aspect ratio
header.pixelAspect = read();
}
/**
* Reads color table as 256 RGB integer values.
*
* @param ncolors int number of colors to read.
* @return int array containing 256 colors (packed ARGB with full alpha).
*/privateint[] readColorTable(int ncolors) {
int nbytes = 3 * ncolors;
int[] tab = null;
byte[] c = newbyte[nbytes];
try {
rawData.get(c);
// TODO: what bounds checks are we avoiding if we know the number of colors?// Max size to avoid bounds checks.
tab = newint[MAX_BLOCK_SIZE];
int i = 0;
int j = 0;
while (i < ncolors) {
int r = ((int) c[j++]) & 0xff;
int g = ((int) c[j++]) & 0xff;
int b = ((int) c[j++]) & 0xff;
tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
}
} catch (BufferUnderflowException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Format Error Reading Color Table", e);
}
header.status = GifDecoder.STATUS_FORMAT_ERROR;
}
return tab;
}
/**
* Skips LZW image data for a single frame to advance buffer.
*/privatevoidskipImageData() {
// lzwMinCodeSize
read();
// data sub-blocks
skip();
}
/**
* Skips variable length blocks up to and including next zero length block.
*/privatevoidskip() {
try {
int blockSize;
do {
blockSize = read();
rawData.position(rawData.position() + blockSize);
} while (blockSize > 0);
} catch (IllegalArgumentException ex) {
}
}
/**
* Reads next variable length block from input.
*
* @return number of bytes stored in "buffer"
*/privateintreadBlock() {
blockSize = read();
int n = 0;
if (blockSize > 0) {
int count = 0;
try {
while (n < blockSize) {
count = blockSize - n;
rawData.get(block, n, count);
n += count;
}
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG,
"Error Reading Block n: " + n + " count: " + count + " blockSize: " + blockSize, e);
}
header.status = GifDecoder.STATUS_FORMAT_ERROR;
}
}
return n;
}
/**
* Reads a single byte from the input stream.
*/privateintread() {
int curByte = 0;
try {
curByte = rawData.get() & 0xFF;
} catch (Exception e) {
header.status = GifDecoder.STATUS_FORMAT_ERROR;
}
return curByte;
}
/**
* Reads next 16-bit value, LSB first.
*/privateintreadShort() {
// Read 16-bit value.return rawData.getShort();
}
privatebooleanerr() {
return header.status != GifDecoder.STATUS_OK;
}
}
GifImageView.java
package com.lenovo.cava.widget.GifImageView;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageView;
importstatic com.lenovo.cava.utils.Log.TAG_WITH_CLASS_NAME;
importstatic com.lenovo.cava.utils.Log.TAG_CAVA;
publicclassGifImageViewextendsImageViewimplementsRunnable {privatestaticfinal String TAG = TAG_WITH_CLASS_NAME ? "GifDecoderView" : TAG_CAVA;
private GifDecoder gifDecoder;
private Bitmap tmpBitmap;
privatefinal Handler handler = new Handler(Looper.getMainLooper());
privateboolean animating;
privateboolean renderFrame;
privateboolean shouldClear;
private Thread animationThread;
private OnFrameAvailable frameCallback = null;
privatelong framesDisplayDuration = -1L;
private OnAnimationStop animationStopCallback = null;
private OnAnimationStart animationStartCallback = null;
privatefinal Runnable updateResults = new Runnable() {
@Overridepublicvoidrun() {
if (tmpBitmap != null && !tmpBitmap.isRecycled()) {
setImageBitmap(tmpBitmap);
}
}
};
privatefinal Runnable cleanupRunnable = new Runnable() {
@Overridepublicvoidrun() {
if(gifDecoder != null) {
gifDecoder.clear();
}
tmpBitmap = null;
gifDecoder = null;
shouldClear = false;
}
};
publicGifImageView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
publicGifImageView(final Context context) {
super(context);
}
publicvoidsetBytes(finalbyte[] bytes) {
gifDecoder = new GifDecoder();
try {
gifDecoder.read(bytes);
} catch (final Exception e) {
gifDecoder = null;
Log.e(TAG, e.getMessage(), e);
return;
}
if(animating){
startAnimationThread();
} else {
gotoFrame(0);
}
}
publiclonggetFramesDisplayDuration() {
return framesDisplayDuration;
}
/**
* Sets custom display duration in milliseconds for the all frames. Should be called before {@link
* #startAnimation()}
*
* @param framesDisplayDuration Duration in milliseconds. Default value = -1, this property will
* be ignored and default delay from gif file will be used.
*/publicvoidsetFramesDisplayDuration(long framesDisplayDuration) {
this.framesDisplayDuration = framesDisplayDuration;
}
publicvoidstartAnimation() {
animating = true;
startAnimationThread();
}
publicbooleanisAnimating() {
return animating;
}
publicvoidstopAnimation() {
animating = false;
if (animationThread != null) {
animationThread.interrupt();
animationThread = null;
}
sleep(200);
}
privatevoidsleep(long time){
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
publicvoidgotoFrame(int frame){
if(gifDecoder.getCurrentFrameIndex() == frame) return;
if(gifDecoder.setFrameIndex(frame-1) && !animating){
renderFrame = true;
startAnimationThread();
}
}
publicvoidresetAnimation(){
gifDecoder.resetLoopIndex();
gotoFrame(0);
}
publicvoidclear() {
animating = false;
renderFrame = false;
shouldClear = true;
stopAnimation();
handler.post(cleanupRunnable);
}
publicbooleancanStart() {
return (animating || renderFrame) && gifDecoder != null && animationThread == null;
}
publicintgetGifWidth() {
return gifDecoder.getWidth();
}
publicintgetGifHeight() {
return gifDecoder.getHeight();
}
@Overridepublicvoidrun() {
if (animationStartCallback != null) {
animationStartCallback.onAnimationStart();
}
boolean callBack = true;
do {
callBack = true;
if (!animating && !renderFrame) {
callBack = false;
break;
}
long frameDecodeTime = 0;
long before = System.nanoTime();
boolean advance = false;
try {
advance = gifDecoder.advance();
//milliseconds spent on frame decode
tmpBitmap = gifDecoder.getNextFrame();
if (frameCallback != null) {
tmpBitmap = frameCallback.onFrameAvailable(tmpBitmap);
}
frameDecodeTime = (System.nanoTime() - before) / 1000000;
handler.post(updateResults);
} catch (Exception e) {
advance = false;
callBack = false;
Log.w(TAG, e);
}
renderFrame = false;
if (!animating || !advance) {
animating = false;
break;
}
try {
int delay = gifDecoder.getNextDelay();
// Sleep for frame duration minus time already spent on frame decode// Actually we need next frame decode duration here,// but I use previous frame time to make code more readable
delay -= frameDecodeTime;
if (delay > 0) {
Thread.sleep(framesDisplayDuration > 0 ? framesDisplayDuration : delay);
}
} catch (final Exception e) {
// suppress exceptionbreak;
}
} while (animating);
if (shouldClear) {
handler.post(cleanupRunnable);
}
animationThread = null;
if (animationStopCallback != null && callBack) {
animationStopCallback.onAnimationStop();
}
}
public OnFrameAvailable getOnFrameAvailable() {
return frameCallback;
}
publicvoidsetOnFrameAvailable(OnFrameAvailable frameProcessor) {
this.frameCallback = frameProcessor;
}
publicinterfaceOnFrameAvailable {
Bitmap onFrameAvailable(Bitmap bitmap);
}
public OnAnimationStop getOnAnimationStop() {
return animationStopCallback;
}
publicvoidsetOnAnimationStop(OnAnimationStop animationStop) {
this.animationStopCallback = animationStop;
}
publicvoidsetOnAnimationStart(OnAnimationStart animationStart) {
this.animationStartCallback = animationStart;
}
publicinterfaceOnAnimationStop {void onAnimationStop();
}
publicinterfaceOnAnimationStart {void onAnimationStart();
}
@OverrideprotectedvoidonDetachedFromWindow() {
super.onDetachedFromWindow();
clear();
}
privatevoidstartAnimationThread() {
if (canStart()) {
animationThread = new Thread(this);
animationThread.start();
}
}
}
## GifDecoder.java##/** * Copyright (c) 2013 Xcellent Creations, Inc. * Copyright 2014 Google, Inc. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a