mirror of git://gcc.gnu.org/git/gcc.git
710 lines
18 KiB
Java
710 lines
18 KiB
Java
/* GIFFile.java -- GIF decoder
|
|
Copyright (C) 2006 Free Software Foundation, Inc.
|
|
|
|
This file is part of GNU Classpath.
|
|
|
|
GNU Classpath 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, or (at your option)
|
|
any later version.
|
|
|
|
GNU Classpath 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 GNU Classpath; see the file COPYING. If not, write to the
|
|
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
02110-1301 USA.
|
|
|
|
Linking this library statically or dynamically with other modules is
|
|
making a combined work based on this library. Thus, the terms and
|
|
conditions of the GNU General Public License cover the whole
|
|
combination.
|
|
|
|
As a special exception, the copyright holders of this library give you
|
|
permission to link this library with independent modules to produce an
|
|
executable, regardless of the license terms of these independent
|
|
modules, and to copy and distribute the resulting executable under
|
|
terms of your choice, provided that you also meet, for each linked
|
|
independent module, the terms and conditions of the license of that
|
|
module. An independent module is a module which is not derived from
|
|
or based on this library. If you modify this library, you may extend
|
|
this exception to your version of the library, but you are not
|
|
obligated to do so. If you do not wish to do so, delete this
|
|
exception statement from your version. */
|
|
|
|
package gnu.javax.imageio.gif;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.Vector;
|
|
|
|
/**
|
|
* GIFFile - reads a GIF file.
|
|
*
|
|
* This class only does the bare minimum work, and returns the data in raw
|
|
* formats (described below). The class is J2ME compatible, and hopefully
|
|
* we can keep it that way without any significant overhead.
|
|
*
|
|
* @author Sven de Marothy.
|
|
*/
|
|
public class GIFFile
|
|
{
|
|
// "NETSCAPE2.0" - identifier
|
|
private final static byte[] nsBlock = new byte[]
|
|
{0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30 };
|
|
|
|
/**
|
|
* Block identifiers
|
|
*/
|
|
private final static int EXTENSION = 0x21;
|
|
private final static int LOCAL = 0x2C;
|
|
private final static int TERMINATOR = 0x3B;
|
|
|
|
/**
|
|
* Extension block types
|
|
*/
|
|
private final static int EXTENSION_COMMENT = 254;
|
|
private final static int EXTENSION_GCONTROL = 249;
|
|
private final static int EXTENSION_APPLICATION = 255;
|
|
|
|
/**
|
|
* Undraw commands for animation.
|
|
*/
|
|
private final static int UNDRAW_OVERWRITE = 1;
|
|
private final static int UNDRAW_RESTORE_BACKGROUND = 2;
|
|
private final static int UNDRAW_RESTORE_PREVIOUS = 3;
|
|
|
|
/**
|
|
* Image position and dimensions (images may be partial)
|
|
*/
|
|
private int x, y, width, height;
|
|
|
|
/**
|
|
* Global dimensions
|
|
*/
|
|
private int globalWidth, globalHeight;
|
|
|
|
/**
|
|
* Background color index.
|
|
*/
|
|
private byte bgIndex;
|
|
|
|
/**
|
|
* Number of colors
|
|
*/
|
|
private int nColors;
|
|
|
|
/**
|
|
* Global palette, if any
|
|
*/
|
|
private byte[] globalPalette;
|
|
|
|
/**
|
|
* Any
|
|
*/
|
|
private boolean hasGlobalColorMap;
|
|
|
|
/**
|
|
* Local palette, if any (used if available)
|
|
*/
|
|
private byte[] localPalette;
|
|
|
|
/**
|
|
* Interlaced GIF or not?
|
|
*/
|
|
private boolean interlaced;
|
|
|
|
/**
|
|
* Has transparency?
|
|
*/
|
|
private boolean hasTransparency;
|
|
|
|
/**
|
|
* Undraw mode (animations)
|
|
*/
|
|
private int undraw;
|
|
|
|
/**
|
|
* Transparent index;
|
|
*/
|
|
private int transparentIndex;
|
|
|
|
/**
|
|
* The uncompressed raster
|
|
*/
|
|
private byte[] raster;
|
|
|
|
/**
|
|
* The compressed data (freed after uncompressing)
|
|
*/
|
|
private byte[] compressedData;
|
|
|
|
/**
|
|
* Frame delay in 100ths of a second ( centiseconds, metrically )
|
|
*/
|
|
private int duration;
|
|
|
|
/**
|
|
* Indices used during decompression
|
|
*/
|
|
private int dataBlockIndex;
|
|
|
|
/**
|
|
* The file comment , if a comment block exists.
|
|
*/
|
|
private String comment;
|
|
|
|
/**
|
|
* Fields used by getBits()
|
|
*/
|
|
private int remainingBits = 0;
|
|
private int currentBits = 0;
|
|
|
|
/**
|
|
* Netscape animation extension
|
|
*/
|
|
private boolean isLooped = false;
|
|
|
|
/** Number of loops, 0 = infinite */
|
|
private int loops;
|
|
|
|
/**
|
|
* Additional frames if it's an animated GIF.
|
|
*/
|
|
private Vector animationFrames;
|
|
|
|
/**
|
|
* Loads the file from an input stream, which is not closed.
|
|
* @throws IOException if an I/O error occured.
|
|
* @throws GIFException if some file parsing error occured
|
|
*/
|
|
public GIFFile(InputStream in) throws IOException, GIFException
|
|
{
|
|
// Validate the signature
|
|
if( !readSignature( in ) )
|
|
throw new GIFException("Invalid GIF signature.");
|
|
|
|
{
|
|
byte[] data = new byte[7];
|
|
if (in.read(data) != 7)
|
|
throw new IOException("Couldn't read global descriptor.");
|
|
|
|
globalWidth = ((data[1] & 0xFF) << 8) | (data[0] & 0xFF);
|
|
globalHeight = ((data[3] & 0xFF) << 8) | (data[2] & 0xFF);
|
|
byte flags = data[4];
|
|
bgIndex = data[5];
|
|
nColors = (1 << (( flags & 0x07) + 1));
|
|
hasGlobalColorMap = ((flags & 0x80) != 0);
|
|
}
|
|
|
|
if( hasGlobalColorMap )
|
|
{
|
|
globalPalette = new byte[ nColors * 3 ];
|
|
if( in.read( globalPalette ) != nColors * 3 )
|
|
throw new IOException("Couldn't read color map.");
|
|
}
|
|
|
|
int c = in.read();
|
|
while( c == EXTENSION )
|
|
{
|
|
readExtension( in );
|
|
c = in.read();
|
|
}
|
|
|
|
if( c != LOCAL )
|
|
throw new GIFException("Extension blocks not followed by a local descriptor ("+c+")");
|
|
|
|
loadImage( in );
|
|
c = in.read();
|
|
|
|
if( c == TERMINATOR ) // Not an animated GIF.
|
|
return;
|
|
|
|
// Load animation frames. Just quit if an error occurs instead
|
|
// of throwing an exception.
|
|
animationFrames = new Vector();
|
|
try
|
|
{
|
|
while( c != TERMINATOR )
|
|
{
|
|
animationFrames.add( new GIFFile( this, in, c ) );
|
|
c = in.read();
|
|
}
|
|
}
|
|
catch(IOException ioe)
|
|
{
|
|
}
|
|
catch(GIFException gife)
|
|
{
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructor for additional animation frames.
|
|
*/
|
|
private GIFFile(GIFFile parent, InputStream in, int c)
|
|
throws IOException, GIFException
|
|
{
|
|
// Copy global properties.
|
|
globalWidth = parent.globalWidth;
|
|
globalHeight = parent.globalHeight;
|
|
nColors = parent.nColors;
|
|
globalPalette = parent.globalPalette;
|
|
hasGlobalColorMap = parent.hasGlobalColorMap;
|
|
interlaced = parent.interlaced;
|
|
comment = parent.comment;
|
|
isLooped = parent.isLooped;
|
|
loops = parent.loops;
|
|
|
|
while( c == EXTENSION )
|
|
{
|
|
readExtension( in );
|
|
c = in.read();
|
|
}
|
|
|
|
if( c != LOCAL )
|
|
throw new GIFException("Extension blocks not followed by a local descriptor ("+c+")");
|
|
|
|
loadImage( in );
|
|
}
|
|
|
|
/**
|
|
* Reads a GIF file signature from an inputstream and checks it.
|
|
*
|
|
* @param in - the stream (reads 6 bytes, does not close or reset).
|
|
* @return true if the signature is a valid GIF signature.
|
|
* @throws IOException if the signature could not be read.
|
|
*/
|
|
public static boolean readSignature( InputStream in ) throws IOException
|
|
{
|
|
byte[] data = new byte[6];
|
|
if (in.read(data) != 6)
|
|
throw new IOException("Couldn't read signature.");
|
|
|
|
if( data[0] != 0x47 || data[1] != 0x49 || data[2] != 0x46 ||
|
|
data[3] != 0x38 ) // GIF8
|
|
return false;
|
|
|
|
if( (data[4] != 0x39 && data[4] != 0x37) || // 7 | 9
|
|
(data[5] != 0x61 && data[5] != 0x62) ) // 'a' or 'b'
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Loads the image local descriptor and then loads/decodes the image raster,
|
|
* and then performs any necessary postprocessing like deinterlacing.
|
|
*/
|
|
private void loadImage(InputStream in)
|
|
throws IOException, GIFException
|
|
{
|
|
readLocal( in );
|
|
|
|
try
|
|
{
|
|
decodeRaster( in );
|
|
}
|
|
catch(ArrayIndexOutOfBoundsException aioobe)
|
|
{
|
|
throw new GIFException("Error decompressing image.");
|
|
}
|
|
|
|
if( interlaced ) // Clean up
|
|
deinterlace();
|
|
packPixels();
|
|
}
|
|
|
|
/**
|
|
* Pack the pixels if it's a 2, 4 or 16 color image.
|
|
* While GIF may support any number of colors from 2-256, we won't bother
|
|
* trying to pack pixels not resulting in even byte boundaries.
|
|
* (AWT doesn't support that anyway, and most apps do the same.)
|
|
*/
|
|
private void packPixels()
|
|
{
|
|
if( nColors != 2 && nColors != 4 && nColors != 16 )
|
|
return;
|
|
|
|
int nbits = 1;
|
|
int ppbyte = 8;
|
|
if( nColors == 4 )
|
|
{
|
|
nbits = 2;
|
|
ppbyte = 4;
|
|
}
|
|
else if( nColors == 16 )
|
|
{
|
|
nbits = 4;
|
|
ppbyte = 2;
|
|
}
|
|
|
|
int rem = (width & (ppbyte - 1));
|
|
int w = ( rem == 0 ) ? (width / ppbyte) :
|
|
((width + ppbyte - rem) / ppbyte);
|
|
byte[] nr = new byte[ w * height ];
|
|
for(int j = 0; j < height; j++)
|
|
{
|
|
for(int i = 0; i < width - ppbyte; i += ppbyte)
|
|
for(int k = 0; k < ppbyte; k++)
|
|
nr[ j * w + (i / ppbyte) ] |= (byte)((raster[ width * j + i + k ]
|
|
<< (8 - nbits * (1 + k))));
|
|
for(int i = 0; i < rem; i++)
|
|
nr[ j * w + w - 1 ] |= (byte)((raster[ width * j + width - rem + i ]
|
|
<< (nbits * (rem - i))));
|
|
}
|
|
raster = nr;
|
|
}
|
|
|
|
/**
|
|
* Returns the (global) width
|
|
*/
|
|
public int getWidth()
|
|
{
|
|
return width;
|
|
}
|
|
|
|
/**
|
|
* Returns the image height
|
|
*/
|
|
public int getHeight()
|
|
{
|
|
return height;
|
|
}
|
|
|
|
/**
|
|
* Returns the # of colors.
|
|
*/
|
|
public int getNColors()
|
|
{
|
|
return nColors;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the GIF has transparency.
|
|
*/
|
|
public boolean hasTransparency()
|
|
{
|
|
return hasTransparency;
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the transparent color.
|
|
*/
|
|
public int getTransparentIndex()
|
|
{
|
|
return transparentIndex;
|
|
}
|
|
|
|
/**
|
|
* Retuns the GIF file comment, or null if none exists.
|
|
*/
|
|
public String getComment()
|
|
{
|
|
return comment;
|
|
}
|
|
|
|
/**
|
|
* Get duration of the frame for animations.
|
|
*/
|
|
public int getDuration()
|
|
{
|
|
return duration;
|
|
}
|
|
|
|
/**
|
|
* Deinterlaces the image.
|
|
*/
|
|
private void deinterlace()
|
|
{
|
|
byte[] nr = new byte[ width * height ];
|
|
int n = 0;
|
|
for(int i = 0; i < ((height + 7) >> 3); i++)
|
|
{
|
|
System.arraycopy( raster, n, nr, width * i * 8, width );
|
|
n += width;
|
|
}
|
|
for(int i = 0; i < ((height + 3) >> 3); i++)
|
|
{
|
|
System.arraycopy( raster, n, nr, width * ( 8 * i + 4 ), width );
|
|
n += width;
|
|
}
|
|
for(int i = 0; i < (height >> 2); i++)
|
|
{
|
|
System.arraycopy( raster, n, nr, width * (4 * i + 2), width );
|
|
n += width;
|
|
}
|
|
for(int i = 0; i < (height >> 1); i++)
|
|
{
|
|
System.arraycopy( raster, n, nr, width * (2 * i + 1), width );
|
|
n += width;
|
|
}
|
|
raster = nr;
|
|
}
|
|
|
|
/**
|
|
* Reads the local descriptor
|
|
*/
|
|
private void readLocal(InputStream in) throws IOException
|
|
{
|
|
byte[] data = new byte[9];
|
|
if (in.read(data) != 9)
|
|
throw new IOException("Couldn't read local descriptor.");
|
|
x = ((data[1] & 0xFF) << 8) | (data[0] & 0xFF);
|
|
y = ((data[3] & 0xFF) << 8) | (data[2] & 0xFF);
|
|
width = ((data[5] & 0xFF) << 8) | (data[4] & 0xFF);
|
|
height = ((data[7] & 0xFF) << 8) | (data[6] & 0xFF);
|
|
byte flags = data[8];
|
|
interlaced = (( flags & 0x40 ) != 0);
|
|
if( (flags & 0x80) != 0 )
|
|
{ // has a local color map
|
|
int nLocalColors = (1 << (( flags & 0x07) + 1));
|
|
if( !hasGlobalColorMap )
|
|
nColors = nLocalColors;
|
|
localPalette = new byte[ nLocalColors * 3 ];
|
|
if( in.read( localPalette ) != nLocalColors * 3 )
|
|
throw new IOException("Couldn't read color map.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the image's palette in raw format
|
|
* (r0,g0,b0,r1,g1,b2..r(Ncolors-1),g(Ncolors-1),b(Ncolors-1))
|
|
*/
|
|
public byte[] getRawPalette()
|
|
{
|
|
return hasGlobalColorMap ? globalPalette : localPalette;
|
|
}
|
|
|
|
/**
|
|
* Returns the image file for animated gifs.
|
|
*/
|
|
public GIFFile getImage( int index )
|
|
{
|
|
if( index == 0 )
|
|
return this;
|
|
if( animationFrames == null )
|
|
throw new ArrayIndexOutOfBoundsException("Only one image in file");
|
|
return (GIFFile)animationFrames.elementAt( index - 1 );
|
|
}
|
|
|
|
/**
|
|
* Return the image's raw image data.
|
|
* If the color depth is 1,2 or 4 bits per pixel the pixels are packed
|
|
* and the scanlines padded up to the nearest byte if needed.
|
|
*/
|
|
public byte[] getRawImage()
|
|
{
|
|
return raster;
|
|
}
|
|
|
|
/**
|
|
* Return the number of images in the GIF file
|
|
*/
|
|
public int nImages()
|
|
{
|
|
if( animationFrames != null )
|
|
return 1 + animationFrames.size();
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Handles extension blocks.
|
|
*/
|
|
private void readExtension(InputStream in) throws IOException, GIFException
|
|
{
|
|
int functionCode = in.read();
|
|
byte[] data = readData(in);
|
|
switch( functionCode )
|
|
{
|
|
case EXTENSION_COMMENT: // comment block
|
|
comment = new String(data, "8859_1");
|
|
break;
|
|
|
|
case EXTENSION_GCONTROL: // Graphics control extension
|
|
undraw = (data[0] & 0x1C) >> 2;
|
|
// allegedly there can be bad values of this.
|
|
if( undraw < 1 && undraw > 3 ) undraw = 1;
|
|
hasTransparency = ((data[0] & 0x01) == 1);
|
|
transparentIndex = (data[3] & 0xFF);
|
|
duration = ((data[2] & 0xFF) << 8) | (data[1] & 0xFF);
|
|
break;
|
|
|
|
// Application extension. We only parse the Netscape animation
|
|
// extension here. Which is the only one most use anyway.
|
|
case EXTENSION_APPLICATION:
|
|
boolean isNS = true;
|
|
for(int i = 0; i < nsBlock.length; i++ )
|
|
if( nsBlock[i] != data[i] )
|
|
isNS = false;
|
|
if( isNS )
|
|
{
|
|
isLooped = true;
|
|
loops = ((data[12] & 0xFF) << 8) | (data[13] & 0xFF);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads a series of data blocks and merges them into a single one.
|
|
*/
|
|
private byte[] readData(InputStream in) throws IOException
|
|
{
|
|
Vector v = new Vector();
|
|
int totalBytes = 0;
|
|
|
|
int n = in.read();
|
|
do
|
|
{
|
|
totalBytes += n;
|
|
byte[] block = new byte[ n ];
|
|
in.read(block);
|
|
v.add(block);
|
|
n = in.read();
|
|
}
|
|
while( n > 0 );
|
|
|
|
n = 0;
|
|
byte[] bigBuffer = new byte[ totalBytes ];
|
|
for( int i = 0; i < v.size(); i++ )
|
|
{
|
|
byte[] block = (byte[])v.elementAt(i);
|
|
System.arraycopy(block, 0, bigBuffer, n, block.length);
|
|
n += block.length;
|
|
}
|
|
return bigBuffer;
|
|
}
|
|
|
|
/**
|
|
* Loads a compressed image block and decompresses it.
|
|
*/
|
|
private void decodeRaster(InputStream in) throws IOException
|
|
{
|
|
int initialCodeSize = in.read();
|
|
compressedData = readData( in );
|
|
dataBlockIndex = 0;
|
|
|
|
int rasterIndex = 0; // Index into the raster
|
|
int clearCode = (1 << initialCodeSize); // 256 usually
|
|
int endCode = clearCode + 1; // The stop code.
|
|
|
|
raster = new byte[ width * height ];
|
|
|
|
int codeSize = initialCodeSize + 1;
|
|
int code = getBits( codeSize ); // = clear
|
|
int nextCode = endCode + 1;
|
|
|
|
/*
|
|
* Initialize LZW dictionary
|
|
*
|
|
* First index - code #
|
|
* Second index:
|
|
* 0 = color index
|
|
* 1 = parent (-1 - no parent)
|
|
* 2 = first value
|
|
* 3 - depth
|
|
* The latter two aren't strictly necessary but make things faster, since
|
|
* copying the values forward is faster than going back and looking.
|
|
*/
|
|
short[][] dictionary = new short[ 4096 ][ 4 ];
|
|
|
|
for(short i = 0; i < nColors; i ++ )
|
|
{
|
|
dictionary[i][0] = i; // color index
|
|
dictionary[i][1] = -1; // parent
|
|
dictionary[i][2] = i; // first
|
|
dictionary[i][3] = 1; // depth
|
|
}
|
|
|
|
code = getBits( codeSize ); // get second code
|
|
raster[ rasterIndex++ ] = (byte)dictionary[code][0];
|
|
int old = code;
|
|
code = getBits( codeSize ); // start at the third code
|
|
int c;
|
|
|
|
do
|
|
{
|
|
if( code == clearCode )
|
|
{
|
|
codeSize = initialCodeSize + 1;
|
|
nextCode = endCode + 1;
|
|
// get and output second code
|
|
code = getBits( codeSize );
|
|
raster[ rasterIndex++ ] = (byte)dictionary[code][0];
|
|
old = code;
|
|
}
|
|
else
|
|
{
|
|
dictionary[nextCode][1] = (short)old; // parent = old
|
|
dictionary[nextCode][2] = dictionary[old][2]; // first pixel
|
|
dictionary[nextCode][3] = (short)(dictionary[old][3] + 1); // depth
|
|
|
|
// appended pixel = first pixel of c
|
|
if( code < nextCode )
|
|
{
|
|
dictionary[nextCode][0] = dictionary[code][2];
|
|
old = code;
|
|
}
|
|
else // first of old
|
|
{
|
|
dictionary[nextCode][0] = dictionary[old][2];
|
|
old = nextCode;
|
|
}
|
|
|
|
c = old;
|
|
// output the code c
|
|
int depth = dictionary[c][3];
|
|
for( int i = depth - 1; i >= 0; i-- )
|
|
{
|
|
raster[ rasterIndex + i ] = (byte)dictionary[c][0];
|
|
c = dictionary[c][1]; // go to parent.
|
|
}
|
|
rasterIndex += depth;
|
|
nextCode ++;
|
|
|
|
if( codeSize < 12 && nextCode >= (1 << codeSize) )
|
|
codeSize++;
|
|
}
|
|
code = getBits( codeSize );
|
|
}
|
|
while( code != endCode && dataBlockIndex < compressedData.length );
|
|
|
|
compressedData = null; // throw away compressed data.
|
|
}
|
|
|
|
/**
|
|
* Returns nbits number of bits (in the LSBs) from compressedData
|
|
*/
|
|
private int getBits( int nbits )
|
|
{
|
|
while( nbits > remainingBits )
|
|
{
|
|
int c = (compressedData[ dataBlockIndex++ ] & 0xFF) << remainingBits;
|
|
currentBits |= c;
|
|
remainingBits += 8;
|
|
}
|
|
int rval = (currentBits & ((1 << nbits) - 1));
|
|
currentBits = (currentBits >> nbits);
|
|
remainingBits -= nbits;
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Generic exception used by GIFFile to report decoding errors.
|
|
*/
|
|
public static class GIFException extends Exception
|
|
{
|
|
public GIFException(String message)
|
|
{
|
|
super(message);
|
|
}
|
|
}
|
|
}
|