// // 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; } }