com.day.imageio.plugins.GifImageWriter Maven / Gradle / Ivy
/*************************************************************************
*
* ADOBE CONFIDENTIAL
* __________________
*
* Copyright 2012 Adobe Systems Incorporated
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated and its
* suppliers and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package com.day.imageio.plugins;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.nio.ByteOrder;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
/**
* The GifImageWriter
class implements the ImageIO version
* independent part of the GIF image writer for the ImageIO API. Extensions need
* to implement methods depending on API available in either J2SE 1.3 or J2SE
* 1.4.
*
* As usual we take the MetaData for additional configuration. As a special case
* we treat :
*
* - backgroundColorIndex - The RGB color value of the background color
*
- transparentColorIndex - The RGB color value of the predefined
* transparent color
*
* Both values will be translated to index values during color compression.
*
* @version $Revision: 26109 $, $Date: 2007-04-17 16:41:22 +0200 (Di, 17 Apr 2007) $
* @author fmeschbe
* @since coati
* @audience core
*/
/* package */class GifImageWriter extends ImageWriter {
/** The stream to which the GIF image is written */
protected ImageOutputStream stream;
/** true
as soon as the stream metadata has been written */
private boolean streamInitialized;
/**
* Prepares the GIF image writer.
*
* @param originatingProvider the ImageWriterSpi
that is
* constructing this object, or null
.
*/
protected GifImageWriter(ImageWriterSpi originatingProvider) {
super(originatingProvider);
// no stream yet and not initialized
stream = null;
streamInitialized = false;
}
/**
* Sets the output stream to use for writing the GIF image. The output
* object must be an instance of ImageOutputStream
class or
* an IllegalArgumentException
is thrown.
*
* @param output The output stream to set for writing
* @throws IllegalArgumentException if the output object is not an
* ImageOutputStream
.
*/
public void setOutput(Object output) {
super.setOutput(output);
// assign the output
try {
this.stream = (ImageOutputStream) output;
} catch (ClassCastException cce) {
throw new IllegalArgumentException("output not ImageOutputStream");
}
// reset stream initialization
this.streamInitialized = false;
}
/**
* Returns an IIOMetadata
object containing default values
* for encoding a stream of images. The contents of the object may be
* manipulated using either the XML tree structure returned by the
* IIOMetadata.getAsTree
method, an
* IIOMetadataController
object, or via plug-in specific
* interfaces, and the resulting data supplied to one of the
* write
methods that take a stream metadata parameter.
*
* An optional ImageWriteParam
may be supplied for cases
* where it may affect the structure of the stream metadata.
*
* If the supplied ImageWriteParam
contains optional setting
* values not supported by this writer, they will be ignored.
*
* @param param an ImageWriteParam
that will be used to
* encode the image, or null
.
* @return an IIOMetadata
object.
*/
public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
return new GIFStreamMetadata();
}
/**
* Returns an instance of image metada usable for this image writer. This
* implementation currently does not support transcoding of foreign image
* metadata to GIF image metadata and therefor only returns the inData
* object if it is a GIF image metadata, else null
is
* returned.
*
* @param inData an IIOMetadata
object representing stream
* metadata, used to initialize the state of the returned object.
* @param param an ImageWriteParam
that will be used to
* encode the image, or null
.
* @return an IIOMetadata
object, or null
if
* the plug-in does not provide metadata encoding capabilities.
* @exception IllegalArgumentException if inData
is
* null
.
*/
public IIOMetadata convertStreamMetadata(IIOMetadata inData,
ImageWriteParam param) {
if (inData == null) {
throw new IllegalArgumentException("inData must not be null");
}
// We only understand out own meta data for the moment
if (inData instanceof GIFStreamMetadata) {
return inData;
} else {
return null;
}
}
/**
* Returns an IIOMetadata
object containing default values
* for encoding an image of the given type. The contents of the object may
* be manipulated using either the XML tree structure returned by the
* IIOMetadata.getAsTree
method, an
* IIOMetadataController
object, or via plug-in specific
* interfaces, and the resulting data supplied to one of the
* write
methods that take a stream metadata parameter.
*
* An optional ImageWriteParam
may be supplied for cases
* where it may affect the structure of the image metadata.
*
* If the supplied ImageWriteParam
contains optional setting
* values not supported by this writer, they will be ignored.
*
* @param imageType an ImageTypeSpecifier
indicating the
* format of the image to be written later.
* @param param an ImageWriteParam
that will be used to
* encode the image, or null
.
* @return a GIF specific IIOMetadata
object.
*/
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
ImageWriteParam param) {
return new GIFImageMetadata();
}
/**
* Returns an instance of image metada usable for this image writer. This
* implementation currently does not support transcoding of foreign image
* metadata to GIF image metadata and therefor only returns the inData
* object if it is a GIF image metadata, else null
is
* returned.
*
* @param inData an IIOMetadata
object representing image
* metadata, used to initialize the state of the returned object.
* @param imageType an ImageTypeSpecifier
indicating the
* layout and color information of the image with which the
* metadata will be associated.
* @param param an ImageWriteParam
that will be used to
* encode the image, or null
.
* @return an IIOMetadata
object, or null
if
* the plug-in does not provide metadata encoding capabilities.
* @exception IllegalArgumentException if inData
is
* null
.
*/
public IIOMetadata convertImageMetadata(IIOMetadata inData,
ImageTypeSpecifier imageType, ImageWriteParam param) {
if (inData == null) {
throw new IllegalArgumentException("inData must not be null");
}
// We only understand our own meta data for the moment
if (inData instanceof GIFImageMetadata) {
return inData;
} else {
return null;
}
}
/**
* Appends a complete image stream containing a single image and associated
* stream and image metadata and thumbnails to the output. Any necessary
* header information is included. If the output is an
* ImageOutputStream
, its existing contents prior to the
* current seek position are not affected, and need not be readable or
* writable.
*
* The output must have been set beforehand using the setOutput
* method.
*
* Stream metadata may optionally be supplied; if it is null
,
* default stream metadata will be used.
*
* If canWriteRasters
returns true
, the
* IIOImage
may contain a Raster
source.
* Otherwise, it must contain a RenderedImage
source.
*
* The supplied thumbnails will be resized if needed, and any thumbnails in
* excess of the supported number will be ignored. If the format requires
* additional thumbnails that are not provided, the writer should generate
* them internally.
*
* An ImageWriteParam
may optionally be supplied to control
* the writing process. If param
is null
, a
* default write param will be used.
*
* If the supplied ImageWriteParam
contains optional setting
* values not supported by this writer, they will be ignored.
*
* @param streamMetadata an IIOMetadata
object representing
* stream metadata, or null
to use default values.
* @param image an IIOImage
object containing an image,
* thumbnails, and metadata to be written.
* @param param an ImageWriteParam
, or null
* to use a default ImageWriteParam
.
* @exception IllegalStateException if the output has not been set.
* @exception UnsupportedOperationException if image
contains
* a Raster
and canWriteRasters
* returns false
.
* @exception UnsupportedOperationException if the
* RenderedImage
contained in
* image
either does not have an
* IndexColorModel
or if the number of colors
* is higher than 256.
* @exception IllegalArgumentException if image
is
* null
.
* @exception IIOException if an error occurs during writing.
*/
public void write(IIOMetadata streamMetadata, IIOImage image,
ImageWriteParam param) throws IIOException {
// Check whether we have some destination to write to
if (stream == null) {
throw new IllegalStateException("output not yet set");
}
// check image for null
if (image == null) {
throw new IllegalArgumentException("image must not be null");
}
// First get the image and check for existence
RenderedImage rim = image.getRenderedImage();
if (rim == null) {
throw new UnsupportedOperationException("Image not a RenderedImage");
}
// Get the metadata
IIOMetadata imd = image.getMetadata();
GIFImageMetadata metadata = null;
if (imd != null) {
metadata = (GIFImageMetadata) convertImageMetadata(imd, null, null);
}
// Insurance policy, but results may not be valid !
if (metadata == null) metadata = new GIFImageMetadata();
GIFStreamMetadata gifMetaData = (streamMetadata == null)
? new GIFStreamMetadata()
: (GIFStreamMetadata) streamMetadata;
// Check whether it has correct color model and color numbers
ColorModel cm = rim.getColorModel();
IndexColorModel icm;
if (cm instanceof IndexColorModel) {
icm = (IndexColorModel) cm;
if (icm.getMapSize() > 256) {
throw new UnsupportedOperationException(
"Number of colors > 256");
}
} else {
throw new UnsupportedOperationException(
"Image must have IndexColorModel");
}
// Figure out how many bits to use.
int colTabLen = icm.getMapSize();
int depth;
if (colTabLen <= 2) {
depth = 1;
} else if (colTabLen <= 4) {
depth = 2;
} else if (colTabLen <= 8) {
depth = 3;
} else if (colTabLen <= 16) {
depth = 4;
} else if (colTabLen <= 32) {
depth = 5;
} else if (colTabLen <= 64) {
depth = 6;
} else if (colTabLen <= 128) {
depth = 7;
} else {
depth = 8;
}
// Turn colors into colormap entries.
int mapSize = 1 << depth;
int[] rgba = new int[colTabLen];
byte[] gifColTab = new byte[mapSize * 3];
icm.getRGBs(rgba);
for (int i = 0, j = 0; i < colTabLen; i++) {
int col = rgba[i];
gifColTab[j++] = (byte) (col >> 16);
gifColTab[j++] = (byte) (col >> 8);
gifColTab[j++] = (byte) (col);
}
// Set the local color table - we don't want a global, don't we ?
boolean useGlobalColorTable = !streamInitialized;
// If the background color didn't force to use the local tab
if (useGlobalColorTable) {
gifMetaData.globalColorTable = gifColTab;
metadata.localColorTable = null;
} else {
// This might not be the first picture, so we have a local table
metadata.localColorTable = gifColTab;
}
try {
// Maybe we should start the GIF stream
if (!streamInitialized) {
startGifStream(gifMetaData);
streamInitialized = true;
}
// This is it, we write it
writeGifImageMetaData(metadata);
// This is hardest, encode the stuff
writeCompressedImage(rim.getData(), depth);
// flush the output
stream.flush();
} catch (IOException ioe) {
throw new IIOException(ioe.getMessage(), ioe);
}
}
// ---------- API to be overwritten by ImageIO version aware extensions
// -----
/**
* Sets the byte order on the stream to little endian.
*/
protected void setByteOrder() {
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
}
// ---------- internal
// ------------------------------------------------------
/**
* Start the GIF image stream writing out the global stream meta data
*
* @param streamMetadata The GIF global meta data
*/
private void startGifStream(GIFStreamMetadata streamMetadata)
throws IOException {
// preparation of packed fields
byte packed;
// This is obviously needed. But how do I know whether true/false ?
setByteOrder();
// HEADER - we fix at GIF 89a, sorry
stream.write("GIF89a".getBytes());
// Logical Screen Description
// Write out the screen width and height
stream.writeShort(streamMetadata.logicalScreenWidth);
stream.writeShort(streamMetadata.logicalScreenHeight);
// global colors
packed = (byte) (((streamMetadata.globalColorTable != null) ? 1 : 0) << 7);
packed |= 0x70; // max palette size, ok
// packed |= 0; // sort flag cleared
if (streamMetadata.globalColorTable != null) {
int nc = streamMetadata.globalColorTable.length / 3;
int i;
for (i = -2; nc > 0; i++, nc >>= 1);
packed |= i;
}
stream.write(packed);
// Write out the Background colour
stream.write(streamMetadata.backgroundColorIndex);
// Pixel Aspect Ration (none)
stream.write(0);
// Global color table
if (streamMetadata.globalColorTable != null) {
stream.write(streamMetadata.globalColorTable);
}
if (streamMetadata.extensions != null) {
for (int i = 0; i < streamMetadata.extensions.length; i++) {
GIFStreamMetadata.ApplicationExtension ext = streamMetadata.extensions[i];
stream.write(0x21); // GIF Extension Code
stream.write(0xff); // Application Extension Label
// write the application block
byte[] block11 = "\u000b ".getBytes();
int idLen = Math.min(ext.identifier.length, 8);
System.arraycopy(ext.identifier, 0, block11, 1, idLen);
idLen = Math.min(ext.authCode.length, 3);
System.arraycopy(ext.authCode, 0, block11, 9, idLen);
stream.write(block11);
// write sub blocks if any
for (int j = 0; j < ext.subBlocks.length; j++) {
stream.write(ext.subBlocks[j].length);
stream.write(ext.subBlocks[j]);
}
stream.write(0x00); // data block terminator
}
}
}
/**
* Restores the ImageWriter
to its initial state.
*
* Besides doing a default reset by calling the base class
* reset()
, the flag that the global header has been sent is
* also cleared.
*/
public void reset() {
// force writing the global header at next write start
this.streamInitialized = false;
// base class reset
super.reset();
}
/**
* Allows any resources held by this object to be released. The result of
* calling any other method (other than finalize
) subsequent
* to a call to this method is undefined.
*
* The GifImageWriter
writes the ending tag and resets the
* flag, that the global has been sent.
*/
public void dispose() {
// terminate the image writing and force global header writing
if (stream != null) {
try {
stream.write(0x3b);
} catch (IOException ioe) {
// we should do anything but fail silently ...
}
streamInitialized = false;
}
// base class reset
super.dispose();
}
/**
* Write out the GIF image with all the headers and stuff
*
* @param metadata The image specific GIF data
*/
private void writeGifImageMetaData(GIFImageMetadata metadata)
throws IOException {
byte packed;
// create graphics control extension
// setting the delay and removal instruction
stream.write(0x21); // Extension introducer
stream.write(0xf9); // graph ctrl label
stream.write(4); // size of block
// prepare the packed control information :
packed = 0;
packed |= ((metadata.disposalMethod & 0x7) << 2); // disposal
packed |= metadata.transparentColorFlag ? 1 : 0; // transparent flag
stream.write(packed); // packed info
stream.writeShort(metadata.delayTime); // display delay
stream.write(metadata.transparentColorIndex); // Transparent Color
// index
stream.write(0); // End Block
// Now comes the image....
stream.write(0x2c);
stream.writeShort(metadata.imageLeftPosition);
stream.writeShort(metadata.imageTopPosition);
stream.writeShort(metadata.imageWidth);
stream.writeShort(metadata.imageHeight);
// prepare image packed information and write
// opt. local col table, no interlace, no sort
packed = (byte) (((metadata.localColorTable != null) ? 1 : 0) << 7);
if (metadata.localColorTable != null) {
int nc = metadata.localColorTable.length / 3;
int i;
for (i = -2; nc > 0; i++, nc >>= 1);
packed |= i;
}
stream.write(packed);
// Local color table
if (metadata.localColorTable != null) {
stream.write(metadata.localColorTable);
}
}
// ---------- The real hard work : the GIF encoder
// --------------------------
/**
* This is the dirty part of the job. We have to implement the LZW
* compression. This algorithm is based on Hans Dinsen-Hansen's gifencode.c
* which in turn is based on Michael A. Mayer's gifcode.c This is from
* gifencode.c : Copyright (c) 1997,1998 by Hans Dinsen-Hansen The
* algorithms are inspired by those of gifcode.c Copyright (c) 1995,1996
* Michael A. Mayer All rights reserved. This software may be freely copied,
* modified and redistributed without fee provided that above copyright
* notices are preserved intact on all copies and modified copies. There is
* no warranty or other guarantee of fitness of this software. It is
* provided solely "as is". The author(s) disclaim(s) all responsibility and
* liability with respect to this software's usage or its effect upon
* hardware or computer systems. The Graphics Interchange format (c) is the
* Copyright property of Compuserve Incorporated. Gif(sm) is a Service Mark
* property of Compuserve Incorporated. Implements GIF encoding by means of
* a tree search. -------------------------------------------------- - The
* string table may be thought of being stored in a "b-tree of steroids," or
* more specifically, a {256,128,...,4}-tree, depending on the size of the
* color map. - Each (non-NULL) node contains the string table index (or
* code) and {256,128,...,4} pointers to other nodes. - For example, the
* index associated with the string 0-3-173-25 would be stored in:
* first->node[0]->node[3]->node[173]->node[25]->code - Speed and
* effectivity considerations, however, have made this implementation
* somewhat obscure, because it is costly to initialize a node-array where
* most elements will never be used. - Initially, a new node will be marked
* as terminating, TERMIN. If this node is used at a later stage, its mark
* will be changed. - Only nodes with several used nodes will be associated
* with a node-array. Such nodes are marked LOOKUP. - The remaining nodes
* are marked SEARCH. They are linked together in a search-list, where a
* field, NODE->alt, points at an alternative following color. - It is
* hardly feasible exactly to predict which nodes will have most used node
* pointers. The theory here is that the very first node as well as the
* first couple of nodes which need at least one alternative color, will be
* among the ones with many nodes ("... whatever that means", as my tutor in
* Num. Analysis and programming used to say). - The number of possible
* LOOKUP nodes depends on the size of the color map. Large color maps will
* have many SEARCH nodes; small color maps will probably have many LOOKUP
* nodes.
*/
// ---------- GIFTree internal helper class
// ---------------------------------
private static final class GifTree {
static final byte TERMIN = (byte) 'T';
static final byte LOOKUP = (byte) 'L';
static final byte SEARCH = (byte) 'S';
byte type; /* terminating, lookup, or search */
int code; /* the code to be output */
int idx; /* the color map index */
GifTree[] node;
GifTree nxt;
GifTree alt;
GifTree(byte type) {
this.type = type;
}
GifTree(byte type, int code, int idx) {
this.type = type;
this.code = code;
this.idx = idx;
}
GifTree() {
}
}
private static final int BLOCKLEN = 255;
private static final int BUFLEN = 1000;
int chainlen = 0;
int maxchainlen = 0;
int nodecount = 0;
int lookuptypes = 0;
int nbits;
long obits;
byte[] buffer;
private short need = 8;
GifTree root = new GifTree(GifTree.LOOKUP);
private void writeCompressedImage(Raster data, int depth)
throws IOException {
int w = data.getWidth();
int h = data.getHeight();
// IndexColorModel have one band only
int[] chunk = new int[w];
GifTree first = root;
GifTree newNode;
buffer = new byte[BUFLEN];
int pos = 0;
int cc = (depth == 1) ? 0x4 : 1 << depth; // clear code
int cLength = (depth == 1) ? 3 : depth + 1; // cod length (?)
int eoi = cc + 1; // end code
int next = cc + 2; // next available code
// Insert the minimum code size in the stream
stream.write(cLength - 1);
// Assert clean code tree
clearTree(cc, first);
// Start with clear code
pos = addCodeToBuffer(cc, cLength, pos);
// Start at the root node
GifTree curNode = first;
// loop through the pixels
for (int y = 0; y < h; y++) {
// assume band 0 is the pixel numbers
data.getSamples(0, y, w, 1, 0, chunk);
for (int x = 0; x < w;) {
int curPix = chunk[x];
if (curNode.node != null && curNode.node[curPix] != null) {
// if we (still) match an existing string, continue
curNode = curNode.node[curPix];
chainlen++;
x++;
continue;
} else if (curNode.type == GifTree.SEARCH) {
// if we hit a search node, check for a match
newNode = curNode.nxt;
// Loop for the value in the search list
while (newNode.alt != null) {
if (newNode.idx == curPix) break;
newNode = newNode.alt;
}
// We found a value, follow that trail and continue
if (newNode.idx == curPix) {
chainlen++;
curNode = newNode;
x++;
continue;
}
}
// Here we didn't find a match, create a new node
newNode = new GifTree(GifTree.TERMIN, next, curPix);
switch (curNode.type) {
case GifTree.LOOKUP:
// add the node to the existing lookup
curNode.node[curPix] = newNode;
break;
case GifTree.SEARCH:
// make the search to a lookup and insert the new node
curNode.node = new GifTree[256];
curNode.type = GifTree.LOOKUP;
curNode.node[curPix] = newNode;
// insert the old search list node, too
curNode.node[curNode.nxt.idx] = curNode.nxt;
// lookup counter
lookuptypes++;
// Remove the link to the search list
curNode.nxt = null;
break;
case GifTree.TERMIN:
// Link the old list to the new alternatives
newNode.alt = curNode.nxt;
newNode.nxt = null;
// make an existing terminal node to a search node
curNode.nxt = newNode;
curNode.type = GifTree.SEARCH;
break;
default:
// We have a problem here, this is not foreseen
}
// increase the node counter
nodecount++;
// so we have a code to add
pos = addCodeToBuffer(curNode.code, cLength, pos);
// Check the chain length and reset
if (chainlen > maxchainlen) maxchainlen = chainlen;
chainlen = 0;
// Do we have a full block ? emit
if (pos >= BLOCKLEN) {
stream.write(BLOCKLEN);
stream.write(buffer, 0, BLOCKLEN);
buffer[0] = buffer[BLOCKLEN];
buffer[1] = buffer[BLOCKLEN + 1];
buffer[2] = buffer[BLOCKLEN + 2];
buffer[3] = buffer[BLOCKLEN + 3];
pos -= BLOCKLEN;
}
// Reset the search to the first node again
curNode = first;
// Define the next code value, possible extending the code
// length
if (next == (1 << cLength)) cLength++;
next++;
// if we reach the maximum code (12bit == 0xfff == 4095)
if (next == 0xfff) {
// Reset the tree and emit the clear code
clearTree(cc, first);
pos = addCodeToBuffer(cc, cLength, pos);
if (pos >= BLOCKLEN) {
stream.write(BLOCKLEN);
stream.write(buffer, 0, BLOCKLEN);
buffer[0] = buffer[BLOCKLEN];
buffer[1] = buffer[BLOCKLEN + 1];
buffer[2] = buffer[BLOCKLEN + 2];
buffer[3] = buffer[BLOCKLEN + 3];
pos -= BLOCKLEN;
}
// Reset the next code value and code length
next = cc + 2;
cLength = (depth == 1) ? 3 : depth + 1;
}
}
}
// add the last code to to the buffer
pos = addCodeToBuffer(curNode.code, cLength, pos);
if (pos >= BLOCKLEN - 3) {
stream.write(BLOCKLEN - 3);
stream.write(buffer, 0, BLOCKLEN - 3);
buffer[0] = buffer[BLOCKLEN - 3];
buffer[1] = buffer[BLOCKLEN - 2];
buffer[2] = buffer[BLOCKLEN - 1];
buffer[3] = buffer[BLOCKLEN];
buffer[4] = buffer[BLOCKLEN + 1];
pos -= BLOCKLEN - 3;
}
pos = addCodeToBuffer(eoi, cLength, pos); // end of image
pos = addCodeToBuffer(0x0, -1, pos); // flush fill
stream.write(pos);
stream.write(buffer, 0, pos);
// last but not least the data itself
stream.write(0x00); // empty subblock
}
private void clearTree(int cc, GifTree root) {
// Reset counters
maxchainlen = 0;
lookuptypes = 1;
nodecount = cc;
// clear rest of root nodes
if (root.node == null) {
root.node = new GifTree[256];
} else {
for (int i = cc; i < root.node.length; i++) {
root.node[i] = null;
}
}
// Setup base root nodes
for (int i = 0; i < cc; i++) {
root.node[i] = new GifTree(GifTree.TERMIN, i, i);
}
}
private int addCodeToBuffer(int code, int n, int pos) {
int mask;
if (n < 0) {
if (need < 8) {
pos++;
buffer[pos] = 0;
}
need = 8;
return pos;
}
while (n >= need) {
mask = (1 << need) - 1;
buffer[pos] += (mask & code) << (8 - need);
pos++;
buffer[pos] = 0;
code = code >> need;
n -= need;
need = 8;
}
if (n != 0) {
mask = (1 << n) - 1;
buffer[pos] += (mask & code) << (8 - need);
need -= n;
}
return pos;
}
}