//
// GIFReader.java
//
/*
LOCI Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ Melissa Linkert, Curtis Rueden, Chris Allan,
Eric Kjellman and Brian Loranger.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package loci.formats.in;
import java.io.*;
import java.util.Vector;
import loci.formats.*;
/**
* GIFReader is the file format reader for Graphics Interchange Format
* (GIF) files. Much of this code was adapted from the Animated GIF Reader
* plugin for ImageJ (http://rsb.info.nih.gov/ij).
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/in/GIFReader.java">Trac</a>,
* <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/in/GIFReader.java">SVN</a></dd></dl>
*/
public class GIFReader extends FormatReader {
// -- Constants --
/** Maximum buffer size. */
private static final int MAX_STACK_SIZE = 4096;
// -- Fields --
/** Global color table used. */
private boolean gctFlag;
/** Size of global color table. */
private int gctSize;
/** Global color table. */
private int[] gct;
/** Local color table. */
private int[] lct;
/** Active color table. */
private int[] act;
/** Local color table flag. */
private boolean lctFlag;
/** Interlace flag. */
private boolean interlace;
/** Local color table size. */
private int lctSize;
/** Current image rectangle. */
private int ix, iy, iw, ih;
/** Current data block. */
private byte[] dBlock = new byte[256];
/** Block size. */
private int blockSize = 0;
private int dispose = 0;
private int lastDispose = 0;
/** Use transparent color. */
private boolean transparency = false;
/** Transparent color index. */
private int transIndex;
// LZW working arrays
private short[] prefix;
private byte[] suffix;
private byte[] pixelStack;
private byte[] pixels;
private Vector images;
private Vector colorTables;
// -- Constructor --
/** Constructs a new GIF reader. */
public GIFReader() {
super("Graphics Interchange Format", "gif");
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(byte[]) */
public boolean isThisType(byte[] block) { return false; }
/* @see loci.formats.IFormatReader#get8BitLookupTable() */
public byte[][] get8BitLookupTable() throws FormatException, IOException {
FormatTools.assertId(currentId, true, 1);
byte[][] table = new byte[3][act.length];
for (int i=0; i<act.length; i++) {
table[0][i] = (byte) ((act[i] >> 16) & 0xff);
table[1][i] = (byte) ((act[i] >> 8) & 0xff);
table[2][i] = (byte) (act[i] & 0xff);
}
return table;
}
/* @see loci.formats.IFormatReader#openBytes(int, byte[]) */
public byte[] openBytes(int no, byte[] buf)
throws FormatException, IOException
{
FormatTools.assertId(currentId, true, 1);
FormatTools.checkPlaneNumber(this, no);
FormatTools.checkBufferSize(this, buf.length);
act = (int[]) colorTables.get(no);
buf = (byte[]) images.get(no);
if (no > 0) {
byte[] prev = (byte[]) images.get(no - 1);
for (int i=0; i<buf.length; i++) {
if ((act[buf[i] & 0xff] & 0xffffff) == 0) {
buf[i] = prev[i];
}
}
images.setElementAt(buf, no);
}
return buf;
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
if (debug) debug("GIFReader.initFile(" + id + ")");
super.initFile(id);
status("Verifying GIF format");
in = new RandomAccessStream(id);
in.order(true);
images = new Vector();
colorTables = new Vector();
String ident = in.readString(6);
if (!ident.startsWith("GIF")) {
throw new FormatException("Not a valid GIF file.");
}
status("Reading dimensions");
core.sizeX[0] = in.readShort();
core.sizeY[0] = in.readShort();
int packed = in.read() & 0xff;
gctFlag = (packed & 0x80) != 0;
gctSize = 2 << (packed & 7);
in.skipBytes(1);
in.skipBytes(1);
if (gctFlag) {
int nbytes = 3 * gctSize;
byte[] c = new byte[nbytes];
in.read(c);
gct = new int[256];
int i = 0;
int j = 0;
int r, g, b;
while (i < gctSize) {
r = c[j++] & 0xff;
g = c[j++] & 0xff;
b = c[j++] & 0xff;
gct[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
}
}
status("Reading data blocks");
boolean done = false;
while (!done) {
int code = in.read() & 0xff;
switch (code) {
case 0x2c: // image separator:
ix = in.readShort();
iy = in.readShort();
iw = in.readShort();
ih = in.readShort();
packed = in.read();
lctFlag = (packed & 0x80) != 0;
interlace = (packed & 0x40) != 0;
lctSize = 2 << (packed & 7);
if (lctFlag) {
int nbytes = 3 * lctSize;
byte[] c = new byte[nbytes];
int n = 0;
try { n = in.read(c); }
catch (IOException e) { }
if (n < nbytes) {
throw new FormatException("Local color table not found");
}
lct = new int[256];
int i = 0;
int j = 0;
while (i < lctSize) {
int r = c[j++] & 0xff;
int g = c[j++] & 0xff;
int b = c[j++] & 0xff;
lct[i++] = 0xff000000 | (r << 16) + (g << 8) | b;
}
act = lct;
}
else {
act = gct;
}
int save = 0;
if (transparency) {
save = act[transIndex];
act[transIndex] = 0;
}
if (act == null) throw new FormatException("Color table not found.");
decodeImageData();
int check = 0;
do { check = readBlock(); }
while (blockSize > 0 && check != -1);
core.imageCount[0]++;
if (transparency) act[transIndex] = save;
lastDispose = dispose;
lct = null;
break;
case 0x21: // extension
code = in.read() & 0xff;
switch (code) {
case 0xf9: // graphics control extension
in.skipBytes(1);
packed = in.read() & 0xff;
dispose = (packed & 0x1c) >> 1;
transparency = (packed & 1) != 0;
in.skipBytes(2);
transIndex = in.read() & 0xff;
in.skipBytes(1);
break;
case 0xff: // application extension
if (readBlock() == -1) {
done = true;
break;
}
String app = new String(dBlock, 0, 11);
if (app.equals("NETSCAPE2.0")) {
do {
check = readBlock();
}
while (blockSize > 0 && check != -1);
}
else {
do {
check = readBlock();
}
while (blockSize > 0 && check != -1);
}
break;
default:
do {
check = readBlock();
}
while (blockSize > 0 && check != -1);
}
break;
case 0x3b: // terminator
done = true;
break;
}
}
status("Populating metadata");
core.sizeZ[0] = 1;
core.sizeC[0] = 3;
core.sizeT[0] = core.imageCount[0];
core.currentOrder[0] = "XYCTZ";
core.rgb[0] = true;
core.littleEndian[0] = true;
core.interleaved[0] = true;
core.metadataComplete[0] = true;
core.indexed[0] = true;
core.falseColor[0] = false;
// populate metadata store
MetadataStore store = getMetadataStore();
store.setImage(currentId, null, null, null);
core.pixelType[0] = FormatTools.UINT8;
FormatTools.populatePixels(store, this);
for (int i=0; i<core.sizeC[0]; i++) {
store.setLogicalChannel(i, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null);
}
}
// -- Helper methods --
/** Reads the next variable length block. */
private int readBlock() throws IOException {
if (in.getFilePointer() == in.length()) return -1;
blockSize = in.read() & 0xff;
int n = 0;
int count;
if (blockSize > 0) {
try {
while (n < blockSize) {
count = in.read(dBlock, n, blockSize - n);
if (count == -1) break;
n += count;
}
}
catch (IOException e) { }
}
return n;
}
/** Decodes LZW image data into a pixel array. Adapted from ImageMagick. */
private void decodeImageData() throws IOException {
int nullCode = -1;
int npix = iw * ih;
int available, clear, codeMask, codeSize, eoi, inCode, oldCode, bits, code,
count, i, datum, dataSize, first, top, bi, pi;
if (pixels == null || pixels.length < npix) pixels = new byte[npix];
if (prefix == null) prefix = new short[MAX_STACK_SIZE];
if (suffix == null) suffix = new byte[MAX_STACK_SIZE];
if (pixelStack == null) pixelStack = new byte[MAX_STACK_SIZE + 1];
// initialize GIF data stream decoder
dataSize = in.read() & 0xff;
clear = 1 << dataSize;
eoi = clear + 1;
available = clear + 2;
oldCode = nullCode;
codeSize = dataSize + 1;
codeMask = (1 << codeSize) - 1;
for (code=0; code<clear; code++) {
prefix[code] = 0;
suffix[code] = (byte) code;
}
// decode GIF pixel stream
datum = bits = count = first = top = pi = bi = 0;
for (i=0; i<npix;) {
if (top == 0) {
if (bits < codeSize) {
if (count == 0) {
count = readBlock();
if (count <= 0) break;
bi = 0;
}
datum += (((int) dBlock[bi]) & 0xff) << bits;
bits += 8;
bi++;
count--;
continue;
}
// get the next code
code = datum & codeMask;
datum >>= codeSize;
bits -= codeSize;
// interpret the code
if ((code > available) || (code == eoi)) {
break;
}
if (code == clear) {
// reset the decoder
codeSize = dataSize + 1;
codeMask = (1 << codeSize) - 1;
available = clear + 2;
oldCode = nullCode;
continue;
}
if (oldCode == nullCode) {
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;
if (available >= MAX_STACK_SIZE) break;
pixelStack[top++] = (byte) first;
prefix[available] = (short) oldCode;
suffix[available] = (byte) first;
available++;
if (((available & codeMask) == 0) && (available < MAX_STACK_SIZE)) {
codeSize++;
codeMask += available;
}
oldCode = inCode;
}
top--;
pixels[pi++] = pixelStack[top];
i++;
}
for (i=pi; i<npix; i++) pixels[i] = 0;
setPixels();
}
private void setPixels() {
// expose destination image's pixels as an int array
byte[] dest = new byte[core.sizeX[0] * core.sizeY[0]];
int lastImage = -1;
// fill in starting image contents based on last image's dispose code
if (lastDispose > 0) {
if (lastDispose == 3) { // use image before last
int n = core.imageCount[0] - 2;
if (n > 0) lastImage = n - 1;
}
if (lastImage != -1) {
byte[] prev = (byte[]) images.get(lastImage);
System.arraycopy(prev, 0, dest, 0, core.sizeX[0] * core.sizeY[0]);
}
}
// 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<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;
}
}
line = iline;
iline += inc;
}
line += iy;
if (line < core.sizeY[0]) {
int k = line * core.sizeX[0];
int dx = k + ix; // start of line in dest
int dlim = dx + iw; // end of dest line
if ((k + core.sizeX[0]) < dlim) dlim = k + core.sizeX[0];
int sx = i * iw; // start of line in source
while (dx < dlim) {
// map color and insert in destination
int index = ((int) pixels[sx++]) & 0xff;
dest[dx++] = (byte) index;
}
}
}
colorTables.add(act);
images.add(dest);
}
}