//******************************************************************************
// Gif89Encoder.java
//******************************************************************************
//package net.jmge.gif;
package com.idega.graphics.encoder.gif;
import java.awt.*;
import java.io.*;
import java.util.Vector;
//==============================================================================
/** This is the central class of a JDK 1.1 compatible GIF encoder that, AFAIK,
* supports more features of the extended GIF spec than any other Java open
* source encoder. Some sections of the source are lifted or adapted from Jef
* Poskanzer's <cite>Acme GifEncoder</cite> (so please see the
* <a href="../readme.txt">readme</a> containing his notice), but much of it,
* including nearly all of the present class, is original code. My main
* motivation for writing a new encoder was to support animated GIFs, but the
* package also adds support for embedded textual comments.
* <p>
* There are still some limitations. For instance, animations are limited to
* a single global color table. But that is usually what you want anyway, so
* as to avoid irregularities on some displays. (So this is not really a
* limitation, but a "disciplinary feature" :) Another rather more serious
* restriction is that the total number of RGB colors in a given input-batch
* mustn't exceed 256. Obviously, there is an opening here for someone who
* would like to add a color-reducing preprocessor.
* <p>
* The encoder, though very usable in its present form, is at bottom only a
* partial implementation skewed toward my own particular needs. Hence a
* couple of caveats are in order. (1) During development it was in the back
* of my mind that an encoder object should be reusable - i.e., you should be
* able to make multiple calls to encode() on the same object, with or without
* intervening frame additions or changes to options. But I haven't reviewed
* the code with such usage in mind, much less tested it, so it's likely I
* overlooked something. (2) The encoder classes aren't thread safe, so use
* caution in a context where access is shared by multiple threads. (Better
* yet, finish the library and re-release it :)
* <p>
* There follow a couple of simple examples illustrating the most common way to
* use the encoder, i.e., to encode AWT Image objects created elsewhere in the
* program. Use of some of the most popular format options is also shown,
* though you will want to peruse the API for additional features.
*
* <p>
* <strong>Animated GIF Example</strong>
* <pre>
* import net.jmge.gif.Gif89Encoder;
* // ...
* void writeAnimatedGIF(Image[] still_images,
* String annotation,
* boolean looped,
* double frames_per_second,
* OutputStream out) throws IOException
* {
* Gif89Encoder gifenc = new Gif89Encoder();
* for (int i = 0; i < still_images.length; ++i)
* gifenc.addFrame(still_images[i]);
* gifenc.setComments(annotation);
* gifenc.setLoopCount(looped ? 0 : 1);
* gifenc.setUniformDelay((int) Math.round(100 / frames_per_second));
* gifenc.encode(out);
* }
* </pre>
*
* <strong>Static GIF Example</strong>
* <pre>
* import net.jmge.gif.Gif89Encoder;
* // ...
* void writeNormalGIF(Image img,
* String annotation,
* int transparent_index, // pass -1 for none
* boolean interlaced,
* OutputStream out) throws IOException
* {
* Gif89Encoder gifenc = new Gif89Encoder(img);
* gifenc.setComments(annotation);
* gifenc.setTransparentIndex(transparent_index);
* gifenc.getFrameAt(0).setInterlaced(interlaced);
* gifenc.encode(out);
* }
* </pre>
*
* @version 0.90 beta (15-Jul-2000)
* @author J. M. G. Elliott (tep@jmge.net)
* @see Gif89Frame
* @see DirectGif89Frame
* @see IndexGif89Frame
*/
public class Gif89Encoder {
private Dimension dispDim = new Dimension(0, 0);
/**
*
* @uml.property name="colorTable"
* @uml.associationEnd multiplicity="(1 1)"
*/
private GifColorTable colorTable;
private int bgIndex = 0;
private int loopCount = 1;
private String theComments;
/**
*
* @uml.property name="vFrames"
* @uml.associationEnd multiplicity="(0 -1)" elementType="com.idega.graphics.encoder.gif.Gif89Frame"
*/
private Vector vFrames = new Vector();
//----------------------------------------------------------------------------
/** Use this default constructor if you'll be adding multiple frames
* constructed from RGB data (i.e., AWT Image objects or ARGB-pixel arrays).
*/
public Gif89Encoder()
{
// empty color table puts us into "palette autodetect" mode
this.colorTable = new GifColorTable();
}
//----------------------------------------------------------------------------
/** Like the default except that it also adds a single frame, for conveniently
* encoding a static GIF from an image.
*
* @param static_image
* Any Image object that supports pixel-grabbing.
* @exception IOException
* See the addFrame() methods.
*/
public Gif89Encoder(Image static_image) throws IOException
{
this();
addFrame(static_image);
}
//----------------------------------------------------------------------------
/** This constructor installs a user color table, overriding the detection of
* of a palette from ARBG pixels.
*
* Use of this constructor imposes a couple of restrictions:
* (1) Frame objects can't be of type DirectGif89Frame
* (2) Transparency, if desired, must be set explicitly.
*
* @param colors
* Array of color values; no more than 256 colors will be read, since that's
* the limit for a GIF.
*/
public Gif89Encoder(Color[] colors)
{
this.colorTable = new GifColorTable(colors);
}
//----------------------------------------------------------------------------
/** Convenience constructor for encoding a static GIF from index-model data.
* Adds a single frame as specified.
*
* @param colors
* Array of color values; no more than 256 colors will be read, since
* that's the limit for a GIF.
* @param width
* Width of the GIF bitmap.
* @param height
* Height of same.
* @param ci_pixels
* Array of color-index pixels no less than width * height in length.
* @exception IOException
* See the addFrame() methods.
*/
public Gif89Encoder(Color[] colors, int width, int height, byte ci_pixels[])
throws IOException
{
this(colors);
addFrame(width, height, ci_pixels);
}
//----------------------------------------------------------------------------
/** Get the number of frames that have been added so far.
*
* @return
* Number of frame items.
*/
public int getFrameCount() { return this.vFrames.size(); }
//----------------------------------------------------------------------------
/** Get a reference back to a Gif89Frame object by position.
*
* @param index
* Zero-based index of the frame in the sequence.
* @return
* Gif89Frame object at the specified position (or null if no such frame).
*/
public Gif89Frame getFrameAt(int index)
{
return isOk(index) ? (Gif89Frame) this.vFrames.elementAt(index) : null;
}
//----------------------------------------------------------------------------
/** Add a Gif89Frame frame to the end of the internal sequence. Note that
* there are restrictions on the Gif89Frame type: if the encoder object was
* constructed with an explicit color table, an attempt to add a
* DirectGif89Frame will throw an exception.
*
* @param gf
* An externally constructed Gif89Frame.
* @exception IOException
* If Gif89Frame can't be accommodated. This could happen if either (1) the
* aggregate cross-frame RGB color count exceeds 256, or (2) the Gif89Frame
* subclass is incompatible with the present encoder object.
*/
public void addFrame(Gif89Frame gf) throws IOException
{
accommodateFrame(gf);
this.vFrames.addElement(gf);
}
//----------------------------------------------------------------------------
/** Convenience version of addFrame() that takes a Java Image, internally
* constructing the requisite DirectGif89Frame.
*
* @param image
* Any Image object that supports pixel-grabbing.
* @exception IOException
* If either (1) pixel-grabbing fails, (2) the aggregate cross-frame RGB
* color count exceeds 256, or (3) this encoder object was constructed with
* an explicit color table.
*/
public void addFrame(Image image) throws IOException
{
addFrame(new DirectGif89Frame(image));
}
//----------------------------------------------------------------------------
/** The index-model convenience version of addFrame().
*
* @param width
* Width of the GIF bitmap.
* @param height
* Height of same.
* @param ci_pixels
* Array of color-index pixels no less than width * height in length.
* @exception IOException
* Actually, in the present implementation, there aren't any unchecked
* exceptions that can be thrown when adding an IndexGif89Frame
* <i>per se</i>. But I might add some pedantic check later, to justify the
* generality :)
*/
public void addFrame(int width, int height, byte ci_pixels[])
throws IOException
{
addFrame(new IndexGif89Frame(width, height, ci_pixels));
}
//----------------------------------------------------------------------------
/** Like addFrame() except that the frame is inserted at a specific point in
* the sequence rather than appended.
*
* @param index
* Zero-based index at which to insert frame.
* @param gf
* An externally constructed Gif89Frame.
* @exception IOException
* If Gif89Frame can't be accommodated. This could happen if either (1)
* the aggregate cross-frame RGB color count exceeds 256, or (2) the
* Gif89Frame subclass is incompatible with the present encoder object.
*/
public void insertFrame(int index, Gif89Frame gf) throws IOException
{
accommodateFrame(gf);
this.vFrames.insertElementAt(gf, index);
}
//----------------------------------------------------------------------------
/** Set the color table index for the transparent color, if any.
*
* @param index
* Index of the color that should be rendered as transparent, if any.
* A value of -1 turns off transparency. (Default: -1)
*/
public void setTransparentIndex(int index)
{
this.colorTable.setTransparent(index);
}
public void setTransparentColor(Color color)
{
this.colorTable.setTransparent(color);
}
//----------------------------------------------------------------------------
/** Sets attributes of the multi-image display area, if applicable.
*
* @param dim
* Width/height of display. (Default: largest detected frame size)
* @param background
* Color table index of background color. (Default: 0)
* @see Gif89Frame#setPosition
*/
public void setLogicalDisplay(Dimension dim, int background)
{
this.dispDim = new Dimension(dim);
this.bgIndex = background;
}
//----------------------------------------------------------------------------
/**
* Set animation looping parameter, if applicable.
*
* @param count
* Number of times to play sequence. Special value of 0 specifies
* indefinite looping. (Default: 1)
*
* @uml.property name="loopCount"
*/
public void setLoopCount(int count) {
this.loopCount = count;
}
//----------------------------------------------------------------------------
/** Specify some textual comments to be embedded in GIF.
*
* @param comments
* String containing ASCII comments.
*/
public void setComments(String comments)
{
this.theComments = comments;
}
//----------------------------------------------------------------------------
/** A convenience method for setting the "animation speed". It simply sets
* the delay parameter for each frame in the sequence to the supplied value.
* Since this is actually frame-level rather than animation-level data, take
* care to add your frames before calling this method.
*
* @param interval
* Interframe interval in centiseconds.
*/
public void setUniformDelay(int interval)
{
for (int i = 0; i < this.vFrames.size(); ++i) {
((Gif89Frame) this.vFrames.elementAt(i)).setDelay(interval);
}
}
//----------------------------------------------------------------------------
/** After adding your frame(s) and setting your options, simply call this
* method to write the GIF to the passed stream. Multiple calls are
* permissible if for some reason that is useful to your application. (The
* method simply encodes the current state of the object with no thought
* to previous calls.)
*
* @param out
* The stream you want the GIF written to.
* @exception IOException
* If a write error is encountered.
*/
public void encode(OutputStream out) throws IOException
{
int nframes = getFrameCount();
boolean is_sequence = nframes > 1;
// N.B. must be called before writing screen descriptor
this.colorTable.closePixelProcessing();
// write GIF HEADER
Put.ascii("GIF89a", out);
// write global blocks
writeLogicalScreenDescriptor(out);
this.colorTable.encode(out);
if (is_sequence && this.loopCount != 1) {
writeNetscapeExtension(out);
}
if (this.theComments != null && this.theComments.length() > 0) {
writeCommentExtension(out);
}
// write out the control and rendering data for each frame
for (int i = 0; i < nframes; ++i) {
((Gif89Frame) this.vFrames.elementAt(i)).encode(
out, is_sequence, this.colorTable.getDepth(), this.colorTable.getTransparent()
);
}
// write GIF TRAILER
out.write(';');
out.flush();
}
//----------------------------------------------------------------------------
/** A simple driver to test the installation and to demo usage. Put the JAR
* on your classpath and run ala
* <blockquote>java net.jmge.gif.Gif89Encoder {filename}</blockquote>
* The filename must be either (1) a JPEG file with extension 'jpg', for
* conversion to a static GIF, or (2) a file containing a list of GIFs and/or
* JPEGs, one per line, to be combined into an animated GIF. The output will
* appear in the current directory as 'gif89out.gif'.
* <p>
* (N.B. This test program will abort if the input file(s) exceed(s) 256 total
* RGB colors, so in its present form it has no value as a generic JPEG to GIF
* converter. Also, when multiple files are input, you need to be wary of the
* total color count, regardless of file type.)
*
* @param args
* Command-line arguments, only the first of which is used, as mentioned
* above.
*/
public static void main(String[] args)
{
try {
Toolkit tk = Toolkit.getDefaultToolkit();
OutputStream out = new BufferedOutputStream(
new FileOutputStream("gif89out.gif")
);
if (args[0].toUpperCase().endsWith(".JPG")) {
new Gif89Encoder(tk.getImage(args[0])).encode(out);
}
else
{
BufferedReader in = new BufferedReader(new FileReader(args[0]));
Gif89Encoder ge = new Gif89Encoder();
String line;
while ((line = in.readLine()) != null) {
ge.addFrame(tk.getImage(line.trim()));
}
ge.setLoopCount(0); // let's loop indefinitely
ge.encode(out);
in.close();
}
out.close();
}
catch (Exception e) { e.printStackTrace(); }
finally { System.exit(0); } // must kill VM explicitly (Toolkit thread?)
}
//----------------------------------------------------------------------------
private void accommodateFrame(Gif89Frame gf) throws IOException
{
this.dispDim.width = Math.max(this.dispDim.width, gf.getWidth());
this.dispDim.height = Math.max(this.dispDim.height, gf.getHeight());
this.colorTable.processPixels(gf);
}
//----------------------------------------------------------------------------
private void writeLogicalScreenDescriptor(OutputStream os) throws IOException
{
Put.leShort(this.dispDim.width, os);
Put.leShort(this.dispDim.height, os);
// write 4 fields, packed into a byte (bitfieldsize:value)
// global color map present? (1:1)
// bits per primary color less 1 (3:7)
// sorted color table? (1:0)
// bits per pixel less 1 (3:varies)
os.write(0xf0 | this.colorTable.getDepth() - 1);
// write background color index
os.write(this.bgIndex);
// Jef Poskanzer's notes on the next field, for our possible edification:
// Pixel aspect ratio - 1:1.
//Putbyte( (byte) 49, outs );
// Java's GIF reader currently has a bug, if the aspect ratio byte is
// not zero it throws an ImageFormatException. It doesn't know that
// 49 means a 1:1 aspect ratio. Well, whatever, zero works with all
// the other decoders I've tried so it probably doesn't hurt.
// OK, if it's good enough for Jef, it's definitely good enough for us:
os.write(0);
}
//----------------------------------------------------------------------------
private void writeNetscapeExtension(OutputStream os) throws IOException
{
// n.b. most software seems to interpret the count as a repeat count
// (i.e., interations beyond 1) rather than as an iteration count
// (thus, to avoid repeating we have to omit the whole extension)
os.write('!'); // GIF Extension Introducer
os.write(0xff); // Application Extension Label
os.write(11); // application ID block size
Put.ascii("NETSCAPE2.0", os); // application ID data
os.write(3); // data sub-block size
os.write(1); // a looping flag? dunno
// we finally write the relevent data
Put.leShort(this.loopCount > 1 ? this.loopCount - 1 : 0, os);
os.write(0); // block terminator
}
//----------------------------------------------------------------------------
private void writeCommentExtension(OutputStream os) throws IOException
{
os.write('!'); // GIF Extension Introducer
os.write(0xfe); // Comment Extension Label
int remainder = this.theComments.length() % 255;
int nsubblocks_full = this.theComments.length() / 255;
int nsubblocks = nsubblocks_full + (remainder > 0 ? 1 : 0);
int ibyte = 0;
for (int isb = 0; isb < nsubblocks; ++isb)
{
int size = isb < nsubblocks_full ? 255 : remainder;
os.write(size);
Put.ascii(this.theComments.substring(ibyte, ibyte + size), os);
ibyte += size;
}
os.write(0); // block terminator
}
//----------------------------------------------------------------------------
private boolean isOk(int frame_index)
{
return frame_index >= 0 && frame_index < this.vFrames.size();
}
}
//==============================================================================
class GifColorTable {
// the palette of ARGB colors, packed as returned by Color.getRGB()
private int[] theColors = new int[256];
// other basic attributes
private int colorDepth;
private int transparentIndex = -1;
// these fields track color-index info across frames
private int ciCount = 0; // count of distinct color indices
/**
*
* @uml.property name="ciLookup"
* @uml.associationEnd multiplicity="(0 1)"
*/
private ReverseColorMap ciLookup; // cumulative rgb-to-ci lookup table
//----------------------------------------------------------------------------
GifColorTable()
{
this.ciLookup = new ReverseColorMap(); // puts us into "auto-detect mode"
}
//----------------------------------------------------------------------------
GifColorTable(Color[] colors)
{
int n2copy = Math.min(this.theColors.length, colors.length);
for (int i = 0; i < n2copy; ++i) {
this.theColors[i] = colors[i].getRGB();
}
}
//----------------------------------------------------------------------------
int getDepth() { return this.colorDepth; }
//----------------------------------------------------------------------------
int getTransparent() { return this.transparentIndex; }
//----------------------------------------------------------------------------
// default: -1 (no transparency)
void setTransparent(int color_index)
{
this.transparentIndex = color_index;
}
// default: -1 (no transparency)
void setTransparent(Color color)
{
int rgbColor = color.getRGB();
if(this.theColors != null){
for (int i = 0; i < this.theColors.length; i++) {
int colorAt = this.theColors[i];
if(colorAt==rgbColor){
setTransparent(i);
}
}
}
}
//----------------------------------------------------------------------------
void processPixels(Gif89Frame gf) throws IOException
{
if (gf instanceof DirectGif89Frame) {
filterPixels((DirectGif89Frame) gf);
}
else {
trackPixelUsage((IndexGif89Frame) gf);
}
}
//----------------------------------------------------------------------------
void closePixelProcessing() // must be called before encode()
{
this.colorDepth = computeColorDepth(this.ciCount);
}
//----------------------------------------------------------------------------
void encode(OutputStream os) throws IOException
{
// size of palette written is the smallest power of 2 that can accomdate
// the number of RGB colors detected (or largest color index, in case of
// index pixels)
int palette_size = 1 << this.colorDepth;
for (int i = 0; i < palette_size; ++i)
{
os.write(this.theColors[i] >> 16 & 0xff);
os.write(this.theColors[i] >> 8 & 0xff);
os.write(this.theColors[i] & 0xff);
}
}
//----------------------------------------------------------------------------
// This method accomplishes three things:
// (1) converts the passed rgb pixels to indexes into our rgb lookup table
// (2) fills the rgb table as new colors are encountered
// (3) looks for transparent pixels so as to set the transparent index
// The information is cumulative across multiple calls.
//
// (Note: some of the logic is borrowed from Jef Poskanzer's code.)
//----------------------------------------------------------------------------
private void filterPixels(DirectGif89Frame dgf) throws IOException
{
if (this.ciLookup == null) {
throw new IOException("RGB frames require palette autodetection");
}
int[] argb_pixels = (int[]) dgf.getPixelSource();
byte[] ci_pixels = dgf.getPixelSink();
int npixels = argb_pixels.length;
for (int i = 0; i < npixels; ++i)
{
int argb = argb_pixels[i];
// handle transparency
if ((argb >>> 24) < 0x80) {
if (this.transparentIndex == -1) {
this.transparentIndex = this.ciCount; // record its index
}
else if (argb != this.theColors[this.transparentIndex]) // different pixel value?
{
// collapse all transparent pixels into one color index
ci_pixels[i] = (byte) this.transparentIndex;
continue; // CONTINUE - index already in table
}
}
// try to look up the index in our "reverse" color table
int color_index = this.ciLookup.getPaletteIndex(argb & 0xffffff);
if (color_index == -1) // if it isn't in there yet
{
if (this.ciCount == 256) {
throw new IOException("can't encode as GIF (> 256 colors)");
}
// store color in our accumulating palette
this.theColors[this.ciCount] = argb;
// store index in reverse color table
this.ciLookup.put(argb & 0xffffff, this.ciCount);
// send color index to our output array
ci_pixels[i] = (byte) this.ciCount;
// increment count of distinct color indices
++this.ciCount;
}
else {
ci_pixels[i] = (byte) color_index; // just send filtered pixel
}
}
}
//----------------------------------------------------------------------------
private void trackPixelUsage(IndexGif89Frame igf) throws IOException
{
byte[] ci_pixels = (byte[]) igf.getPixelSource();
int npixels = ci_pixels.length;
for (int i = 0; i < npixels; ++i) {
if (ci_pixels[i] >= this.ciCount) {
this.ciCount = ci_pixels[i] + 1;
}
}
}
//----------------------------------------------------------------------------
private int computeColorDepth(int colorcount)
{
// color depth = log-base-2 of maximum number of simultaneous colors, i.e.
// bits per color-index pixel
if (colorcount <= 2) {
return 1;
}
if (colorcount <= 4) {
return 2;
}
if (colorcount <= 16) {
return 4;
}
return 8;
}
}
//==============================================================================
// We're doing a very simple linear hashing thing here, which seems sufficient
// for our needs. I make no claims for this approach other than that it seems
// an improvement over doing a brute linear search for each pixel on the one
// hand, and creating a Java object for each pixel (if we were to use a Java
// Hashtable) on the other. Doubtless my little hash could be improved by
// tuning the capacity (at the very least). Suggestions are welcome.
//==============================================================================
class ReverseColorMap {
private static class ColorRecord {
int rgb;
int ipalette;
ColorRecord(int rgb, int ipalette)
{
this.rgb = rgb;
this.ipalette = ipalette;
}
}
// I wouldn't really know what a good hashing capacity is, having missed out
// on data structures and algorithms class :) Alls I know is, we've got a lot
// more space than we have time. So let's try a sparse table with a maximum
// load of about 1/8 capacity.
private static final int HCAPACITY = 2053; // a nice prime number
/**
*
* @uml.property name="hTable"
* @uml.associationEnd multiplicity="(0 -1)" elementType="com.idega.graphics.encoder.gif.ReverseColorMap$ColorRecord"
*/
// our hash table proper
private ColorRecord[] hTable = new ColorRecord[HCAPACITY];
//----------------------------------------------------------------------------
// Assert: rgb is not negative (which is the same as saying, be sure the
// alpha transparency byte - i.e., the high byte - has been masked out).
//----------------------------------------------------------------------------
int getPaletteIndex(int rgb)
{
ColorRecord rec;
for ( int itable = rgb % this.hTable.length;
(rec = this.hTable[itable]) != null && rec.rgb != rgb;
itable = ++itable % this.hTable.length
) {
;
}
if (rec != null) {
return rec.ipalette;
}
return -1;
}
//----------------------------------------------------------------------------
// Assert: (1) same as above; (2) rgb key not already present
//----------------------------------------------------------------------------
void put(int rgb, int ipalette)
{
int itable;
for ( itable = rgb % this.hTable.length;
this.hTable[itable] != null;
itable = ++itable % this.hTable.length
) {
;
}
this.hTable[itable] = new ColorRecord(rgb, ipalette);
}
}