/*
* Copyright (c) 2008 Los Alamos National Security, LLC.
*
* Los Alamos National Laboratory
* Research Library
* Digital Library Research & Prototyping Team
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
package gov.lanl.adore.djatoka.io.writer;
import gov.lanl.adore.djatoka.io.FormatIOException;
import gov.lanl.adore.djatoka.io.IWriter;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Properties;
import ij.ImagePlus;
import ij.process.ImageProcessor;
/**
* GIF File Writer. Uses GifEncoder to write BufferedImage as GIF
* @author Ryan Chute
*
*/
public class GIFWriter implements IWriter {
/**
* Write a BufferedImage instance using implementation to the
* provided OutputStream.
* @param bi a BufferedImage instance to be serialized
* @param os OutputStream to output the image to
* @throws FormatIOException
*/
public void write(BufferedImage bi, OutputStream os) throws FormatIOException {
if (bi != null) {
BufferedOutputStream bos = null;
bos = new BufferedOutputStream(os);
ImagePlus ip = new ImagePlus();
ip.setImage(bi);
GifEncoder ge = new GifEncoder();
ge.start(bos);
ge.addFrame(ip);
ge.finish();
}
}
/**
* NOT SUPPORTED.
*/
public void setWriterProperties(Properties props) {}
/**
* This plugin encodes a GIF file consisting of one or more frames.
*
* <pre>
* Extensively Modified for ImagePlus
* Extended to handle 8 bit Images with more complex Color lookup tables with transparency index
*
* Ryan Raz March 2002
* raz@rraz.ca
* Version 1.01
** Extensively Modified for ImagePlus
* Extended to handle 8 bit Images with more complex Color lookup tables with transparency index
*
* Ryan Raz March 2002
* ryan@rraz.ca
* Version 1.01 Please report any bugs
*
* Credits for the base conversion codes
* No copyright asserted on the source code of this class. May be used
* for any purpose, however, refer to the Unisys LZW patent for restrictions
* on use of the associated LZWEncoder class. Please forward any corrections
* to kweiner@fmsware.com.
* </pre>
*
* @author Kevin Weiner, FM Software
* @version 1.0 December 2000
*
*/
class GifEncoder {
int width; // image size
int height;
boolean transparent; // transparent color if given
int transIndex; // transparent index in color table
int repeat = 0; // repeat forever
protected int delay = 50; // frame delay (hundredths)
boolean started = false; // ready to output frames
OutputStream out;
ImagePlus image; // current frame
byte[] pixels; // BGR byte array from frame
byte[] indexedPixels; // converted frame indexed to palette
int colorDepth; // number of bit planes
byte[] colorTab; // RGB palette
int lctSize = 7; // local color table size (bits-1)
int dispose = 0; // disposal code (-1 = use default)
boolean closeStream = false; // close stream when finished
boolean firstFrame = true;
boolean sizeSet = false; // if false, get size from first frame
int sample = 2; // default sample interval for quantizer distance should be small for small icons
byte[] gct = null; //Global color table
boolean GCTextracted = false; // Set if global color table extracted from rgb image
boolean GCTloadedExternal = false; // Set if global color table loaded directly from external image
int GCTred = 0; //Transparent Color
int GCTgrn = 0; // green
int GCTbl = 0; // blue
int GCTcindex = 0; //index into color table
boolean GCTsetTransparent = false; //If true then Color table transparency index is set
boolean GCToverideIndex = false; //If true Transparent index is set to index with closest colors
boolean GCToverideColor = false; //if true Color at Transparent index is set to GCTred, GCTgrn GCTbl
/**
* Adds next GIF frame. The frame is not written immediately, but is
* actually deferred until the next frame is received so that timing
* data can be inserted. Invoking <code>finish()</code> flushes all
* frames. If <code>setSize</code> was not invoked, the size of the
* first image is used for all subsequent frames.
*
* @param im containing frame to write.
* @return true if successful.
*/
public boolean addFrame(ImagePlus image) {
if ((image == null) || !started) return false;
boolean ok = true;
try {
if (firstFrame) {
if (!sizeSet) {
// use first frame's size
setSize(image.getWidth(), image.getHeight());
}
writeLSD();
if (repeat>=0) writeNetscapeExt(); // use NS app extension to indicate reps
firstFrame = false;
}
int bitDepth = image.getBitDepth();
// If indexed byte image then format does not need changing
int k;
Process8bitCLT(image);
writeGraphicCtrlExt(); // write graphic control extension
writeImageDesc(); // image descriptor
writePalette(); // local color table
writePixels(); // encode and write pixel data
} catch (IOException e) { ok = false; }
return ok;
}
/*
* Get Options because options box has been checked
Some of the code being set
setTransparent(Color.black);
Dispose = 0;
setDelay(500); // time per frame in milliseconds
gctused = false; // Set to true to use Global color table
GCTextracted = false; // Set if global color table extracted from rgb image
GCTloadedExternal = false; // Set if global color table loaded directly from external image
GCTextracted = false; // Set if global color table extracted from rgb image
GCTred = 0; //Transparent Color
GCTgrn = 0; // green
GCTbl = 0; // blue
GCTcindex = 0; //index into color table
autotransparent = false; // Set True if transparency index coming from image 8 bit only
GCTsetTransparent = true; //If true then Color table transparency index is set
GCToverideIndex = false; //If true Transparent index is set to index with closest colors
GCToverideColor = false; //if true Color at Transparent index is set to GCTred, GCTgrn GCTbl
*/
/********************************************************
* Gets Color lookup Table from 8 bit ImagePlus
*/
void Process8bitCLT(ImagePlus image){
colorDepth = 8;
//setTransparent(false);
ImageProcessor ip = image.getProcessor();
ip = ip.convertToByte(true);
ColorModel cm = ip.getColorModel();
indexedPixels = (byte[])(ip.getPixels());
IndexColorModel m = (IndexColorModel)cm;
int mapSize = m.getMapSize();
if (transIndex>=mapSize) {
setTransparent(false);
transIndex = 0;
}
int k;
colorTab = new byte[mapSize*3];
for (int i = 0; i < mapSize; i++) {
k=i*3;
colorTab[k] = (byte)m.getRed(i);
colorTab[k+1] = (byte)m.getGreen(i);
colorTab[k+2] = (byte)m.getBlue(i);
}
m.finalize();
}
/**
* Flushes any pending data and closes output file.
* If writing to an OutputStream, the stream is not
* closed.
*/
public boolean finish() {
if (!started) return false;
boolean ok = true;
started = false;
try {
out.write(0x3b); // gif trailer
out.flush();
if (closeStream)
out.close();
} catch (IOException e) { ok = false; }
// reset for subsequent use
GCTextracted = false; // Set if global color table extracted from rgb image
GCTloadedExternal = false; // Set if global color table loaded directly from external image
transIndex = 0;
transparent = false;
gct = null; //Global color table
out = null;
image = null;
pixels = null;
indexedPixels = null;
colorTab = null;
closeStream = false;
firstFrame = true;
return ok;
}
/**
* Sets the delay time between each frame, or changes it
* for subsequent frames (applies to last frame added).
*
* @param ms int delay time in milliseconds
*/
public void setDelay(int ms) {
delay = Math.round(ms / 10.0f);
}
/**
* Sets the GIF frame disposal code for the last added frame
* and any subsequent frames. Default is 0 if no transparent
* color has been set, otherwise 2.
* @param code int disposal code.
*/
public void setDispose(int code) {
if (code >= 0)
dispose = code;
}
/**
* Sets frame rate in frames per second. Equivalent to
* <code>setDelay(1000/fps)</code>.
*
* @param fps float frame rate (frames per second)
*/
public void setFrameRate(float fps) {
if (fps != 0f) {
delay = Math.round(100f/fps);
}
}
/**
* Sets quality of color quantization (conversion of images
* to the maximum 256 colors allowed by the GIF specification).
* Lower values (minimum = 1) produce better colors, but slow
* processing significantly. 10 is the default, and produces
* good color mapping at reasonable speeds. Values greater
* than 20 do not yield significant improvements in speed.
*
* @param quality int greater than 0.
* @return
*/
public void setQuality(int quality) {
if (quality < 1) quality = 1;
sample = quality;
}
/**
* Sets the number of times the set of GIF frames
* should be played. Default is 1; 0 means play
* indefinitely. Must be invoked before the first
* image is added.
*
* @param iter int number of iterations.
* @return
*/
public void setRepeat(int iter) {
if (iter >= 0)
repeat = iter;
}
/**
* Sets the GIF frame size. The default size is the
* size of the first frame added if this method is
* not invoked.
*
* @param w int frame width.
* @param h int frame width.
*/
public void setSize(int w, int h) {
if (started && !firstFrame) return;
width = w;
height = h;
if (width < 1) width = 320;
if (height < 1) height = 240;
sizeSet = true;
}
/**
* Sets the transparent color for the last added frame
* and any subsequent frames.
* Since all colors are subject to modification
* in the quantization process, the color in the final
* palette for each frame closest to the given color
* becomes the transparent color for that frame.
* May be set to null to indicate no transparent color.
*
* @param c Color to be treated as transparent on display.
*/
public void setTransparent(boolean c) {
transparent = c;
}
/**
* Initiates GIF file creation on the given stream. The stream
* is not closed automatically.
*
* @param os OutputStream on which GIF images are written.
* @return false if initial write failed.
*/
public boolean start(OutputStream os) {
if (os == null) return false;
boolean ok = true;
closeStream = false;
out = os;
try {
writeString("GIF89a"); // header
} catch (IOException e) { ok = false; }
return started = ok;
}
/**
* Initiates writing of a GIF file with the specified name.
*
* @param file String containing output file name.
* @return false if open or initial write failed.
*/
public boolean start(String file) {
boolean ok = true;
try {
out = new BufferedOutputStream(new FileOutputStream(file));
ok = start(out);
closeStream = true;
} catch (IOException e) { ok = false; }
return started = ok;
}
/**
Sets Net sample size depending on image size
**/
public void OverRideQuality(int npixs){
if(npixs>100000) sample = 10;
else sample = npixs/10000;
if(sample < 1) sample = 1;
}
/**
* Writes Graphic Control Extension
*/
protected void writeGraphicCtrlExt() throws IOException {
out.write(0x21); // extension introducer
out.write(0xf9); // GCE label
out.write(4); // data block size
int transp, disp;
if (!transparent) {
transp = 0;
disp = 0; // dispose = no action
} else {
transp = 1;
disp = 2; // force clear if using transparent color
}
if (dispose >= 0)
disp = dispose & 7; // user override
disp <<= 2;
// packed fields
out.write( 0 | // 1:3 reserved
disp | // 4:6 disposal
0 | // 7 user input - 0 = none
transp); // 8 transparency flag
writeShort(delay); // delay x 1/100 sec
out.write(transIndex); // transparent color index
out.write(0); // block terminator
}
/**
* Writes Image Descriptor
*/
protected void writeImageDesc() throws IOException {
out.write(0x2c); // image separator
writeShort(0); // image position x,y = 0,0
writeShort(0);
writeShort(width); // image size
writeShort(height);
// packed fields
out.write(0x80 | // 1 local color table 1=yes
0 | // 2 interlace - 0=no
0 | // 3 sorted - 0=no
0 | // 4-5 reserved
lctSize); // size of local color table
}
/**
* Writes Logical Screen Descriptor with global color table
*/
protected void writeLSDgct() throws IOException {
// logical screen size
writeShort(width);
writeShort(height);
// packed fields
out.write((0x80 | // 1 : global color table flag = 0 (nn
0x70 | // 2-4 : color resolution = 7
0x00 | // 5 : gct sort flag = 0
lctSize)); // 6-8 : gct size = 0
out.write(0); // background color index
out.write(0); // pixel aspect ratio - assume 1:1
}
/**
* Writes Logical Screen Descriptor without global color table
*/
protected void writeLSD() throws IOException {
// logical screen size
writeShort(width);
writeShort(height);
// packed fields
out.write((0x00 | // 1 : global color table flag = 0 (none)
0x70 | // 2-4 : color resolution = 7
0x00 | // 5 : gct sort flag = 0
0x00)); // 6-8 : gct size = 0
out.write(0); // background color index
out.write(0); // pixel aspect ratio - assume 1:1
}
/**
* Writes Netscape application extension to define
* repeat count.
*/
protected void writeNetscapeExt() throws IOException {
out.write(0x21); // extension introducer
out.write(0xff); // app extension label
out.write(11); // block size
writeString("NETSCAPE"+"2.0"); // app id + auth code
out.write(3); // sub-block size
out.write(1); // loop sub-block id
writeShort(repeat); // loop count (extra iterations, 0=repeat forever)
out.write(0); // block terminator
}
/**
* Writes color table
*/
protected void writePalette() throws IOException {
out.write(colorTab, 0, colorTab.length);
int n = (3 * 256) - colorTab.length;
for (int i = 0; i < n; i++)
out.write(0);
}
/**
* Encodes and writes pixel data
*/
protected void writePixels() throws IOException {
LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, colorDepth);
encoder.encode(out);
}
/**
* Write 16-bit value to output stream, LSB first
*/
protected void writeShort(int value) throws IOException {
out.write(value & 0xff);
out.write((value >> 8) & 0xff);
}
/**
* Writes string to output stream
*/
protected void writeString(String s) throws IOException {
for (int i = 0; i < s.length(); i++)
out.write((byte) s.charAt(i));
}
}
// ==============================================================================
// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott.
// K Weiner 12/00
class LZWEncoder {
private static final int EOF = -1;
private int imgW, imgH;
private byte[] pixAry;
private int initCodeSize;
private int remaining;
private int curPixel;
// GIFCOMPR.C - GIF Image compression routines
//
// Lempel-Ziv compression based on 'compress'. GIF modifications by
// David Rowley (mgardi@watdcsu.waterloo.edu)
// General DEFINEs
static final int BITS = 12;
static final int HSIZE = 5003; // 80% occupancy
// GIF Image compression - modified 'compress'
//
// Based on: compress.c - File compression ala IEEE Computer, June 1984.
//
// By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
// Jim McKie (decvax!mcvax!jim)
// Steve Davies (decvax!vax135!petsd!peora!srd)
// Ken Turkowski (decvax!decwrl!turtlevax!ken)
// James A. Woods (decvax!ihnp4!ames!jaw)
// Joe Orost (decvax!vax135!petsd!joe)
int n_bits; // number of bits/code
int maxbits = BITS; // user settable max # bits/code
int maxcode; // maximum code, given n_bits
int maxmaxcode = 1 << BITS; // should NEVER generate this code
int[] htab = new int[HSIZE];
int[] codetab = new int[HSIZE];
int hsize = HSIZE; // for dynamic table sizing
int free_ent = 0; // first unused entry
// block compression parameters -- after all codes are used up,
// and compression rate changes, start over.
boolean clear_flg = false;
// Algorithm: use open addressing double hashing (no chaining) on the
// prefix code / next character combination. We do a variant of Knuth's
// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
// secondary probe. Here, the modular division first probe is gives way
// to a faster exclusive-or manipulation. Also do block compression with
// an adaptive reset, whereby the code table is cleared when the compression
// ratio decreases, but after the table fills. The variable-length output
// codes are re-sized at this point, and a special CLEAR code is generated
// for the decompressor. Late addition: construct the table according to
// file size for noticeable speed improvement on small files. Please direct
// questions about this implementation to ames!jaw.
int g_init_bits;
int ClearCode;
int EOFCode;
// output
//
// Output the given code.
// Inputs:
// code: A n_bits-bit integer. If == -1, then EOF. This assumes
// that n_bits =< wordsize - 1.
// Outputs:
// Outputs code to the file.
// Assumptions:
// Chars are 8 bits long.
// Algorithm:
// Maintain a BITS character long buffer (so that 8 codes will
// fit in it exactly). Use the VAX insv instruction to insert each
// code in turn. When the buffer fills up empty it and start over.
int cur_accum = 0;
int cur_bits = 0;
int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F,
0x001F, 0x003F, 0x007F, 0x00FF,
0x01FF, 0x03FF, 0x07FF, 0x0FFF,
0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF };
// Number of characters so far in this 'packet'
int a_count;
// Define the storage for the packet accumulator
byte[] accum = new byte[256];
//----------------------------------------------------------------------------
LZWEncoder(int width, int height, byte[] pixels, int color_depth) {
imgW = width;
imgH = height;
pixAry = pixels;
initCodeSize = Math.max(2, color_depth);
}
// Add a character to the end of the current packet, and if it is 254
// characters, flush the packet to disk.
void char_out( byte c, OutputStream outs ) throws IOException
{
accum[a_count++] = c;
if ( a_count >= 254 )
flush_char( outs );
}
// Clear out the hash table
// table clear for block compress
void cl_block( OutputStream outs ) throws IOException
{
cl_hash( hsize );
free_ent = ClearCode + 2;
clear_flg = true;
output( ClearCode, outs );
}
// reset code table
void cl_hash( int hsize )
{
for ( int i = 0; i < hsize; ++i )
htab[i] = -1;
}
void compress( int init_bits, OutputStream outs ) throws IOException
{
int fcode;
int i /* = 0 */;
int c;
int ent;
int disp;
int hsize_reg;
int hshift;
// Set up the globals: g_init_bits - initial number of bits
g_init_bits = init_bits;
// Set up the necessary values
clear_flg = false;
n_bits = g_init_bits;
maxcode = MAXCODE( n_bits );
ClearCode = 1 << ( init_bits - 1 );
EOFCode = ClearCode + 1;
free_ent = ClearCode + 2;
a_count = 0; // clear packet
ent = nextPixel();
hshift = 0;
for ( fcode = hsize; fcode < 65536; fcode *= 2 )
++hshift;
hshift = 8 - hshift; // set hash code range bound
hsize_reg = hsize;
cl_hash( hsize_reg ); // clear hash table
output( ClearCode, outs );
outer_loop:
while ( (c = nextPixel()) != EOF )
{
fcode = ( c << maxbits ) + ent;
i = ( c << hshift ) ^ ent; // xor hashing
if ( htab[i] == fcode )
{
ent = codetab[i];
continue;
}
else if ( htab[i] >= 0 ) // non-empty slot
{
disp = hsize_reg - i; // secondary hash (after G. Knott)
if ( i == 0 )
disp = 1;
do
{
if ( (i -= disp) < 0 )
i += hsize_reg;
if ( htab[i] == fcode )
{
ent = codetab[i];
continue outer_loop;
}
}
while ( htab[i] >= 0 );
}
output( ent, outs );
ent = c;
if ( free_ent < maxmaxcode )
{
codetab[i] = free_ent++; // code -> hashtable
htab[i] = fcode;
}
else
cl_block( outs );
}
// Put out the final code.
output( ent, outs );
output( EOFCode, outs );
}
//----------------------------------------------------------------------------
void encode(OutputStream os) throws IOException
{
os.write(initCodeSize); // write "initial code size" byte
remaining = imgW * imgH; // reset navigation variables
curPixel = 0;
compress(initCodeSize + 1, os); // compress and write the pixel data
os.write(0); // write block terminator
}
// Flush the packet to disk, and reset the accumulator
void flush_char( OutputStream outs ) throws IOException
{
if ( a_count > 0 )
{
outs.write( a_count );
outs.write( accum, 0, a_count );
a_count = 0;
}
}
final int MAXCODE( int n_bits )
{
return ( 1 << n_bits ) - 1;
}
//----------------------------------------------------------------------------
// Return the next pixel from the image
//----------------------------------------------------------------------------
private int nextPixel()
{
if (remaining == 0)
return EOF;
--remaining;
byte pix = pixAry[curPixel++];
return pix & 0xff;
}
void output( int code, OutputStream outs ) throws IOException
{
cur_accum &= masks[cur_bits];
if ( cur_bits > 0 )
cur_accum |= ( code << cur_bits );
else
cur_accum = code;
cur_bits += n_bits;
while ( cur_bits >= 8 )
{
char_out( (byte) ( cur_accum & 0xff ), outs );
cur_accum >>= 8;
cur_bits -= 8;
}
// If the next entry is going to be too big for the code size,
// then increase it, if possible.
if ( free_ent > maxcode || clear_flg )
{
if ( clear_flg )
{
maxcode = MAXCODE(n_bits = g_init_bits);
clear_flg = false;
}
else
{
++n_bits;
if ( n_bits == maxbits )
maxcode = maxmaxcode;
else
maxcode = MAXCODE(n_bits);
}
}
if ( code == EOFCode )
{
// At EOF, write the rest of the buffer.
while ( cur_bits > 0 )
{
char_out( (byte) ( cur_accum & 0xff ), outs );
cur_accum >>= 8;
cur_bits -= 8;
}
flush_char( outs );
}
}
}
}