//
// GIFReader.java
//
/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU 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.IOException;
import java.util.Arrays;
import java.util.Vector;
import loci.common.RandomAccessInputStream;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
/**
* 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="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/in/GIFReader.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/GIFReader.java;hb=HEAD">Gitweb</a></dd></dl>
*/
public class GIFReader extends FormatReader {
// -- Constants --
public static final String GIF_MAGIC_STRING = "GIF";
/** Maximum buffer size. */
private static final int MAX_STACK_SIZE = 4096;
private static final int IMAGE_SEPARATOR = 0x2c;
private static final int EXTENSION = 0x21;
private static final int END = 0x3b;
private static final int GRAPHICS = 0xf9;
// -- Fields --
/** Global color table. */
private int[] gct;
/** Active color table. */
private int[] act;
/** Interlace flag. */
private boolean interlace;
/** 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<byte[]> images;
private Vector<int[]> colorTables;
// -- Constructor --
/** Constructs a new GIF reader. */
public GIFReader() {
super("Graphics Interchange Format", "gif");
domains = new String[] {FormatTools.GRAPHICS_DOMAIN};
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
final int blockLen = GIF_MAGIC_STRING.length();
if (!FormatTools.validStream(stream, blockLen, false)) return false;
return stream.readString(blockLen).startsWith(GIF_MAGIC_STRING);
}
/* @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[], int, int, int, int)
*/
public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
throws FormatException, IOException
{
FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);
act = colorTables.get(no);
byte[] b = images.get(no);
if (no > 0 && transparency) {
byte[] prev = images.get(no - 1);
int idx = transIndex;
if (idx >= 127) idx = 0;
for (int i=0; i<b.length; i++) {
if ((act[b[i] & 0xff] & 0xffffff) == idx) {
b[i] = prev[i];
}
}
images.setElementAt(b, no);
}
for (int row=0; row<h; row++) {
System.arraycopy(b, (row + y) * getSizeX() + x, buf, row*w, w);
}
return buf;
}
/* @see loci.formats.IFormatReader#close(boolean) */
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
interlace = transparency = false;
ix = iy = iw = ih = blockSize = 0;
dispose = lastDispose = transIndex = 0;
gct = act;
prefix = null;
suffix = pixelStack = pixels = null;
images = null;
colorTables = null;
Arrays.fill(dBlock, (byte) 0);
}
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
LOGGER.info("Verifying GIF format");
in = new RandomAccessInputStream(id);
in.order(true);
images = new Vector<byte[]>();
colorTables = new Vector<int[]>();
String ident = in.readString(6);
if (!ident.startsWith(GIF_MAGIC_STRING)) {
throw new FormatException("Not a valid GIF file.");
}
LOGGER.info("Reading dimensions");
core[0].sizeX = in.readShort();
core[0].sizeY = in.readShort();
int packed = in.read() & 0xff;
boolean gctFlag = (packed & 0x80) != 0;
int gctSize = 2 << (packed & 7);
in.skipBytes(2);
addGlobalMeta("Global lookup table size", gctSize);
if (gctFlag) {
gct = readLut(gctSize);
}
LOGGER.info("Reading data blocks");
boolean done = false;
while (!done) {
int code = in.read() & 0xff;
switch (code) {
case IMAGE_SEPARATOR:
readImageBlock();
break;
case EXTENSION:
code = in.read() & 0xff;
switch (code) {
case GRAPHICS:
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;
default:
if (readBlock() == -1) {
done = true;
break;
}
skipBlocks();
}
break;
case END:
done = true;
break;
}
}
act = colorTables.get(0);
LOGGER.info("Populating metadata");
core[0].sizeZ = 1;
core[0].sizeC = 1;
core[0].sizeT = getImageCount();
core[0].dimensionOrder = "XYCTZ";
core[0].rgb = false;
core[0].littleEndian = true;
core[0].interleaved = true;
core[0].metadataComplete = true;
core[0].indexed = true;
core[0].falseColor = false;
core[0].pixelType = FormatTools.UINT8;
// populate metadata store
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this);
MetadataTools.setDefaultCreationDate(store, id, 0);
}
// -- 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) {
LOGGER.trace("Truncated block", 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;
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
int dataSize = in.read() & 0xff;
int clear = 1 << dataSize;
int eoi = clear + 1;
int available = clear + 2;
int oldCode = nullCode;
int codeSize = dataSize + 1;
int codeMask = (1 << codeSize) - 1;
int code = 0, inCode = 0;
for (code=0; code<clear; code++) {
prefix[code] = 0;
suffix[code] = (byte) code;
}
// decode GIF pixel stream
int datum = 0, first = 0, top = 0, pi = 0, bi = 0, bits = 0, count = 0;
int i = 0;
for (i=0; i<npix;) {
if (top == 0) {
if (bits < codeSize) {
if (count == 0) {
count = readBlock();
if (count <= 0) break;
bi = 0;
}
datum += (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 = 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[getSizeX() * getSizeY()];
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 = getImageCount() - 2;
if (n > 0) lastImage = n - 1;
}
if (lastImage != -1) {
byte[] prev = images.get(lastImage);
System.arraycopy(prev, 0, dest, 0, getSizeX() * getSizeY());
}
}
// 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 < getSizeY()) {
int k = line * getSizeX();
int dx = k + ix; // start of line in dest
int dlim = dx + iw; // end of dest line
if ((k + getSizeX()) < dlim) dlim = k + getSizeX();
int sx = i * iw; // start of line in source
while (dx < dlim) {
// map color and insert in destination
int index = pixels[sx++] & 0xff;
dest[dx++] = (byte) index;
}
}
}
colorTables.add(act);
images.add(dest);
}
private void skipBlocks() throws IOException {
int check = 0;
do { check = readBlock(); }
while (blockSize > 0 && check != -1);
}
private void readImageBlock() throws FormatException, IOException {
ix = in.readShort();
iy = in.readShort();
iw = in.readShort();
ih = in.readShort();
int packed = in.read();
boolean lctFlag = (packed & 0x80) != 0;
interlace = (packed & 0x40) != 0;
int lctSize = 2 << (packed & 7);
act = lctFlag ? readLut(lctSize) : gct;
int save = 0;
if (transparency) {
save = act[transIndex];
act[transIndex] = 0;
}
if (act == null) throw new FormatException("Color table not found.");
decodeImageData();
skipBlocks();
core[0].imageCount++;
if (transparency) act[transIndex] = save;
lastDispose = dispose;
}
/** Read a color lookup table of the specified size. */
private int[] readLut(int size) throws FormatException {
int nbytes = 3 * size;
byte[] c = new byte[nbytes];
int n = 0;
try { n = in.read(c); }
catch (IOException e) { }
if (n < nbytes) {
throw new FormatException("Color table not found");
}
int[] lut = new int[256];
int j = 0;
for (int i=0; i<size; i++) {
int r = c[j++] & 0xff;
int g = c[j++] & 0xff;
int b = c[j++] & 0xff;
lut[i] = 0xff000000 | (r << 16) | (g << 8) | b;
}
return lut;
}
}