/**
* It used to be against patent laws to produce unlicensed
* GIF images. Now the Unisys patents have all expired.
* Java now has a GIFEncoder built-in with ImageIO.write.
* This code shows you what is going on under the covers to create the image.
* --
* Greg Faron
* Integre Technical Publishing Co.
*/
import java.awt.AWTException;
import java.awt.Image;
import java.awt.image.PixelGrabber;
import java.io.IOException;
import java.io.OutputStream;
/**
* Class <CODE>GIFEncoder</CODE> takes an <CODE>Image</CODE> and
* saves it to a file using the <CODE>GIF</CODE> file format
* (<A HREF="http://www.dcs.ed.ac.uk/%7Emxr/gfx/">Graphics Interchange
* Format</A>).
* A <CODE>GIFEncoder</CODE> object is constructed with either an
* <CODE>Image</CODE> object (which must be fully loaded), or a set of
* three 2-dimensional <CODE>byte</CODE> arrays. The image file can be
* written out with a call to {@link #write(OutputStream) write()}.<p>
* Three caveats:
* <UL>
* <LI>Class <CODE>GIFEncoder</CODE> will convert the image to
* indexed color upon
* construction. This will take some time, depending on the size of
* the image.
* Also, the act of writing the image to disk will take some
* time.</LI>
* <LI>The image cannot have more than 256 colors, since GIF is an 8
* bit format. For a 24 bit to 8 bit quantization algorithm, see
* Graphics Gems II III.2 by <A
* HREF="http://www.csd.uwo.ca/faculty/wu/">Xialoin Wu</A>.
* Or check out his <A HREF="http://www.csd.uwo.ca/faculty/wu/cq.c">C
* source</A>.</LI>
* <LI>Since the image must be completely loaded into memory,
* <CODE>GIFEncoder</CODE>
* may have problems with large images. Attempting to encode an
* image which will not
* fit into memory will probably result in the following
* exception:<BR>
* <CODE>java.awt.AWTException: Grabber returned false:
* 192</CODE></LI>
* </UL>
* <CODE>GIFEncoder</CODE> is based upon gifsave.c, which was written
* and released by:<p>
* <DIV ALIGN="CENTER">
* Sverre H. Huseby<BR>
* Bjoelsengt. 17<BR>
* N-0468 Oslo<BR>
* Norway<p>
* Phone: +47 2 230539<BR>
* <A HREF="mailto:sverrehu@ifi.uio.no">sverrehu@ifi.uio.no</A><BR>
* </DIV>
*
* @author Adam Doppelt (dead link <A
* @author Greg Faron - Integre Technical Publishing -
* @version 0.90 21 Apr 1996
*/
public class GIFEncoder extends Object
{
/**
* image height in pixels
*/
short imageWidth, imageHeight;
/**
* number of different colours in image
*/
int numberOfColors;
/**
* linear array of all pixels in row major order.
*/
byte[] allPixels = null;
/**
* list of all colours used in the image.
*/
byte[] allColors = null;
/**
* Convenience constructor for class <CODE>GIFEncoder</CODE>. The
* argument will
* be converted to an indexed color array.
* <B>This may take some time.</B>
*
* @param image The image to encode. The image <B>must</B> be
* completely loaded.
* @exception AWTException
* Will be thrown if the pixel grab fails.
* This can happen
* if Java runs out of memory. It may also
* indicate that the
* image contains more than 256 colors.
*/
public GIFEncoder( Image image )
throws AWTException
{
this.imageWidth = (short)image.getWidth( null );
this.imageHeight = (short)image.getHeight( null );
int values[] = new int[this.imageWidth * this.imageHeight];
PixelGrabber grabber = new PixelGrabber( image,
0, 0,
this.imageWidth,
this.imageHeight,
values, 0, this.imageWidth );
try
{
if ( grabber.grabPixels() != true )
throw new AWTException( "Grabber returned false: " +
grabber.status() );
}
catch ( InterruptedException ie )
{
}
byte[][] r = new byte[ this.imageWidth ][ this.imageHeight ];
byte[][] g = new byte[ this.imageWidth ][ this.imageHeight ];
byte[][] b = new byte[ this.imageWidth ][ this.imageHeight ];
int index = 0;
for ( int y=0; y<this.imageHeight; y++ )
{
for ( int x=0; x<this.imageWidth; x++, index++ )
{
r[ x ][ y ] = (byte)( ( values[ index ] >> 16 ) & 0xFF );
g[ x ][ y ] = (byte)( ( values[ index ] >> 8 ) & 0xFF );
b[ x ][ y ] = (byte)( ( values[ index ] ) & 0xFF );
}
}
this.toIndexColor( r, g, b );
}
/** Standard constructor for class <CODE>GIFEncoder</CODE>.
* Each array stores intensity
* values for the image. In other words,
* <NOBR><CODE>r[x][y]</CODE></NOBR> refers to
* the red intensity of the pixel at column <CODE>x</CODE>, row
* <CODE>y</CODE>.
* @param r A 2-dimensional array containing the red intensity values.
* @param g A 2-dimensional array containing the green intensity
* values.
* @param b A 2-dimensional array containing the blue intensity values.
* @exception AWTException Thrown if the image contains more than 256
* colors.
*/
public GIFEncoder( byte[][] r, byte[][] g, byte[][] b )
throws AWTException
{
this.imageWidth = (short)( r.length );
this.imageHeight = (short)( r[0].length );
this.toIndexColor( r, g, b );
}
/** Writes the image out to a stream in the <CODE>GIF</CODE> file
* format.
* This will be a single GIF87a image, non-interlaced, with no
* background color.
* <B>This may take some time.</B>
* @param output The stream to which to output. This should probably be
* a buffered stream.
* @exception IOException Thrown if a write operation fails.
*/
public void write( OutputStream output )
throws IOException
{
BitUtils.writeString( output, "GIF87a" );
ScreenDescriptor sd = new ScreenDescriptor( this.imageWidth,
this.imageHeight, this.numberOfColors );
sd.write( output );
output.write( this.allColors, 0, this.allColors.length) ;
ImageDescriptor id = new ImageDescriptor( this.imageWidth, this.imageHeight, ',' );
id.write( output );
byte codesize = BitUtils.BitsNeeded(this.numberOfColors);
if ( codesize == 1 )
{
codesize ++;
}
output.write( codesize );
LZWCompressor.LZWCompress( output, codesize, this.allPixels );
output.write( 0 );
id = new ImageDescriptor( (byte)0, (byte)0, ';' );
id.write( output );
output.flush();
}
/**
* Converts rgb desrcription of image to colour
* number description used by GIF.
*
* @param r red array of pixels
* @param g green array of pixels
* @param b blue array of pixels
* @exception AWTException
* Thrown if
* too many different colours in image.
*/
void toIndexColor( byte[][] r, byte[][] g, byte[][] b )
throws AWTException
{
this.allPixels = new byte[this.imageWidth * this.imageHeight];
this.allColors = new byte[256 * 3];
int colornum = 0;
for ( int x=0; x<this.imageWidth; x++ )
{
for ( int y=0; y<this.imageHeight; y++ )
{
int search;
for ( search=0; search < colornum; search++ )
{
if ( this.allColors[search * 3] == r[x][y] &&
this.allColors[search * 3 + 1] == g[x][y] &&
this.allColors[search * 3 + 2] == b[x][y] )
{
break;
}
}
if ( search > 255 )
throw new AWTException( "Too many colors." );
this.allPixels[ y * this.imageWidth + x ] = (byte)search;
if ( search == colornum )
{
this.allColors[ search * 3 ] = r[x][y];
this.allColors[ search * 3 + 1 ] = g[x][y];
this.allColors[ search * 3 + 2 ] = b[x][y];
colornum++;
}
}
}
this.numberOfColors = 1 << BitUtils.BitsNeeded( colornum );
byte copy[] = new byte[this.numberOfColors * 3];
System.arraycopy( this.allColors, 0, copy, 0, this.numberOfColors * 3 );
this.allColors = copy;
}
}
/**
* Used to write the bits composing the GIF image.
*/
class BitFile extends Object
{
/**
* The outputstream where the data is written.
*/
OutputStream output = null;
/**
* buffer for bits to write.
*/
byte[] buffer;
/**
*/
int indexIntoOutputStream, bitsLeft;
/**
* constructor
*
* @param output Where image will be written
*/
public BitFile( OutputStream output )
{
this.output = output;
this.buffer = new byte[ 256 ];
this.indexIntoOutputStream = 0;
this.bitsLeft = 8;
}
/**
* Ensures image in ram gets to disk.
*
* @exception IOException
*/
public void flush( )
throws IOException
{
int numBytes = this.indexIntoOutputStream + ( (this.bitsLeft == 8 ) ? 0 : 1 );
if ( numBytes > 0 )
{
this.output.write( numBytes );
this.output.write( this.buffer, 0, numBytes );
this.buffer[0] = 0;
this.indexIntoOutputStream = 0;
this.bitsLeft = 8;
}
}
/**
* Write bits to stream.
*
* @param bits source of bits, low/high order?
* @param numbits how many bits to write.
*
* @exception IOException
*/
public void writeBits( int bits, int numbits )
throws IOException
{
int bitsWritten = 0;
int numBytes = 255;
do
{
if ( (this.indexIntoOutputStream == 254 && this.bitsLeft == 0 ) ||
this.indexIntoOutputStream > 254 )
{
this.output.write( numBytes );
this.output.write( this.buffer, 0, numBytes );
this.buffer[0] = 0;
this.indexIntoOutputStream = 0;
this.bitsLeft = 8;
}
if ( numbits <= this.bitsLeft )
{
this.buffer[this.indexIntoOutputStream] |= ( bits & ( ( 1 << numbits ) - 1 ) ) << ( 8 - this.bitsLeft );
bitsWritten += numbits;
this.bitsLeft -= numbits;
numbits = 0;
}
else
{
this.buffer[ this.indexIntoOutputStream ] |= ( bits & ( ( 1 << this.bitsLeft ) - 1 ) ) << ( 8 - this.bitsLeft );
bitsWritten += this.bitsLeft;
bits >>= this.bitsLeft;
numbits -= this.bitsLeft;
this.buffer[ ++this.indexIntoOutputStream ] = 0;
this.bitsLeft = 8;
}
} while ( numbits != 0 );
}
}
/**
* Used to compress the image by looking for repeating
* elements.
*/
class LZWStringTable extends Object
{
private final static int RES_CODES = 2;
private final static short HASH_FREE = (short ) 0xFFFF;
private final static short NEXT_FIRST = (short ) 0xFFFF;
private final static int MAXBITS = 12;
private final static int MAXSTR = ( 1 << MAXBITS );
private final static short HASHSIZE = 9973;
private final static short HASHSTEP = 2039;
byte strChr_[];
short strNxt_[];
short strHsh_[];
short numStrings_;
public LZWStringTable( )
{
strChr_ = new byte[MAXSTR];
strNxt_ = new short[MAXSTR];
strHsh_ = new short[HASHSIZE];
}
public int addCharString( short index, byte b )
{
int hshidx;
if ( numStrings_ >= MAXSTR )
return 0xFFFF;
hshidx = Hash( index, b );
while ( strHsh_[hshidx] != HASH_FREE )
hshidx = ( hshidx + HASHSTEP ) % HASHSIZE;
strHsh_[hshidx] = numStrings_;
strChr_[numStrings_] = b;
strNxt_[numStrings_] = ( index != HASH_FREE ) ? index : NEXT_FIRST;
return numStrings_++;
}
public short findCharString( short index, byte b )
{
int hshidx, nxtidx;
if ( index == HASH_FREE )
return b;
hshidx = Hash( index, b );
while ( ( nxtidx = strHsh_[hshidx] ) != HASH_FREE )
{
if ( strNxt_[nxtidx] == index && strChr_[nxtidx] == b )
return(short ) nxtidx;
hshidx = ( hshidx + HASHSTEP ) % HASHSIZE;
}
return(short ) 0xFFFF;
}
public void ClearTable( int codesize )
{
numStrings_ = 0;
for ( int q = 0; q < HASHSIZE; q++ )
strHsh_[q] = HASH_FREE;
int w = ( 1 << codesize ) + RES_CODES;
for ( int q = 0; q < w; q++ )
this.addCharString( (short ) 0xFFFF, (byte)q );
}
public static int Hash( short index, byte lastbyte )
{
return( (int)( (short ) ( lastbyte << 8 ) ^ index ) & 0xFFFF ) %
HASHSIZE;
}
}
/**
* Used to compress the image by looking for repeated
* elements.
*/
class LZWCompressor extends Object
{
public static void LZWCompress( OutputStream output, int codesize, byte
toCompress[] )
throws IOException
{
byte c;
short index;
int clearcode, endofinfo, numbits, limit, errcode;
short prefix = (short ) 0xFFFF;
BitFile bitFile = new BitFile( output );
LZWStringTable strings = new LZWStringTable( );
clearcode = 1 << codesize;
endofinfo = clearcode + 1;
numbits = codesize + 1;
limit = ( 1 << numbits ) - 1;
strings.ClearTable( codesize );
bitFile.writeBits( clearcode, numbits );
for ( int loop = 0; loop < toCompress.length; loop++ )
{
c = toCompress[loop];
if ( ( index = strings.findCharString( prefix, c ) ) != -1 )
prefix = index;
else
{
bitFile.writeBits( prefix, numbits );
if ( strings.addCharString( prefix, c ) > limit )
{
if ( ++numbits > 12 )
{
bitFile.writeBits( clearcode, numbits - 1 );
strings.ClearTable( codesize );
numbits = codesize + 1;
}
limit = ( 1 << numbits ) - 1;
}
prefix = (short ) ( (short ) c & 0xFF );
}
}
if ( prefix != -1 )
bitFile.writeBits( prefix, numbits );
bitFile.writeBits( endofinfo, numbits );
bitFile.flush( );
}
}
/**
*/
class ScreenDescriptor extends Object
{
public short localScreenWidth, localScreenHeight;
private byte currentByte;
public byte backgroundColorIndex, pixelAspectRatio;
/**
* tool for dumping current screen image??
*
* @param width
* @param height
* @param numColors
*/
public ScreenDescriptor( short width, short height, int numColors )
{
this.localScreenWidth = width;
this.localScreenHeight = height;
SetGlobalColorTableSize( (byte)( BitUtils.BitsNeeded( numColors ) -
1 ) );
SetGlobalColorTableFlag( (byte)1 );
SetSortFlag( (byte)0 );
SetColorResolution( (byte)7 );
this.backgroundColorIndex = 0;
this.pixelAspectRatio = 0;
}
public void write(OutputStream output )
throws IOException
{
BitUtils.writeWord( output, this.localScreenWidth );
BitUtils.writeWord( output, this.localScreenHeight );
output.write( this.currentByte );
output.write( this.backgroundColorIndex );
output.write( this.pixelAspectRatio );
}
public void SetGlobalColorTableSize( byte num )
{
this.currentByte |= ( num & 7 );
}
public void SetSortFlag( byte num )
{
this.currentByte |= ( num & 1 ) << 3;
}
public void SetColorResolution( byte num )
{
this.currentByte |= ( num & 7 ) << 4;
}
public void SetGlobalColorTableFlag( byte num )
{
this.currentByte |= ( num & 1 ) << 7;
}
}
/**
*/
class ImageDescriptor extends Object
{
public byte separator_;
public short leftPosition, topPosition, imageWidth, imageHeight;
private byte currentByte;
/**
* ???
*
* @param width
* @param height
* @param separator
*/
public ImageDescriptor( short width, short height, char separator )
{
separator_ = (byte)separator;
this.leftPosition = 0;
this.topPosition = 0;
this.imageWidth = width;
this.imageHeight = height;
SetLocalColorTableSize( (byte)0 );
SetReserved( (byte)0 );
SetSortFlag( (byte)0 );
SetInterlaceFlag( (byte)0 );
SetLocalColorTableFlag( (byte)0 );
}
public void write( OutputStream output )
throws IOException
{
output.write( separator_ );
BitUtils.writeWord( output, this.leftPosition );
BitUtils.writeWord( output, this.topPosition );
BitUtils.writeWord( output, this.imageWidth );
BitUtils.writeWord( output, this.imageHeight );
output.write( this.currentByte );
}
public void SetLocalColorTableSize( byte num )
{
this.currentByte |= ( num & 7 );
}
public void SetReserved( byte num )
{
this.currentByte |= ( num & 3 ) << 3;
}
public void SetSortFlag( byte num )
{
this.currentByte |= ( num & 1 ) << 5;
}
public void SetInterlaceFlag( byte num )
{
this.currentByte |= ( num & 1 ) << 6;
}
public void SetLocalColorTableFlag( byte num )
{
this.currentByte |= ( num & 1 ) << 7;
}
}
class BitUtils extends Object
{
/**
* Bits needed no represent the number n???
*/
public static byte BitsNeeded( int n )
{
byte ret = 1;
if ( n-- == 0 )
return 0;
while ( ( n >>= 1 ) != 0 )
ret++;
return ret;
}
public static void writeWord( OutputStream output, short w ) throws IOException
{
output.write( w & 0xFF );
output.write( ( w >> 8 ) & 0xFF );
}
static void writeString( OutputStream output, String string ) throws IOException
{
for ( int loop = 0; loop < string.length(); loop++ )
{
output.write( (byte)( string.charAt( loop ) ) );
}
}
}