All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 : *

    *
  1. backgroundColorIndex - The RGB color value of the background color *
  2. 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy