blob: 574abcca37f5281f912d0475f85733f75c755d48 [file] [log] [blame]
/*
* Copyright (c) 1995, 2006, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 in the LICENSE file that
* accompanied this code).
*
* 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.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.
*
* The algorithm is copyright of CompuServe.
*/
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
*
* @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;
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, 0);
} 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, 0))) {
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) {
// no global colormap so make up our own
// If there is a local colormap, it will override what we
// have here. If there is not a local colormap, the rules
// for GIF89 say that we can use whatever colormap we want.
// This means that we should probably put in a full 256 colormap
// at some point. REMIND!
num_global_colors = 2;
global_bgpixel = 0;
global_colormap = new byte[2*3];
global_colormap[0] = global_colormap[1] = global_colormap[2] = (byte)0;
global_colormap[3] = global_colormap[4] = global_colormap[5] = (byte)255;
}
else {
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(Integer.MAX_VALUE); // 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 static native void initIDs();
static {
/* ensure that the necessary native libraries are loaded */
NativeLibLoader.loadLibraries();
initIDs();
}
private native boolean parseImage(int x, int y, int width, int height,
boolean interlace, int initCodeSize,
byte block[], byte rasline[],
IndexColorModel model);
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 && model.equals(saved_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...
// Fix for 6301050: the interlacing is ignored in this case
// in order to avoid artefacts in case of animated images.
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, 1,
model, rasline,
runstart, 0);
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, 1,
model, rasline,
runstart, 0);
}
return count;
}
} else if (save) {
System.arraycopy(rasline, rasbeg, saved_image, off, width);
}
int count = setPixels(x2, y, width, height, model,
rasline, rasbeg, 0);
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);
/*
* Majority of gif images have
* same logical screen and frame dimensions.
* Also, Photoshop and Mozilla seem to use the logical
* screen dimension (from the global stream header)
* if frame dimension is invalid.
*
* We use similar heuristic and trying to recover
* frame width from logical screen dimension and
* frame offset.
*/
if (width == 0 && global_width != 0) {
width = global_width - x;
}
if (height == 0 && global_height != 0) {
height = global_height - y;
}
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();
}
if (trans_pixel >= num_local_colors) {
// Fix for 4233748: extend colormap to contain transparent pixel
num_local_colors = trans_pixel + 1;
local_colormap = grow_colormap(local_colormap, num_local_colors);
}
model = new IndexColorModel(8, num_local_colors, local_colormap,
0, false, trans_pixel);
} else if (model == null
|| trans_pixel != model.getTransparentPixel()) {
if (trans_pixel >= num_global_colors) {
// Fix for 4233748: extend colormap to contain transparent pixel
num_global_colors = trans_pixel + 1;
global_colormap = grow_colormap(global_colormap, num_global_colors);
}
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];
/*
* If height of current image is smaller than the global height,
* fill the gap with transparent pixels.
*/
if ((height < global_height) && (model != null)) {
byte tpix = (byte)model.getTransparentPixel();
if (tpix >= 0) {
byte trans_rasline[] = new byte[global_width];
for (int i=0; i<global_width;i++) {
trans_rasline[i] = tpix;
}
setPixels(0, 0, global_width, y,
model, trans_rasline, 0, 0);
setPixels(0, y+height, global_width,
global_height-height-y, model, trans_rasline,
0, 0);
}
}
}
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...");
}
int initCodeSize = ExtractByte(block, 9);
if (initCodeSize >= 12) {
if (verbose) {
System.out.println("Invalid initial code size: " +
initCodeSize);
}
return false;
}
boolean ret = parseImage(x, y, width, height,
interlace, initCodeSize,
block, rasline, model);
if (!ret) {
abort();
}
if (verbose) {
System.out.println("done in "
+ (System.currentTimeMillis() - tm)
+ "ms");
}
return ret;
}
public static byte[] grow_colormap(byte[] colormap, int newlen) {
byte[] newcm = new byte[newlen * 3];
System.arraycopy(colormap, 0, newcm, 0, colormap.length);
return newcm;
}
}
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();
}
byte[] rasline = new byte[width];
if (tpix != 0) {
for (int i = 0; i < width; i++) {
rasline[i] = tpix;
}
}
// clear saved_image using transparent pixels
// this will be used as the background in the next display
if( decoder.saved_image != null ) {
for( int i = 0; i < global_width * global_height; i ++ )
decoder.saved_image[i] = tpix;
}
setPixels(x, y, width, height, model, rasline, 0, 0);
break;
case DISPOSAL_SAVE:
decoder.saved_model = model;
break;
}
}
return true;
}
}