/* * @(#)GifImageDecoder.java 1.55 06/10/24 * * Copyright 1990-2008 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License version * 2 only, as published by the Free Software Foundation. * * 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 version 2 for more details (a copy is * included at /legal/license.txt). * * You should have received a copy of the GNU General Public License * version 2 along with this work; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa * Clara, CA 95054 or visit www.sun.com if you need additional * information or have any questions. */ /*- * Reads GIF images from an InputStream and reports the * image data to an InputStreamImageSource object. */ package sun.awt.image; import java.util.Vector; import java.util.Hashtable; import java.io.InputStream; import java.io.IOException; import java.awt.image.*; /** * Gif Image converter * * @version 1.41 01/05/01 * @author Arthur van Hoff * @author Jim Graham */ public class GifImageDecoder extends ImageDecoder { private static final boolean verbose = false; private static final int IMAGESEP = 0x2c; private static final int EXBLOCK = 0x21; private static final int EX_GRAPHICS_CONTROL= 0xf9; private static final int EX_COMMENT = 0xfe; private static final int EX_APPLICATION = 0xff; private static final int TERMINATOR = 0x3b; private static final int TRANSPARENCYMASK = 0x01; private static final int INTERLACEMASK = 0x40; private static final int COLORMAPMASK = 0x80; int num_global_colors; byte[] global_colormap; int trans_pixel = -1; IndexColorModel global_model; Hashtable props = new Hashtable(); byte[] saved_image; IndexColorModel saved_model; int global_width; int global_height; int global_bgpixel; GifFrame curframe; public GifImageDecoder(InputStreamImageSource src, InputStream is) { super(src, is); } /** * An error has occurred. Throw an exception. */ private static void error(String s1) throws ImageFormatException { throw new ImageFormatException(s1); } /** * Read a number of bytes into a buffer. * @return number of bytes that were not read due to EOF or error */ private int readBytes(byte buf[], int off, int len) { while (len > 0) { try { int n = input.read(buf, off, len); if (n < 0) { break; } off += n; len -= n; } catch (IOException e) { break; } } return len; } private static final int ExtractByte(byte buf[], int off) { return (buf[off] & 0xFF); } private static final int ExtractWord(byte buf[], int off) { return (buf[off] & 0xFF) | ((buf[off + 1] & 0xFF) << 8); } /** * produce an image from the stream. */ public void produceImage() throws IOException, ImageFormatException { try { readHeader(); int totalframes = 0; int frameno = 0; int nloops = -1; int disposal_method = 0; int delay = -1; boolean loopsRead = false; boolean isAnimation = false; while (!aborted) { int code; if (!ImageProductionMonitor.allowsProduction()) { abort(); break; } switch (code = input.read()) { case EXBLOCK: switch (code = input.read()) { case EX_GRAPHICS_CONTROL: { byte buf[] = new byte[6]; if (readBytes(buf, 0, 6) != 0) { return;//error("corrupt GIF file"); } if ((buf[0] != 4) || (buf[5] != 0)) { return;//error("corrupt GIF file (GCE size)"); } // Get the index of the transparent color delay = ExtractWord(buf, 2) * 10; if (delay > 0 && !isAnimation) { isAnimation = true; ImageFetcher.startingAnimation(); } disposal_method = (buf[1] >> 2) & 7; if ((buf[1] & TRANSPARENCYMASK) != 0) { trans_pixel = ExtractByte(buf, 4); } else { trans_pixel = -1; } break; } case EX_COMMENT: case EX_APPLICATION: default: boolean loop_tag = false; String comment = ""; while (true) { int n = input.read(); if (n <= 0) { break; } byte buf[] = new byte[n]; if (readBytes(buf, 0, n) != 0) { return;//error("corrupt GIF file"); } if (code == EX_COMMENT) { comment += new String(buf); } else if (code == EX_APPLICATION) { if (loop_tag) { if (n == 3 && buf[0] == 1) { if (loopsRead) { ExtractWord(buf, 1); } else { nloops = ExtractWord(buf, 1); loopsRead = true; } } else { loop_tag = false; } } if ("NETSCAPE2.0".equals(new String(buf))) { loop_tag = true; } } } if (code == EX_COMMENT) { props.put("comment", comment); } if (loop_tag && !isAnimation) { isAnimation = true; ImageFetcher.startingAnimation(); } break; case -1: return; //error("corrupt GIF file"); } break; case IMAGESEP: if (!isAnimation) { input.mark(0); // we don't need the mark buffer } try { if (!readImage(totalframes == 0, disposal_method, delay)) { return; } } catch (Exception e) { if (verbose) { e.printStackTrace(); } return; } frameno++; totalframes++; break; default: case -1: if (verbose) { if (code == -1) { System.err.println("Premature EOF in GIF file," + " frame " + frameno); } else { System.err.println("corrupt GIF file (parse) [" + code + "]."); } } if (frameno == 0) { return; } // NOBREAK case TERMINATOR: if (nloops == 0 || nloops-- >= 0) { try { if (curframe != null) { curframe.dispose(); curframe = null; } input.reset(); saved_image = null; saved_model = null; frameno = 0; break; } catch (IOException e) { return; // Unable to reset input buffer } } if (verbose && frameno != 1) { System.out.println("processing GIF terminator," + " frames: " + frameno + " total: " + totalframes); } imageComplete(ImageConsumer.STATICIMAGEDONE, true); return; } } } finally { close(); } } /** * Read Image header */ private void readHeader() throws IOException, ImageFormatException { // Create a buffer byte buf[] = new byte[13]; // Read the header if (readBytes(buf, 0, 13) != 0) { throw new IOException(); } // Check header if ((buf[0] != 'G') || (buf[1] != 'I') || (buf[2] != 'F')) { error("not a GIF file."); } // Global width&height global_width = ExtractWord(buf, 6); global_height = ExtractWord(buf, 8); // colormap info int ch = ExtractByte(buf, 10); if ((ch & COLORMAPMASK) == 0) { error("no global colormap in GIF file."); } num_global_colors = 1 << ((ch & 0x7) + 1); global_bgpixel = ExtractByte(buf, 11); if (buf[12] != 0) { props.put("aspectratio", ""+((ExtractByte(buf, 12) + 15) / 64.0)); } // Read colors global_colormap = new byte[num_global_colors * 3]; if (readBytes(global_colormap, 0, num_global_colors * 3) != 0) { throw new IOException(); } input.mark(1000000); // set this mark in case this is an animated GIF } /** * The ImageConsumer hints flag for a non-interlaced GIF image. */ private static final int normalflags = ImageConsumer.TOPDOWNLEFTRIGHT | ImageConsumer.COMPLETESCANLINES | ImageConsumer.SINGLEPASS | ImageConsumer.SINGLEFRAME; /** * The ImageConsumer hints flag for an interlaced GIF image. */ private static final int interlaceflags = ImageConsumer.RANDOMPIXELORDER | ImageConsumer.COMPLETESCANLINES | ImageConsumer.SINGLEPASS | ImageConsumer.SINGLEFRAME; private short prefix[] = new short[4096]; private byte suffix[] = new byte[4096]; private byte outCode[] = new byte[4097]; //private native boolean parseImage(int x, int y, int width, int height, // boolean interlace, int initCodeSize, // byte block[], byte rasline[], // IndexColorModel model); /* * This is converted from the native version. The java version is * much faster when we have JIT. */ private boolean parseImage(int relx, int rely, int width, int height, boolean interlace, int initCodeSize, byte block[], byte rasline[], IndexColorModel model) { int OUTCODELENGTH = 4097; int clearCode = (1 << initCodeSize); int eofCode = clearCode + 1; int bitMask; int curCode; int outCount = OUTCODELENGTH; /* Variables used to form reading data */ boolean blockEnd = false; int remain = 0; int byteoff = 0; int accumbits = 0; int accumdata = 0; /* Variables used to decompress the data */ int codeSize = initCodeSize + 1; int maxCode = 1 << codeSize; int codeMask = maxCode - 1; int freeCode = clearCode + 2; int code = 0; int oldCode = 0; char prevChar = 0; //int blockLength = 0; int blockLength = 0; /* Variables used for writing pixels */ int x = width; int y = 0; int off = 0; int passinc = interlace ? 8 : 1; int passht = passinc; int len; bitMask = model.getMapSize() - 1; /* Read codes until the eofCode is encountered */ for (;;) { if (accumbits < codeSize) { boolean lastByte = false; /* fill the buffer if needed */ while (remain < 2) { if (blockEnd) { /* Sometimes we have one last byte to process... */ if (remain == 1 && accumbits + 8 >= codeSize) { break; } if (off > 0) { // sendPixels(relx, rely+y, width, passht, rasline, model); sendPixels(relx, rely+y, width, 1, rasline, model); } /* quietly accept truncated GIF images */ return false; } /* move remaining bytes to the beginning of the buffer */ block[0] = block[byteoff]; byteoff = 0; /* fill the block */ len = readBytes(block, remain, blockLength + 1); remain += blockLength; if (len > 0) { remain -= (len - 1); blockLength = 0; } else { blockLength = (block[remain] & 0xff); } if (blockLength == 0) { blockEnd = true; } } /* 2 bytes at a time saves checking for accumbits < codeSize. * We know we'll get enough and also that we can't overflow * since codeSize <= 12. */ if (!lastByte) { remain -= 2; accumdata += (block[byteoff++] & 0xff) << accumbits; accumbits += 8; accumdata += (block[byteoff++] & 0xff) << accumbits; accumbits += 8; } else { remain--; accumdata += (block[byteoff++] & 0xff) << accumbits; accumbits += 8; } } /* Compute the code */ code = accumdata & codeMask; accumdata >>= codeSize; accumbits -= codeSize; /* * Interpret the code */ if (code == clearCode) { /* Clear code sets everything back to its initial value, then * reads the immediately subsequent code as uncompressed data. */ /* Note that freeCode is one less than it is supposed to be, * this is because it will be incremented next time round the * loop */ freeCode = clearCode + 1; codeSize = initCodeSize + 1; maxCode = 1 << codeSize; codeMask = maxCode - 1; /* Continue if we've NOT reached the end, some Gif images * contain bogus codes after the last clear code. */ if (y < height) { continue; } /* pretend we've reached the end of the data */ code = eofCode; } if (code == eofCode) { /* make sure we read the whole block of pixels. */ while (!blockEnd) { if (readBytes(block, 0, blockLength + 1) != 0) { /* quietly accept truncated GIF images */ return false; } blockLength = block[blockLength]; blockEnd = (blockLength == 0); } return true; } /* It must be data: save code in CurCode */ curCode = code; /* Whenever it gets here outCount is always equal to OUTCODELENGTH, so no need to reset outCount. */ //outCount = OUTCODELENGTH; /* If greater or equal to freeCode, not in the hash table * yet; repeat the last character decoded */ if (curCode >= freeCode) { if (curCode > freeCode) { /* * if we get a code too far outside our range, it * could case the parser to start traversing parts * of our data structure that are out of range... */ /*In native version: goto flushit;*/ while (!blockEnd) { if (readBytes(block, 0, blockLength + 1) != 0) { /* quietly accept truncated GIF images */ return false; } blockLength = block[blockLength]; blockEnd = (blockLength == 0); } return true; } curCode = oldCode; outCode[--outCount] = (byte)prevChar; } /* Unless this code is raw data, pursue the chain pointed * to by curCode through the hash table to its end; each * code in the chain puts its associated output code on * the output queue. */ while (curCode > bitMask) { outCode[--outCount] = suffix[curCode]; if (outCount == 0) { /* * In theory this should never happen since our * prefix and suffix arrays are monotonically * decreasing and so outCode will only be filled * as much as those arrays, but I don't want to * take that chance and the test is probably * cheap compared to the read and write operations. * If we ever do overflow the array, we will just * flush the rest of the data and quietly accept * the GIF as truncated here. */ //In native version: goto flushit; while (!blockEnd) { if (readBytes(block, 0, blockLength + 1) != 0) { /* quietly accept truncated GIF images */ return false; } blockLength = block[blockLength]; blockEnd = (blockLength == 0); } return true; } curCode = prefix[curCode]; } /* The last code in the chain is treated as raw data. */ prevChar = (char)curCode; outCode[--outCount] = (byte)prevChar; /* Now we put the data out to the Output routine. It's * been stacked LIFO, so deal with it that way... */ len = OUTCODELENGTH - outCount; /* This is why I commented out the code that resets outCount. */ while (--len >= 0) { rasline[off++] = outCode[outCount++]; /* Update the X-coordinate, and if it overflows, update the * Y-coordinate */ if (--x == 0) { int count; /* If a non-interlaced picture, just increment y to the next * scan line. If it's interlaced, deal with the interlace as * described in the GIF spec. Put the decoded scan line out * to the screen if we haven't gone past the bottom of it */ // count = sendPixels(relx, rely+y, width, passht, rasline, model); count = sendPixels(relx, rely+y, width, 1, rasline, model); if (count <= 0) { /* Nobody is listening any more. */ return false; } x = width; off = 0; /* pass inc ht ystart */ /* 0 8 8 0 */ /* 1 8 4 4 */ /* 2 4 2 2 */ /* 3 2 1 1 */ y += passinc; while (y >= height) { passinc = passht; passht >>= 1; y = passht; if (passht == 0) { //In native version: goto flushit; while (!blockEnd) { if (readBytes(block, 0, blockLength + 1) != 0) { /* quietly accept truncated GIF images */ return false; } blockLength = block[blockLength] & 0xff; blockEnd = (blockLength == 0); } return true; } } } } /* Build the hash table on-the-fly. No table is stored in the file. */ prefix[freeCode] = (short)oldCode; suffix[freeCode] = (byte)prevChar; oldCode = code; /* Point to the next slot in the table. If we exceed the * maxCode, increment the code size unless * it's already 12. If it is, do nothing: the next code * decompressed better be CLEAR */ if (++freeCode >= maxCode) { if (codeSize < 12) { codeSize++; maxCode <<= 1; codeMask = maxCode - 1; } else { /* Just in case */ freeCode = maxCode - 1; } } } } private int sendPixels(int x, int y, int width, int height, byte rasline[], ColorModel model) { int rasbeg, rasend, x2; if (y < 0) { height += y; y = 0; } if (y + height > global_height) { height = global_height - y; } if (height <= 0) { return 1; } // rasline[0] == pixel at coordinate (x,y) // rasline[width] == pixel at coordinate (x+width, y) if (x < 0) { rasbeg = -x; width += x; // same as (width -= rasbeg) x2 = 0; // same as (x2 = x + rasbeg) } else { rasbeg = 0; // width -= 0; // same as (width -= rasbeg) x2 = x; // same as (x2 = x + rasbeg) } // rasline[rasbeg] == pixel at coordinate (x2,y) // rasline[width] == pixel at coordinate (x+width, y) // rasline[rasbeg + width] == pixel at coordinate (x2+width, y) if (x2 + width > global_width) { width = global_width - x2; } if (width <= 0) { return 1; } rasend = rasbeg + width; // rasline[rasbeg] == pixel at coordinate (x2,y) // rasline[rasend] == pixel at coordinate (x2+width, y) int off = y * global_width + x2; boolean save = (curframe.disposal_method == GifFrame.DISPOSAL_SAVE); if (trans_pixel >= 0 && !curframe.initialframe) { if (saved_image != null && saved_model == model) { for (int i = rasbeg; i < rasend; i++, off++) { byte pixel = rasline[i]; if ((pixel & 0xff) == trans_pixel) { rasline[i] = saved_image[off]; } else if (save) { saved_image[off] = pixel; } } } else { // We have to do this the hard way - only transmit // the non-transparent sections of the line... int runstart = -1; int count = 1; for (int i = rasbeg; i < rasend; i++, off++) { byte pixel = rasline[i]; if ((pixel & 0xff) == trans_pixel) { if (runstart >= 0) { // count = setPixels(x + runstart, y, i - runstart, height, model, rasline, runstart, 0); count = setPixels(x + runstart, y, i - runstart, height, model, rasline, runstart, i - runstart); if (count == 0) { break; } } runstart = -1; } else { if (runstart < 0) { runstart = i; } if (save) { saved_image[off] = pixel; } } } if (runstart >= 0) { // count = setPixels(x + runstart, y, rasend - runstart, height, model, rasline, runstart, 0); count = setPixels(x + runstart, y, rasend - runstart, height, model, rasline, runstart, rasend - runstart); } // Since (saved_model != model), store must be null... return count; } } else if (save) { System.arraycopy(rasline, rasbeg, saved_image, off, width); } // int count = setPixels(x2, y, width, height, model, rasline, rasbeg, 0); int count = setPixels(x2, y, width, height, model, rasline, rasbeg, width); return count; } /** * Read Image data */ private boolean readImage(boolean first, int disposal_method, int delay) throws IOException { if (curframe != null && !curframe.dispose()) { abort(); return false; } long tm = 0; if (verbose) { tm = System.currentTimeMillis(); } // Allocate the buffer byte block[] = new byte[256 + 3]; // Read the image descriptor if (readBytes(block, 0, 10) != 0) { throw new IOException(); } int x = ExtractWord(block, 0); int y = ExtractWord(block, 2); int width = ExtractWord(block, 4); int height = ExtractWord(block, 6); boolean interlace = (block[8] & INTERLACEMASK) != 0; IndexColorModel model = global_model; if ((block[8] & COLORMAPMASK) != 0) { // We read one extra byte above so now when we must // transfer that byte as the first colormap byte // and manually read the code size when we are done int num_local_colors = 1 << ((block[8] & 0x7) + 1); // Read local colors byte[] local_colormap = new byte[num_local_colors * 3]; local_colormap[0] = block[9]; if (readBytes(local_colormap, 1, num_local_colors * 3 - 1) != 0) { throw new IOException(); } // Now read the "real" code size byte which follows // the local color table if (readBytes(block, 9, 1) != 0) { throw new IOException(); } model = new IndexColorModel(8, num_local_colors, local_colormap, 0, false, trans_pixel); } else if (model == null || trans_pixel != model.getTransparentPixel()) { model = new IndexColorModel(8, num_global_colors, global_colormap, 0, false, trans_pixel); global_model = model; } // Notify the consumers if (first) { if (global_width == 0) global_width = width; if (global_height == 0) global_height = height; setDimensions(global_width, global_height); setProperties(props); setColorModel(model); headerComplete(); } if (disposal_method == GifFrame.DISPOSAL_SAVE && saved_image == null) { saved_image = new byte[global_width * global_height]; } int hints = (interlace ? interlaceflags : normalflags); setHints(hints); curframe = new GifFrame(this, disposal_method, delay, (curframe == null), model, x, y, width, height); // allocate the raster data byte rasline[] = new byte[width]; if (verbose) { System.out.print("Reading a " + width + " by " + height + " " + (interlace ? "" : "non-") + "interlaced image..."); } boolean ret = parseImage(x, y, width, height, interlace, ExtractByte(block, 9), block, rasline, model); if (!ret) { abort(); } if (verbose) { System.out.println("done in " + (System.currentTimeMillis() - tm) + "ms"); } return ret; } /* * Since I translated the JNI version of parseImage() into Java, * we no longer need initIDs(). */ /* // if we're using JNI, we need to find the method and field IDs. * private static native void initIDs(); * static { * initIDs(); * } */ } class GifFrame { private static final boolean verbose = false; private static IndexColorModel trans_model; static final int DISPOSAL_NONE = 0x00; static final int DISPOSAL_SAVE = 0x01; static final int DISPOSAL_BGCOLOR = 0x02; static final int DISPOSAL_PREVIOUS = 0x03; GifImageDecoder decoder; int disposal_method; int delay; IndexColorModel model; int x; int y; int width; int height; boolean initialframe; public GifFrame(GifImageDecoder id, int dm, int dl, boolean init, IndexColorModel cm, int x, int y, int w, int h) { this.decoder = id; this.disposal_method = dm; this.delay = dl; this.model = cm; this.initialframe = init; this.x = x; this.y = y; this.width = w; this.height = h; } private void setPixels(int x, int y, int w, int h, ColorModel cm, byte[] pix, int off, int scan) { decoder.setPixels(x, y, w, h, cm, pix, off, scan); } public boolean dispose() { if (decoder.imageComplete(ImageConsumer.SINGLEFRAMEDONE, false) == 0) { return false; } else { if (delay > 0) { try { if (verbose) { System.out.println("sleeping: "+delay); } Thread.sleep(delay); } catch (InterruptedException e) { return false; } } else { Thread.yield(); } if (verbose && disposal_method != 0) { System.out.println("disposal method: "+disposal_method); } int global_width = decoder.global_width; int global_height = decoder.global_height; if (x < 0) { width += x; x = 0; } if (x + width > global_width) { width = global_width - x; } if (width <= 0) { disposal_method = DISPOSAL_NONE; } else { if (y < 0) { height += y; y = 0; } if (y + height > global_height) { height = global_height - y; } if (height <= 0) { disposal_method = DISPOSAL_NONE; } } switch (disposal_method) { case DISPOSAL_PREVIOUS: byte[] saved_image = decoder.saved_image; IndexColorModel saved_model = decoder.saved_model; if (saved_image != null) { setPixels(x, y, width, height, saved_model, saved_image, y * global_width + x, global_width); } break; case DISPOSAL_BGCOLOR: byte tpix; if (model.getTransparentPixel() < 0) { model = trans_model; if (model == null) { model = new IndexColorModel(8, 1, new byte[4], 0, true); trans_model = model; } tpix = 0; } else { tpix = (byte) model.getTransparentPixel(); } int rassize = width * height; byte[] rasfill = new byte[rassize]; if (tpix != 0) { for (int i = 0; i < rassize; i++) { rasfill[i] = tpix; } } // setPixels(x, y, width, height, model, rasfill, 0, 0); setPixels(x, y, width, height, model, rasfill, 0, width); break; case DISPOSAL_SAVE: decoder.saved_model = model; break; } } return true; } }