/*
* File downloaded from:
* https://code.google.com/p/giffiledecoder/
* Based on GifDecoder.java at:
* http://code.google.com/p/android-gifview/
* http://code.google.com/p/animated-gifs-in-android/
* GIF specification:
* http://www.w3.org/Graphics/GIF/spec-gif89a.txt
*
* Contributors: Nick Frolov, Aleksandr Shardakov
*/
package com.tomclaw.mandarin.util;
import android.content.ContentResolver;
import android.net.Uri;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
public class GifFileDecoder {
private static final int MIN_DELAY = 20;
private static final int ENFORCED_DELAY = 100;
private static final int DISPOSE_NONE = 0;
//private static final int DISPOSE_LEAVE = 1;
private static final int DISPOSE_BACKGROUND = 2;
private static final int DISPOSE_PREVIOUS = 3;
private final ContentResolver contentResolver;
private final Uri uri;
private InputStream in;
private long headerSize;
private boolean hasFrame;
private int loopIndex;
private int frameIndex;
private int width; // full image width
private int height; // full image height
private int loopCount; // iterations; 0 = repeat forever
private boolean hasGct;
private final int[] gct = new int[256]; // global color table
private boolean hasLct;
private final int[] lct = new int[256]; // local color table
private int[] act; // active color table
private boolean interlace; // interlace flag
private int ix, iy, iw, ih; // current image rectangle
private int dispose;
private boolean transparency; // use transparent color
private int delay; // delay in milliseconds
private int transIndex; // transparent color index
private int[] baseImage;
private int[] tempImage;
private final byte[] ctbuf = new byte[256 * 3]; // color table reading buffer
// Block reader data
private final byte[] block = new byte[256 + 1]; // current data block
// LZW decoder pixel stack size
private static final int MAX_STACK_SIZE = 4096;
// LZW decoder working arrays
private final int[] ptable = new int[MAX_STACK_SIZE + 1];
private final int[] ltable = new int[MAX_STACK_SIZE + 1];
private final int[] ltableTemplate = new int[MAX_STACK_SIZE + 1];
private int[] pixels;
private final IOException EX_EOF = new IOException("Unexpected end of file");
private final IOException EX_IFF = new IOException("Incorrect file format");
public GifFileDecoder(Uri uri, ContentResolver contentResolver) {
this.uri = uri;
this.contentResolver = contentResolver;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int getDelay() {
return delay;
}
public void start() throws IOException {
reopenStream();
// file header
String id = "";
for (int i = 0; i < 6; i++)
id += (char) read();
if (!id.startsWith("GIF"))
throw EX_IFF;
// logical screen size
width = readShort();
height = readShort();
// packed fields:
// 1 : global color table flag,
// 2-4 : color resolution
// 5 : gct sort flag
// 6-8 : gct size
int packed = read();
hasGct = (packed & 0x80) != 0;
int gctSize = 2 << (packed & 0x07);
read(); // background color index, not used
read(); // pixel aspect ratio, not used
headerSize = 13;
if (hasGct) {
readColorTable(gct, gctSize);
headerSize += 3 * gctSize;
}
// Init fields
baseImage = new int[width * height];
for (int i = 0; i < ltableTemplate.length; i++)
ltableTemplate[i] = 1;
loopCount = 1;
loopIndex = 0;
resetLoop();
}
private void reopenStream() throws IOException {
if (in != null)
in.close();
in = new BufferedInputStream(contentResolver.openInputStream(uri), 32768);
}
public void stop() {
try {
if (in != null) in.close();
} catch (IOException ex) {
}
in = null;
}
private void readColorTable(int[] tab, int ncolors) throws IOException {
int nbytes = 3 * ncolors;
int n = in.read(ctbuf, 0, nbytes);
if (n < nbytes)
throw EX_IFF;
int i = 0;
int j = 0;
while (i < ncolors) {
int r = ((int) ctbuf[j++]) & 0xff;
int g = ((int) ctbuf[j++]) & 0xff;
int b = ((int) ctbuf[j++]) & 0xff;
// Bitmap.copyPixelsFromBuffer with ARGB_8888 really expects ABGR, so doing that.
// Note that Bitmap.createBitmap takes ARGB.
tab[i++] = 0xff000000 | (b << 16) | (g << 8) | r;
}
}
private void resetLoop() {
hasFrame = true;
frameIndex = 0;
resetFrame();
Arrays.fill(baseImage, 0);
}
public boolean hasFrame() {
return (hasFrame && (loopCount == 0 || loopIndex < loopCount));
}
public int[] readFrame() throws IOException {
while (hasFrame) {
int code = read();
switch (code) {
case 0x2C: // image
return readImage();
case 0x21: // extension
readExtension();
break;
case 0x3b: // terminator
hasFrame = false;
break;
case 0x00: // bad byte, but keep going and see what happens
break;
default:
throw EX_IFF;
}
}
// completed all images
if (frameIndex == 0)
throw new IOException("No GIF frames in file");
loopIndex++;
if (loopCount != 0 && loopIndex >= loopCount)
return null;
resetLoop();
// rewind file
reopenStream();
in.skip(headerSize);
// recursion - read first frame
return readFrame();
}
private void readExtension() throws IOException {
int code = read();
switch (code) {
case 0xf9: // graphics control extension
readGraphicControlExt();
break;
case 0xff: // application extension
readBlock();
String app = "";
for (int i = 0; i < 11; i++) {
app += (char) block[i];
}
if (app.equals("NETSCAPE2.0")) {
readNetscapeExt();
} else {
skip(); // don't care
}
break;
case 0xfe:// comment extension
skip();
break;
case 0x01:// plain text extension
skip();
break;
default: // uninteresting extension
skip();
}
}
private void readNetscapeExt() throws IOException {
while (readBlock() > 0) {
if (block[0] == 1) {
// loop count
// This is sometimes ignored, and sometimes browsers add one more
// loop for some reason. I'm gonna do the specified number of loops.
int b1 = ((int) block[1]) & 0xff;
int b2 = ((int) block[2]) & 0xff;
loopCount = (b2 << 8) | b1;
}
}
}
private void resetFrame() {
dispose = DISPOSE_NONE;
transparency = false;
delay = ENFORCED_DELAY;
transIndex = 0;
}
private void readGraphicControlExt() throws IOException {
read(); // block size
int packed = read(); // packed fields
dispose = (packed & 0x1c) >> 2; // disposal method
if (dispose != DISPOSE_BACKGROUND && dispose != DISPOSE_PREVIOUS)
dispose = DISPOSE_NONE;
transparency = (packed & 1) != 0;
delay = readShort() * 10; // delay in milliseconds
if (delay < MIN_DELAY)
delay = ENFORCED_DELAY;
transIndex = read(); // transparent color index
read(); // block terminator
}
private int[] readImage() throws IOException {
// (sub)image position & size
ix = readShort();
iy = readShort();
iw = readShort();
ih = readShort();
// packed fields
// 1 - local color table flag
// 2 - interlace flag
// 3 - lct sorted
// 4-5 - reserved
// 6-8 - lct size
int packed = read();
hasLct = (packed & 0x80) != 0;
int lctSize = 2 << (packed & 0x07);
interlace = (packed & 0x40) != 0;
if (hasLct) {
readColorTable(lct, lctSize);
act = lct; // make local table active
} else {
if (!hasGct)
throw EX_IFF;
act = gct; // make global table active
}
// image data
if (transparency) {
int c = act[transIndex];
act[transIndex] = 0;
decodeBitmapData();
act[transIndex] = c;
} else {
decodeBitmapData();
}
skip();
// draw image
int[] image = drawImage();
frameIndex++;
return image;
}
private void decodeBitmapData() throws IOException {
final int npix = iw * ih;
if ((pixels == null) || (pixels.length < npix))
pixels = new int[npix]; // allocate new pixel array
final byte[] _block = block;
final int[] _ptable = ptable;
final int[] _ltable = ltable;
final int[] _pixels = pixels;
final int[] _act = act;
// Initialize GIF data stream decoder.
final int data_size = read();
final int clear = 1 << data_size;
final int end_of_information = clear + 1;
int available = clear + 1;
int code_size = data_size + 1;
int code_mask = (1 << code_size) - 1;
int bits, code, old_code, count, datum, bi, pi, old_pi;
System.arraycopy(ltableTemplate, 0, _ltable, 0, clear);
// Decode GIF pixel stream.
datum = bits = count = code = old_code = pi = old_pi = bi = 0;
while (pi < npix) {
while (bits < code_size) {
// Load bytes until there are enough bits for a code.
if (count == 0) {
// Read a new data block.
count = readBlock();
if (count == 0)
break;
bi = 0;
}
datum |= (_block[bi] & 0xff) << bits;
bits += 8;
bi++;
count--;
}
// Get the next code.
code = datum & code_mask;
datum >>= code_size;
bits -= code_size;
// Interpret the code
final boolean fl = code < available;
if (!fl) {
old_pi = pi;
if (old_code > clear) {
final int p = _ptable[old_code];
final int len = _ltable[old_code];
for (int i = 0; i < len; i++)
_pixels[pi++] = _pixels[p + i];
} else
_pixels[pi++] = _act[old_code];
_pixels[pi++] = _pixels[old_pi];
}
if (available < MAX_STACK_SIZE) {
_ptable[available] = old_pi;
_ltable[available] = _ltable[old_code] + 1;
}
available++;
if (available > code_mask && available < MAX_STACK_SIZE) {
code_size++;
code_mask |= available;
}
if (fl) {
if (code == clear) {
// Reset decoder.
code_size = data_size + 1;
code_mask = (1 << code_size) - 1;
available = clear + 1;
old_code = 0;
} else if (code == end_of_information) {
// Stop decode
break;
} else {
old_pi = pi;
if (code > clear) {
final int p = _ptable[code];
final int len = _ltable[code];
for (int i = 0; i < len; i++)
_pixels[pi++] = _pixels[p + i];
} else
_pixels[pi++] = _act[code];
}
}
old_code = code;
}
for (int i = pi; i < npix; i++)
_pixels[i] = 0; // clear missing pixels
}
private int[] drawImage() {
int[] dest;
if (dispose == DISPOSE_NONE) {
dest = baseImage;
} else {
if (tempImage == null)
tempImage = new int[width * height];
System.arraycopy(baseImage, 0, tempImage, 0, baseImage.length);
dest = tempImage;
}
if (!interlace && dispose != DISPOSE_BACKGROUND
&& ix == 0 && iy == 0 && iw == width && ih == height)
drawImageFaster(dest);
else
drawImageRegular(dest);
return dest;
}
private void drawImageFaster(final int[] dest) {
final int[] _pixels = pixels;
for (int i = 0; i < dest.length; i++) {
final int c = _pixels[i];
if (c != 0)
dest[i] = c;
}
}
private void drawImageRegular(final int[] dest) {
final int[] _pixels = pixels;
int pass = 1;
int inc = 8;
int iline = 0;
for (int i = 0; i < ih; i++) {
int line = i;
if (interlace) {
if (iline >= ih) {
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 += iy;
if (line < height) {
int k = line * width;
int dx = k + ix; // start of line in dest
int dlim = dx + iw; // end of dest line
if ((k + width) < dlim) {
dlim = k + width; // past dest edge
}
int sx = i * iw; // start of line in source
while (dx < dlim) {
// map color and insert in image
final int c = _pixels[sx++];
if (c != 0)
dest[dx] = c;
// if will be disposing to background, also modify canvas
if (dispose == DISPOSE_BACKGROUND)
baseImage[dx] = 0;
dx++;
}
}
}
}
// Reads a single byte from the input stream.
private int read() throws IOException {
int res = in.read();
if (res == -1)
throw EX_EOF;
return res;
}
// Reads next 16-bit value, LSB first
private int readShort() throws IOException {
final int a = in.read();
if (a == -1)
throw EX_EOF;
final int b = in.read();
if (b == -1)
throw EX_EOF;
return a | (b << 8);
}
// Reads next variable length block from input.
private int readBlock() throws IOException {
final int size = in.read();
if (size == -1)
throw EX_EOF;
int n = 0;
while (n < size) {
final int count = in.read(block, n, size - n);
if (count == -1)
throw EX_EOF;
n += count;
}
return size;
}
// Skips variable length blocks up to and including next zero length block.
private void skip() throws IOException {
while (readBlock() > 0) ;
}
}