0001: /*
0002: * $RCSfile: GIFImageWriter.java,v $
0003: *
0004: *
0005: * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved.
0006: *
0007: * Redistribution and use in source and binary forms, with or without
0008: * modification, are permitted provided that the following conditions
0009: * are met:
0010: *
0011: * - Redistribution of source code must retain the above copyright
0012: * notice, this list of conditions and the following disclaimer.
0013: *
0014: * - Redistribution in binary form must reproduce the above copyright
0015: * notice, this list of conditions and the following disclaimer in
0016: * the documentation and/or other materials provided with the
0017: * distribution.
0018: *
0019: * Neither the name of Sun Microsystems, Inc. or the names of
0020: * contributors may be used to endorse or promote products derived
0021: * from this software without specific prior written permission.
0022: *
0023: * This software is provided "AS IS," without a warranty of any
0024: * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
0025: * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
0026: * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
0027: * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL
0028: * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF
0029: * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
0030: * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR
0031: * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
0032: * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
0033: * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
0034: * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
0035: * POSSIBILITY OF SUCH DAMAGES.
0036: *
0037: * You acknowledge that this software is not designed or intended for
0038: * use in the design, construction, operation or maintenance of any
0039: * nuclear facility.
0040: *
0041: * $Revision: 1.3 $
0042: * $Date: 2006/03/24 22:30:10 $
0043: * $State: Exp $
0044: */
0045:
0046: package com.sun.media.imageioimpl.plugins.gif;
0047:
0048: import java.awt.Dimension;
0049: import java.awt.Rectangle;
0050: import java.awt.image.ColorModel;
0051: import java.awt.image.ComponentSampleModel;
0052: import java.awt.image.DataBufferByte;
0053: import java.awt.image.IndexColorModel;
0054: import java.awt.image.Raster;
0055: import java.awt.image.RenderedImage;
0056: import java.awt.image.SampleModel;
0057: import java.awt.image.WritableRaster;
0058: import java.io.IOException;
0059: import java.nio.ByteOrder;
0060: import java.util.Arrays;
0061: import java.util.Iterator;
0062: import java.util.Locale;
0063: import javax.imageio.IIOException;
0064: import javax.imageio.IIOImage;
0065: import javax.imageio.ImageTypeSpecifier;
0066: import javax.imageio.ImageWriteParam;
0067: import javax.imageio.ImageWriter;
0068: import javax.imageio.spi.ImageWriterSpi;
0069: import javax.imageio.metadata.IIOInvalidTreeException;
0070: import javax.imageio.metadata.IIOMetadata;
0071: import javax.imageio.metadata.IIOMetadataFormatImpl;
0072: import javax.imageio.metadata.IIOMetadataNode;
0073: import javax.imageio.stream.ImageOutputStream;
0074: import org.w3c.dom.Node;
0075: import org.w3c.dom.NodeList;
0076: import com.sun.media.imageioimpl.common.LZWCompressor;
0077: import com.sun.media.imageioimpl.common.PaletteBuilder;
0078:
0079: public class GIFImageWriter extends ImageWriter {
0080: private static final boolean DEBUG = false; // XXX false for release!
0081:
0082: static final String STANDARD_METADATA_NAME = IIOMetadataFormatImpl.standardMetadataFormatName;
0083:
0084: static final String STREAM_METADATA_NAME = GIFWritableStreamMetadata.NATIVE_FORMAT_NAME;
0085:
0086: static final String IMAGE_METADATA_NAME = GIFWritableImageMetadata.NATIVE_FORMAT_NAME;
0087:
0088: /**
0089: * The <code>output</code> case to an <code>ImageOutputStream</code>.
0090: */
0091: private ImageOutputStream stream = null;
0092:
0093: /**
0094: * Whether a sequence is being written.
0095: */
0096: private boolean isWritingSequence = false;
0097:
0098: /**
0099: * Whether the header has been written.
0100: */
0101: private boolean wroteSequenceHeader = false;
0102:
0103: /**
0104: * The stream metadata of a sequence.
0105: */
0106: private GIFWritableStreamMetadata theStreamMetadata = null;
0107:
0108: /**
0109: * The index of the image being written.
0110: */
0111: private int imageIndex = 0;
0112:
0113: /**
0114: * The number of bits represented by the value which should be a
0115: * legal length for a color table.
0116: */
0117: private static int getNumBits(int value) throws IOException {
0118: int numBits;
0119: switch (value) {
0120: case 2:
0121: numBits = 1;
0122: break;
0123: case 4:
0124: numBits = 2;
0125: break;
0126: case 8:
0127: numBits = 3;
0128: break;
0129: case 16:
0130: numBits = 4;
0131: break;
0132: case 32:
0133: numBits = 5;
0134: break;
0135: case 64:
0136: numBits = 6;
0137: break;
0138: case 128:
0139: numBits = 7;
0140: break;
0141: case 256:
0142: numBits = 8;
0143: break;
0144: default:
0145: throw new IOException("Bad palette length: " + value + "!");
0146: }
0147:
0148: return numBits;
0149: }
0150:
0151: /**
0152: * Compute the source region and destination dimensions taking any
0153: * parameter settings into account.
0154: */
0155: private static void computeRegions(Rectangle sourceBounds,
0156: Dimension destSize, ImageWriteParam p) {
0157: ImageWriteParam param;
0158: int periodX = 1;
0159: int periodY = 1;
0160: if (p != null) {
0161: int[] sourceBands = p.getSourceBands();
0162: if (sourceBands != null
0163: && (sourceBands.length != 1 || sourceBands[0] != 0)) {
0164: throw new IllegalArgumentException(
0165: "Cannot sub-band image!");
0166: }
0167:
0168: // Get source region and subsampling factors
0169: Rectangle sourceRegion = p.getSourceRegion();
0170: if (sourceRegion != null) {
0171: // Clip to actual image bounds
0172: sourceRegion = sourceRegion.intersection(sourceBounds);
0173: sourceBounds.setBounds(sourceRegion);
0174: }
0175:
0176: // Adjust for subsampling offsets
0177: int gridX = p.getSubsamplingXOffset();
0178: int gridY = p.getSubsamplingYOffset();
0179: sourceBounds.x += gridX;
0180: sourceBounds.y += gridY;
0181: sourceBounds.width -= gridX;
0182: sourceBounds.height -= gridY;
0183:
0184: // Get subsampling factors
0185: periodX = p.getSourceXSubsampling();
0186: periodY = p.getSourceYSubsampling();
0187: }
0188:
0189: // Compute output dimensions
0190: destSize.setSize((sourceBounds.width + periodX - 1) / periodX,
0191: (sourceBounds.height + periodY - 1) / periodY);
0192: if (destSize.width <= 0 || destSize.height <= 0) {
0193: throw new IllegalArgumentException("Empty source region!");
0194: }
0195: }
0196:
0197: /**
0198: * Create a color table from the image ColorModel and SampleModel.
0199: */
0200: private static byte[] createColorTable(ColorModel colorModel,
0201: SampleModel sampleModel) {
0202: byte[] colorTable;
0203: if (colorModel instanceof IndexColorModel) {
0204: IndexColorModel icm = (IndexColorModel) colorModel;
0205: int mapSize = icm.getMapSize();
0206:
0207: /**
0208: * The GIF image format assumes that size of image palette
0209: * is power of two. We will use closest larger power of two
0210: * as size of color table.
0211: */
0212: int ctSize = getGifPaletteSize(mapSize);
0213:
0214: byte[] reds = new byte[ctSize];
0215: byte[] greens = new byte[ctSize];
0216: byte[] blues = new byte[ctSize];
0217: icm.getReds(reds);
0218: icm.getGreens(greens);
0219: icm.getBlues(blues);
0220:
0221: /**
0222: * fill tail of color component arrays by replica of first color
0223: * in order to avoid appearance of extra colors in the color table
0224: */
0225: for (int i = mapSize; i < ctSize; i++) {
0226: reds[i] = reds[0];
0227: greens[i] = greens[0];
0228: blues[i] = blues[0];
0229: }
0230:
0231: colorTable = new byte[3 * ctSize];
0232: int idx = 0;
0233: for (int i = 0; i < ctSize; i++) {
0234: colorTable[idx++] = reds[i];
0235: colorTable[idx++] = greens[i];
0236: colorTable[idx++] = blues[i];
0237: }
0238: } else if (sampleModel.getNumBands() == 1) {
0239: // create gray-scaled color table for single-banded images
0240: int numBits = sampleModel.getSampleSize()[0];
0241: if (numBits > 8) {
0242: numBits = 8;
0243: }
0244: int colorTableLength = 3 * (1 << numBits);
0245: colorTable = new byte[colorTableLength];
0246: for (int i = 0; i < colorTableLength; i++) {
0247: colorTable[i] = (byte) (i / 3);
0248: }
0249: } else {
0250: // We do not have enough information here
0251: // to create well-fit color table for RGB image.
0252: colorTable = null;
0253: }
0254:
0255: return colorTable;
0256: }
0257:
0258: /**
0259: * According do GIF specification size of clor table (palette here)
0260: * must be in range from 2 to 256 and must be power of 2.
0261: */
0262: private static int getGifPaletteSize(int x) {
0263: if (x <= 2) {
0264: return 2;
0265: }
0266: x = x - 1;
0267: x = x | (x >> 1);
0268: x = x | (x >> 2);
0269: x = x | (x >> 4);
0270: x = x | (x >> 8);
0271: x = x | (x >> 16);
0272: return x + 1;
0273: }
0274:
0275: public GIFImageWriter(GIFImageWriterSpi originatingProvider) {
0276: super (originatingProvider);
0277: if (DEBUG) {
0278: System.err.println("GIF Writer is created");
0279: }
0280: }
0281:
0282: public boolean canWriteSequence() {
0283: return true;
0284: }
0285:
0286: /**
0287: * Merges <code>inData</code> into <code>outData</code>. The supplied
0288: * metadata format name is attempted first and failing that the standard
0289: * metadata format name is attempted.
0290: */
0291: private void convertMetadata(String metadataFormatName,
0292: IIOMetadata inData, IIOMetadata outData) {
0293: String formatName = null;
0294:
0295: String nativeFormatName = inData.getNativeMetadataFormatName();
0296: if (nativeFormatName != null
0297: && nativeFormatName.equals(metadataFormatName)) {
0298: formatName = metadataFormatName;
0299: } else {
0300: String[] extraFormatNames = inData
0301: .getExtraMetadataFormatNames();
0302:
0303: if (extraFormatNames != null) {
0304: for (int i = 0; i < extraFormatNames.length; i++) {
0305: if (extraFormatNames[i].equals(metadataFormatName)) {
0306: formatName = metadataFormatName;
0307: break;
0308: }
0309: }
0310: }
0311: }
0312:
0313: if (formatName == null
0314: && inData.isStandardMetadataFormatSupported()) {
0315: formatName = STANDARD_METADATA_NAME;
0316: }
0317:
0318: if (formatName != null) {
0319: try {
0320: Node root = inData.getAsTree(formatName);
0321: outData.mergeTree(formatName, root);
0322: } catch (IIOInvalidTreeException e) {
0323: // ignore
0324: }
0325: }
0326: }
0327:
0328: /**
0329: * Creates a default stream metadata object and merges in the
0330: * supplied metadata.
0331: */
0332: public IIOMetadata convertStreamMetadata(IIOMetadata inData,
0333: ImageWriteParam param) {
0334: if (inData == null) {
0335: throw new IllegalArgumentException("inData == null!");
0336: }
0337:
0338: IIOMetadata sm = getDefaultStreamMetadata(param);
0339:
0340: convertMetadata(STREAM_METADATA_NAME, inData, sm);
0341:
0342: return sm;
0343: }
0344:
0345: /**
0346: * Creates a default image metadata object and merges in the
0347: * supplied metadata.
0348: */
0349: public IIOMetadata convertImageMetadata(IIOMetadata inData,
0350: ImageTypeSpecifier imageType, ImageWriteParam param) {
0351: if (inData == null) {
0352: throw new IllegalArgumentException("inData == null!");
0353: }
0354: if (imageType == null) {
0355: throw new IllegalArgumentException("imageType == null!");
0356: }
0357:
0358: GIFWritableImageMetadata im = (GIFWritableImageMetadata) getDefaultImageMetadata(
0359: imageType, param);
0360:
0361: // Save interlace flag state.
0362:
0363: boolean isProgressive = im.interlaceFlag;
0364:
0365: convertMetadata(IMAGE_METADATA_NAME, inData, im);
0366:
0367: // Undo change to interlace flag if not MODE_COPY_FROM_METADATA.
0368:
0369: if (param != null
0370: && param.canWriteProgressive()
0371: && param.getProgressiveMode() != param.MODE_COPY_FROM_METADATA) {
0372: im.interlaceFlag = isProgressive;
0373: }
0374:
0375: return im;
0376: }
0377:
0378: public void endWriteSequence() throws IOException {
0379: if (stream == null) {
0380: throw new IllegalStateException("output == null!");
0381: }
0382: if (!isWritingSequence) {
0383: throw new IllegalStateException(
0384: "prepareWriteSequence() was not invoked!");
0385: }
0386: writeTrailer();
0387: resetLocal();
0388: }
0389:
0390: public IIOMetadata getDefaultImageMetadata(
0391: ImageTypeSpecifier imageType, ImageWriteParam param) {
0392: GIFWritableImageMetadata imageMetadata = new GIFWritableImageMetadata();
0393:
0394: // Image dimensions
0395:
0396: SampleModel sampleModel = imageType.getSampleModel();
0397:
0398: Rectangle sourceBounds = new Rectangle(sampleModel.getWidth(),
0399: sampleModel.getHeight());
0400: Dimension destSize = new Dimension();
0401: computeRegions(sourceBounds, destSize, param);
0402:
0403: imageMetadata.imageWidth = destSize.width;
0404: imageMetadata.imageHeight = destSize.height;
0405:
0406: // Interlacing
0407:
0408: if (param != null
0409: && param.canWriteProgressive()
0410: && param.getProgressiveMode() == ImageWriteParam.MODE_DISABLED) {
0411: imageMetadata.interlaceFlag = false;
0412: } else {
0413: imageMetadata.interlaceFlag = true;
0414: }
0415:
0416: // Local color table
0417:
0418: ColorModel colorModel = imageType.getColorModel();
0419:
0420: imageMetadata.localColorTable = createColorTable(colorModel,
0421: sampleModel);
0422:
0423: // Transparency
0424:
0425: if (colorModel instanceof IndexColorModel) {
0426: int transparentIndex = ((IndexColorModel) colorModel)
0427: .getTransparentPixel();
0428: if (transparentIndex != -1) {
0429: imageMetadata.transparentColorFlag = true;
0430: imageMetadata.transparentColorIndex = transparentIndex;
0431: }
0432: }
0433:
0434: return imageMetadata;
0435: }
0436:
0437: public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
0438: GIFWritableStreamMetadata streamMetadata = new GIFWritableStreamMetadata();
0439: streamMetadata.version = "89a";
0440: return streamMetadata;
0441: }
0442:
0443: public ImageWriteParam getDefaultWriteParam() {
0444: return new GIFImageWriteParam(getLocale());
0445: }
0446:
0447: public void prepareWriteSequence(IIOMetadata streamMetadata)
0448: throws IOException {
0449:
0450: if (stream == null) {
0451: throw new IllegalStateException("Output is not set.");
0452: }
0453:
0454: resetLocal();
0455:
0456: // Save the possibly converted stream metadata as an instance variable.
0457: if (streamMetadata == null) {
0458: this .theStreamMetadata = (GIFWritableStreamMetadata) getDefaultStreamMetadata(null);
0459: } else {
0460: this .theStreamMetadata = new GIFWritableStreamMetadata();
0461: convertMetadata(STREAM_METADATA_NAME, streamMetadata,
0462: theStreamMetadata);
0463: }
0464:
0465: this .isWritingSequence = true;
0466: }
0467:
0468: public void reset() {
0469: super .reset();
0470: resetLocal();
0471: }
0472:
0473: /**
0474: * Resets locally defined instance variables.
0475: */
0476: private void resetLocal() {
0477: this .isWritingSequence = false;
0478: this .wroteSequenceHeader = false;
0479: this .theStreamMetadata = null;
0480: this .imageIndex = 0;
0481: }
0482:
0483: public void setOutput(Object output) {
0484: super .setOutput(output);
0485: if (output != null) {
0486: if (!(output instanceof ImageOutputStream)) {
0487: throw new IllegalArgumentException(
0488: "output is not an ImageOutputStream");
0489: }
0490: this .stream = (ImageOutputStream) output;
0491: this .stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
0492: } else {
0493: this .stream = null;
0494: }
0495: }
0496:
0497: public void write(IIOMetadata sm, IIOImage iioimage,
0498: ImageWriteParam p) throws IOException {
0499: if (stream == null) {
0500: throw new IllegalStateException("output == null!");
0501: }
0502: if (iioimage == null) {
0503: throw new IllegalArgumentException("iioimage == null!");
0504: }
0505: if (iioimage.hasRaster()) {
0506: throw new UnsupportedOperationException(
0507: "canWriteRasters() == false!");
0508: }
0509:
0510: resetLocal();
0511:
0512: GIFWritableStreamMetadata streamMetadata;
0513: if (sm == null) {
0514: streamMetadata = (GIFWritableStreamMetadata) getDefaultStreamMetadata(p);
0515: } else {
0516: streamMetadata = (GIFWritableStreamMetadata) convertStreamMetadata(
0517: sm, p);
0518: }
0519:
0520: write(true, true, streamMetadata, iioimage, p);
0521: }
0522:
0523: public void writeToSequence(IIOImage image, ImageWriteParam param)
0524: throws IOException {
0525: if (stream == null) {
0526: throw new IllegalStateException("output == null!");
0527: }
0528: if (image == null) {
0529: throw new IllegalArgumentException("image == null!");
0530: }
0531: if (image.hasRaster()) {
0532: throw new UnsupportedOperationException(
0533: "canWriteRasters() == false!");
0534: }
0535: if (!isWritingSequence) {
0536: throw new IllegalStateException(
0537: "prepareWriteSequence() was not invoked!");
0538: }
0539:
0540: write(!wroteSequenceHeader, false, theStreamMetadata, image,
0541: param);
0542:
0543: if (!wroteSequenceHeader) {
0544: wroteSequenceHeader = true;
0545: }
0546:
0547: this .imageIndex++;
0548: }
0549:
0550: private boolean needToCreateIndex(RenderedImage image) {
0551:
0552: SampleModel sampleModel = image.getSampleModel();
0553: ColorModel colorModel = image.getColorModel();
0554:
0555: return sampleModel.getNumBands() != 1
0556: || sampleModel.getSampleSize()[0] > 8
0557: || colorModel.getComponentSize()[0] > 8;
0558: }
0559:
0560: /**
0561: * Writes any extension blocks, the Image Descriptor, the image data,
0562: * and optionally the header (Signature and Logical Screen Descriptor)
0563: * and trailer (Block Terminator).
0564: *
0565: * @param writeHeader Whether to write the header.
0566: * @param writeTrailer Whether to write the trailer.
0567: * @param sm The stream metadata or <code>null</code> if
0568: * <code>writeHeader</code> is <code>false</code>.
0569: * @param iioimage The image and image metadata.
0570: * @param p The write parameters.
0571: *
0572: * @throws IllegalArgumentException if the number of bands is not 1.
0573: * @throws IllegalArgumentException if the number of bits per sample is
0574: * greater than 8.
0575: * @throws IllegalArgumentException if the color component size is
0576: * greater than 8.
0577: * @throws IllegalArgumentException if <code>writeHeader</code> is
0578: * <code>true</code> and <code>sm</code> is <code>null</code>.
0579: * @throws IllegalArgumentException if <code>writeHeader</code> is
0580: * <code>false</code> and a sequence is not being written.
0581: */
0582: private void write(boolean writeHeader, boolean writeTrailer,
0583: IIOMetadata sm, IIOImage iioimage, ImageWriteParam p)
0584: throws IOException {
0585: clearAbortRequest();
0586:
0587: RenderedImage image = iioimage.getRenderedImage();
0588:
0589: // Check for ability to encode image.
0590: if (needToCreateIndex(image)) {
0591: image = PaletteBuilder.createIndexedImage(image);
0592: iioimage.setRenderedImage(image);
0593: }
0594:
0595: ColorModel colorModel = image.getColorModel();
0596: SampleModel sampleModel = image.getSampleModel();
0597:
0598: // Determine source region and destination dimensions.
0599: Rectangle sourceBounds = new Rectangle(image.getMinX(), image
0600: .getMinY(), image.getWidth(), image.getHeight());
0601: Dimension destSize = new Dimension();
0602: computeRegions(sourceBounds, destSize, p);
0603:
0604: // Convert any provided image metadata.
0605: GIFWritableImageMetadata imageMetadata = null;
0606: if (iioimage.getMetadata() != null) {
0607: imageMetadata = new GIFWritableImageMetadata();
0608: convertMetadata(IMAGE_METADATA_NAME,
0609: iioimage.getMetadata(), imageMetadata);
0610: // Converted rgb image can use palette different from global.
0611: // In order to avoid color artefacts we want to be sure we use
0612: // appropriate palette. For this we initialize local color table
0613: // from current color and sample models.
0614: // At this point we can guarantee that local color table can be
0615: // build because image was already converted to indexed or
0616: // gray-scale representations
0617: if (imageMetadata.localColorTable == null) {
0618: imageMetadata.localColorTable = createColorTable(
0619: colorModel, sampleModel);
0620:
0621: // in case of indexed image we should take care of
0622: // transparent pixels
0623: if (colorModel instanceof IndexColorModel) {
0624: IndexColorModel icm = (IndexColorModel) colorModel;
0625: int index = icm.getTransparentPixel();
0626: imageMetadata.transparentColorFlag = (index != -1);
0627: if (imageMetadata.transparentColorFlag) {
0628: imageMetadata.transparentColorIndex = index;
0629: }
0630: /* NB: transparentColorFlag might have not beed reset for
0631: greyscale images but explicitly reseting it here
0632: is potentially not right thing to do until we have way
0633: to find whether current value was explicitly set by
0634: the user.
0635: */
0636: }
0637: }
0638: }
0639:
0640: // Global color table values.
0641: byte[] globalColorTable = null;
0642:
0643: // Write the header (Signature+Logical Screen Descriptor+
0644: // Global Color Table).
0645: if (writeHeader) {
0646: if (sm == null) {
0647: throw new IllegalArgumentException(
0648: "Cannot write null header!");
0649: }
0650:
0651: GIFWritableStreamMetadata streamMetadata = (GIFWritableStreamMetadata) sm;
0652:
0653: // Set the version if not set.
0654: if (streamMetadata.version == null) {
0655: streamMetadata.version = "89a";
0656: }
0657:
0658: // Set the Logical Screen Desriptor if not set.
0659: if (streamMetadata.logicalScreenWidth == GIFMetadata.UNDEFINED_INTEGER_VALUE) {
0660: streamMetadata.logicalScreenWidth = destSize.width;
0661: }
0662:
0663: if (streamMetadata.logicalScreenHeight == GIFMetadata.UNDEFINED_INTEGER_VALUE) {
0664: streamMetadata.logicalScreenHeight = destSize.height;
0665: }
0666:
0667: if (streamMetadata.colorResolution == GIFMetadata.UNDEFINED_INTEGER_VALUE) {
0668: streamMetadata.colorResolution = colorModel != null ? colorModel
0669: .getComponentSize()[0]
0670: : sampleModel.getSampleSize()[0];
0671: }
0672:
0673: // Set the Global Color Table if not set, i.e., if not
0674: // provided in the stream metadata.
0675: if (streamMetadata.globalColorTable == null) {
0676: if (isWritingSequence && imageMetadata != null
0677: && imageMetadata.localColorTable != null) {
0678: // Writing a sequence and a local color table was
0679: // provided in the metadata of the first image: use it.
0680: streamMetadata.globalColorTable = imageMetadata.localColorTable;
0681: } else if (imageMetadata == null
0682: || imageMetadata.localColorTable == null) {
0683: // Create a color table.
0684: streamMetadata.globalColorTable = createColorTable(
0685: colorModel, sampleModel);
0686: }
0687: }
0688:
0689: // Set the Global Color Table. At this point it should be
0690: // A) the global color table provided in stream metadata, if any;
0691: // B) the local color table of the image metadata, if any, if
0692: // writing a sequence;
0693: // C) a table created on the basis of the first image ColorModel
0694: // and SampleModel if no local color table is available; or
0695: // D) null if none of the foregoing conditions obtain (which
0696: // should only be if a sequence is not being written and
0697: // a local color table is provided in image metadata).
0698: globalColorTable = streamMetadata.globalColorTable;
0699:
0700: // Write the header.
0701: int bitsPerPixel;
0702: if (globalColorTable != null) {
0703: bitsPerPixel = getNumBits(globalColorTable.length / 3);
0704: } else if (imageMetadata != null
0705: && imageMetadata.localColorTable != null) {
0706: bitsPerPixel = getNumBits(imageMetadata.localColorTable.length / 3);
0707: } else {
0708: bitsPerPixel = sampleModel.getSampleSize(0);
0709: }
0710: writeHeader(streamMetadata, bitsPerPixel);
0711: } else if (isWritingSequence) {
0712: globalColorTable = theStreamMetadata.globalColorTable;
0713: } else {
0714: throw new IllegalArgumentException(
0715: "Must write header for single image!");
0716: }
0717:
0718: // Write extension blocks, Image Descriptor, and image data.
0719: writeImage(iioimage.getRenderedImage(), imageMetadata, p,
0720: globalColorTable, sourceBounds, destSize);
0721:
0722: // Write the trailer.
0723: if (writeTrailer) {
0724: writeTrailer();
0725: }
0726: }
0727:
0728: /**
0729: * Writes any extension blocks, the Image Descriptor, and the image data
0730: *
0731: * @param iioimage The image and image metadata.
0732: * @param param The write parameters.
0733: * @param globalColorTable The Global Color Table.
0734: * @param sourceBounds The source region.
0735: * @param destSize The destination dimensions.
0736: */
0737: private void writeImage(RenderedImage image,
0738: GIFWritableImageMetadata imageMetadata,
0739: ImageWriteParam param, byte[] globalColorTable,
0740: Rectangle sourceBounds, Dimension destSize)
0741: throws IOException {
0742: ColorModel colorModel = image.getColorModel();
0743: SampleModel sampleModel = image.getSampleModel();
0744:
0745: boolean writeGraphicsControlExtension;
0746: if (imageMetadata == null) {
0747: // Create default metadata.
0748: imageMetadata = (GIFWritableImageMetadata) getDefaultImageMetadata(
0749: new ImageTypeSpecifier(image), param);
0750:
0751: // Set GraphicControlExtension flag only if there is
0752: // transparency.
0753: writeGraphicsControlExtension = imageMetadata.transparentColorFlag;
0754: } else {
0755: // Check for GraphicControlExtension element.
0756: NodeList list = null;
0757: try {
0758: IIOMetadataNode root = (IIOMetadataNode) imageMetadata
0759: .getAsTree(IMAGE_METADATA_NAME);
0760: list = root
0761: .getElementsByTagName("GraphicControlExtension");
0762: } catch (IllegalArgumentException iae) {
0763: // Should never happen.
0764: }
0765:
0766: // Set GraphicControlExtension flag if element present.
0767: writeGraphicsControlExtension = list != null
0768: && list.getLength() > 0;
0769:
0770: // If progressive mode is not MODE_COPY_FROM_METADATA, ensure
0771: // the interlacing is set per the ImageWriteParam mode setting.
0772: if (param != null && param.canWriteProgressive()) {
0773: if (param.getProgressiveMode() == ImageWriteParam.MODE_DISABLED) {
0774: imageMetadata.interlaceFlag = false;
0775: } else if (param.getProgressiveMode() == ImageWriteParam.MODE_DEFAULT) {
0776: imageMetadata.interlaceFlag = true;
0777: }
0778: }
0779: }
0780:
0781: // Unset local color table if equal to global color table.
0782: if (Arrays.equals(globalColorTable,
0783: imageMetadata.localColorTable)) {
0784: imageMetadata.localColorTable = null;
0785: }
0786:
0787: // Override dimensions
0788: imageMetadata.imageWidth = destSize.width;
0789: imageMetadata.imageHeight = destSize.height;
0790:
0791: // Write Graphics Control Extension.
0792: if (writeGraphicsControlExtension) {
0793: writeGraphicControlExtension(imageMetadata);
0794: }
0795:
0796: // Write extension blocks.
0797: writePlainTextExtension(imageMetadata);
0798: writeApplicationExtension(imageMetadata);
0799: writeCommentExtension(imageMetadata);
0800:
0801: // Write Image Descriptor
0802: int bitsPerPixel = getNumBits(imageMetadata.localColorTable == null ? (globalColorTable == null ? sampleModel
0803: .getSampleSize(0)
0804: : globalColorTable.length / 3)
0805: : imageMetadata.localColorTable.length / 3);
0806: writeImageDescriptor(imageMetadata, bitsPerPixel);
0807:
0808: // Write image data
0809: writeRasterData(image, sourceBounds, destSize, param,
0810: imageMetadata.interlaceFlag);
0811: }
0812:
0813: private void writeRows(RenderedImage image,
0814: LZWCompressor compressor, int sx, int sdx, int sy, int sdy,
0815: int sw, int dy, int ddy, int dw, int dh,
0816: int numRowsWritten, int progressReportRowPeriod)
0817: throws IOException {
0818: if (DEBUG)
0819: System.out.println("Writing unoptimized");
0820:
0821: int[] sbuf = new int[sw];
0822: byte[] dbuf = new byte[dw];
0823:
0824: Raster raster = image.getNumXTiles() == 1
0825: && image.getNumYTiles() == 1 ? image.getTile(0, 0)
0826: : image.getData();
0827: for (int y = dy; y < dh; y += ddy) {
0828: if (numRowsWritten % progressReportRowPeriod == 0) {
0829: if (abortRequested()) {
0830: processWriteAborted();
0831: return;
0832: }
0833: processImageProgress((numRowsWritten * 100.0F) / dh);
0834: }
0835:
0836: raster.getSamples(sx, sy, sw, 1, 0, sbuf);
0837: for (int i = 0, j = 0; i < dw; i++, j += sdx) {
0838: dbuf[i] = (byte) sbuf[j];
0839: }
0840: compressor.compress(dbuf, 0, dw);
0841: numRowsWritten++;
0842: sy += sdy;
0843: }
0844: }
0845:
0846: private void writeRowsOpt(byte[] data, int offset, int lineStride,
0847: LZWCompressor compressor, int dy, int ddy, int dw, int dh,
0848: int numRowsWritten, int progressReportRowPeriod)
0849: throws IOException {
0850: if (DEBUG)
0851: System.out.println("Writing optimized");
0852:
0853: offset += dy * lineStride;
0854: lineStride *= ddy;
0855: for (int y = dy; y < dh; y += ddy) {
0856: if (numRowsWritten % progressReportRowPeriod == 0) {
0857: if (abortRequested()) {
0858: processWriteAborted();
0859: return;
0860: }
0861: processImageProgress((numRowsWritten * 100.0F) / dh);
0862: }
0863:
0864: compressor.compress(data, offset, dw);
0865: numRowsWritten++;
0866: offset += lineStride;
0867: }
0868: }
0869:
0870: private void writeRasterData(RenderedImage image,
0871: Rectangle sourceBounds, Dimension destSize,
0872: ImageWriteParam param, boolean interlaceFlag)
0873: throws IOException {
0874:
0875: int sourceXOffset = sourceBounds.x;
0876: int sourceYOffset = sourceBounds.y;
0877: int sourceWidth = sourceBounds.width;
0878: int sourceHeight = sourceBounds.height;
0879:
0880: int destWidth = destSize.width;
0881: int destHeight = destSize.height;
0882:
0883: int periodX;
0884: int periodY;
0885: if (param == null) {
0886: periodX = 1;
0887: periodY = 1;
0888: } else {
0889: periodX = param.getSourceXSubsampling();
0890: periodY = param.getSourceYSubsampling();
0891: }
0892:
0893: SampleModel sampleModel = image.getSampleModel();
0894: int bitsPerPixel = sampleModel.getSampleSize()[0];
0895:
0896: int initCodeSize = bitsPerPixel;
0897: if (initCodeSize == 1) {
0898: initCodeSize++;
0899: }
0900: stream.write(initCodeSize);
0901:
0902: LZWCompressor compressor = new LZWCompressor(stream,
0903: initCodeSize, false);
0904:
0905: boolean isOptimizedCase = periodX == 1
0906: && periodY == 1
0907: && sampleModel instanceof ComponentSampleModel
0908: && image.getNumXTiles() == 1
0909: && image.getNumYTiles() == 1
0910: && image.getTile(0, 0).getDataBuffer() instanceof DataBufferByte;
0911:
0912: int numRowsWritten = 0;
0913:
0914: int progressReportRowPeriod = Math.max(destHeight / 20, 1);
0915:
0916: processImageStarted(imageIndex);
0917:
0918: if (interlaceFlag) {
0919: if (DEBUG)
0920: System.out.println("Writing interlaced");
0921:
0922: if (isOptimizedCase) {
0923: Raster tile = image.getTile(0, 0);
0924: byte[] data = ((DataBufferByte) tile.getDataBuffer())
0925: .getData();
0926: ComponentSampleModel csm = (ComponentSampleModel) tile
0927: .getSampleModel();
0928: int offset = csm
0929: .getOffset(
0930: sourceXOffset
0931: - tile
0932: .getSampleModelTranslateX(),
0933: sourceYOffset
0934: - tile
0935: .getSampleModelTranslateY(),
0936: 0);
0937: int lineStride = csm.getScanlineStride();
0938:
0939: writeRowsOpt(data, offset, lineStride, compressor, 0,
0940: 8, destWidth, destHeight, numRowsWritten,
0941: progressReportRowPeriod);
0942:
0943: if (abortRequested()) {
0944: return;
0945: }
0946:
0947: numRowsWritten += destHeight / 8;
0948:
0949: writeRowsOpt(data, offset, lineStride, compressor, 4,
0950: 8, destWidth, destHeight, numRowsWritten,
0951: progressReportRowPeriod);
0952:
0953: if (abortRequested()) {
0954: return;
0955: }
0956:
0957: numRowsWritten += (destHeight - 4) / 8;
0958:
0959: writeRowsOpt(data, offset, lineStride, compressor, 2,
0960: 4, destWidth, destHeight, numRowsWritten,
0961: progressReportRowPeriod);
0962:
0963: if (abortRequested()) {
0964: return;
0965: }
0966:
0967: numRowsWritten += (destHeight - 2) / 4;
0968:
0969: writeRowsOpt(data, offset, lineStride, compressor, 1,
0970: 2, destWidth, destHeight, numRowsWritten,
0971: progressReportRowPeriod);
0972: } else {
0973: writeRows(image, compressor, sourceXOffset, periodX,
0974: sourceYOffset, 8 * periodY, sourceWidth, 0, 8,
0975: destWidth, destHeight, numRowsWritten,
0976: progressReportRowPeriod);
0977:
0978: if (abortRequested()) {
0979: return;
0980: }
0981:
0982: numRowsWritten += destHeight / 8;
0983:
0984: writeRows(image, compressor, sourceXOffset, periodX,
0985: sourceYOffset + 4 * periodY, 8 * periodY,
0986: sourceWidth, 4, 8, destWidth, destHeight,
0987: numRowsWritten, progressReportRowPeriod);
0988:
0989: if (abortRequested()) {
0990: return;
0991: }
0992:
0993: numRowsWritten += (destHeight - 4) / 8;
0994:
0995: writeRows(image, compressor, sourceXOffset, periodX,
0996: sourceYOffset + 2 * periodY, 4 * periodY,
0997: sourceWidth, 2, 4, destWidth, destHeight,
0998: numRowsWritten, progressReportRowPeriod);
0999:
1000: if (abortRequested()) {
1001: return;
1002: }
1003:
1004: numRowsWritten += (destHeight - 2) / 4;
1005:
1006: writeRows(image, compressor, sourceXOffset, periodX,
1007: sourceYOffset + periodY, 2 * periodY,
1008: sourceWidth, 1, 2, destWidth, destHeight,
1009: numRowsWritten, progressReportRowPeriod);
1010: }
1011: } else {
1012: if (DEBUG)
1013: System.out.println("Writing non-interlaced");
1014:
1015: if (isOptimizedCase) {
1016: Raster tile = image.getTile(0, 0);
1017: byte[] data = ((DataBufferByte) tile.getDataBuffer())
1018: .getData();
1019: ComponentSampleModel csm = (ComponentSampleModel) tile
1020: .getSampleModel();
1021: int offset = csm
1022: .getOffset(
1023: sourceXOffset
1024: - tile
1025: .getSampleModelTranslateX(),
1026: sourceYOffset
1027: - tile
1028: .getSampleModelTranslateY(),
1029: 0);
1030: int lineStride = csm.getScanlineStride();
1031:
1032: writeRowsOpt(data, offset, lineStride, compressor, 0,
1033: 1, destWidth, destHeight, numRowsWritten,
1034: progressReportRowPeriod);
1035: } else {
1036: writeRows(image, compressor, sourceXOffset, periodX,
1037: sourceYOffset, periodY, sourceWidth, 0, 1,
1038: destWidth, destHeight, numRowsWritten,
1039: progressReportRowPeriod);
1040: }
1041: }
1042:
1043: if (abortRequested()) {
1044: return;
1045: }
1046:
1047: processImageProgress(100.0F);
1048:
1049: compressor.flush();
1050:
1051: stream.write(0x00);
1052:
1053: processImageComplete();
1054: }
1055:
1056: private void writeHeader(String version, int logicalScreenWidth,
1057: int logicalScreenHeight, int colorResolution,
1058: int pixelAspectRatio, int backgroundColorIndex,
1059: boolean sortFlag, int bitsPerPixel, byte[] globalColorTable)
1060: throws IOException {
1061: try {
1062: // Signature
1063: stream.writeBytes("GIF" + version);
1064:
1065: // Screen Descriptor
1066: // Width
1067: stream.writeShort((short) logicalScreenWidth);
1068:
1069: // Height
1070: stream.writeShort((short) logicalScreenHeight);
1071:
1072: // Global Color Table
1073: // Packed fields
1074: int packedFields = globalColorTable != null ? 0x80 : 0x00;
1075: packedFields |= ((colorResolution - 1) & 0x7) << 4;
1076: if (sortFlag) {
1077: packedFields |= 0x8;
1078: }
1079: packedFields |= (bitsPerPixel - 1);
1080: stream.write(packedFields);
1081:
1082: // Background color index
1083: stream.write(backgroundColorIndex);
1084:
1085: // Pixel aspect ratio
1086: stream.write(pixelAspectRatio);
1087:
1088: // Global Color Table
1089: if (globalColorTable != null) {
1090: stream.write(globalColorTable);
1091: }
1092: } catch (IOException e) {
1093: throw new IIOException("I/O error writing header!", e);
1094: }
1095: }
1096:
1097: private void writeHeader(IIOMetadata streamMetadata,
1098: int bitsPerPixel) throws IOException {
1099:
1100: GIFWritableStreamMetadata sm;
1101: if (streamMetadata instanceof GIFWritableStreamMetadata) {
1102: sm = (GIFWritableStreamMetadata) streamMetadata;
1103: } else {
1104: sm = new GIFWritableStreamMetadata();
1105: Node root = streamMetadata.getAsTree(STREAM_METADATA_NAME);
1106: sm.setFromTree(STREAM_METADATA_NAME, root);
1107: }
1108:
1109: writeHeader(sm.version, sm.logicalScreenWidth,
1110: sm.logicalScreenHeight, sm.colorResolution,
1111: sm.pixelAspectRatio, sm.backgroundColorIndex,
1112: sm.sortFlag, bitsPerPixel, sm.globalColorTable);
1113: }
1114:
1115: private void writeGraphicControlExtension(int disposalMethod,
1116: boolean userInputFlag, boolean transparentColorFlag,
1117: int delayTime, int transparentColorIndex)
1118: throws IOException {
1119: try {
1120: stream.write(0x21);
1121: stream.write(0xf9);
1122:
1123: stream.write(4);
1124:
1125: int packedFields = (disposalMethod & 0x3) << 2;
1126: if (userInputFlag) {
1127: packedFields |= 0x2;
1128: }
1129: if (transparentColorFlag) {
1130: packedFields |= 0x1;
1131: }
1132: stream.write(packedFields);
1133:
1134: stream.writeShort((short) delayTime);
1135:
1136: stream.write(transparentColorIndex);
1137: stream.write(0x00);
1138: } catch (IOException e) {
1139: throw new IIOException(
1140: "I/O error writing Graphic Control Extension!", e);
1141: }
1142: }
1143:
1144: private void writeGraphicControlExtension(
1145: GIFWritableImageMetadata im) throws IOException {
1146: writeGraphicControlExtension(im.disposalMethod,
1147: im.userInputFlag, im.transparentColorFlag,
1148: im.delayTime, im.transparentColorIndex);
1149: }
1150:
1151: private void writeBlocks(byte[] data) throws IOException {
1152: if (data != null && data.length > 0) {
1153: int offset = 0;
1154: while (offset < data.length) {
1155: int len = Math.min(data.length - offset, 255);
1156: stream.write(len);
1157: stream.write(data, offset, len);
1158: offset += len;
1159: }
1160: }
1161: }
1162:
1163: private void writePlainTextExtension(GIFWritableImageMetadata im)
1164: throws IOException {
1165: if (im.hasPlainTextExtension) {
1166: try {
1167: stream.write(0x21);
1168: stream.write(0x1);
1169:
1170: stream.write(12);
1171:
1172: stream.writeShort(im.textGridLeft);
1173: stream.writeShort(im.textGridTop);
1174: stream.writeShort(im.textGridWidth);
1175: stream.writeShort(im.textGridHeight);
1176: stream.write(im.characterCellWidth);
1177: stream.write(im.characterCellHeight);
1178: stream.write(im.textForegroundColor);
1179: stream.write(im.textBackgroundColor);
1180:
1181: writeBlocks(im.text);
1182:
1183: stream.write(0x00);
1184: } catch (IOException e) {
1185: throw new IIOException(
1186: "I/O error writing Plain Text Extension!", e);
1187: }
1188: }
1189: }
1190:
1191: private void writeApplicationExtension(GIFWritableImageMetadata im)
1192: throws IOException {
1193: if (im.applicationIDs != null) {
1194: Iterator iterIDs = im.applicationIDs.iterator();
1195: Iterator iterCodes = im.authenticationCodes.iterator();
1196: Iterator iterData = im.applicationData.iterator();
1197:
1198: while (iterIDs.hasNext()) {
1199: try {
1200: stream.write(0x21);
1201: stream.write(0xff);
1202:
1203: stream.write(11);
1204: stream.write((byte[]) iterIDs.next(), 0, 8);
1205: stream.write((byte[]) iterCodes.next(), 0, 3);
1206:
1207: writeBlocks((byte[]) iterData.next());
1208:
1209: stream.write(0x00);
1210: } catch (IOException e) {
1211: throw new IIOException(
1212: "I/O error writing Application Extension!",
1213: e);
1214: }
1215: }
1216: }
1217: }
1218:
1219: private void writeCommentExtension(GIFWritableImageMetadata im)
1220: throws IOException {
1221: if (im.comments != null) {
1222: try {
1223: Iterator iter = im.comments.iterator();
1224: while (iter.hasNext()) {
1225: stream.write(0x21);
1226: stream.write(0xfe);
1227: writeBlocks((byte[]) iter.next());
1228: stream.write(0x00);
1229: }
1230: } catch (IOException e) {
1231: throw new IIOException(
1232: "I/O error writing Comment Extension!", e);
1233: }
1234: }
1235: }
1236:
1237: private void writeImageDescriptor(int imageLeftPosition,
1238: int imageTopPosition, int imageWidth, int imageHeight,
1239: boolean interlaceFlag, boolean sortFlag, int bitsPerPixel,
1240: byte[] localColorTable) throws IOException {
1241:
1242: try {
1243: stream.write(0x2c);
1244:
1245: stream.writeShort((short) imageLeftPosition);
1246: stream.writeShort((short) imageTopPosition);
1247: stream.writeShort((short) imageWidth);
1248: stream.writeShort((short) imageHeight);
1249:
1250: int packedFields = localColorTable != null ? 0x80 : 0x00;
1251: if (interlaceFlag) {
1252: packedFields |= 0x40;
1253: }
1254: if (sortFlag) {
1255: packedFields |= 0x8;
1256: }
1257: packedFields |= (bitsPerPixel - 1);
1258: stream.write(packedFields);
1259:
1260: if (localColorTable != null) {
1261: stream.write(localColorTable);
1262: }
1263: } catch (IOException e) {
1264: throw new IIOException(
1265: "I/O error writing Image Descriptor!", e);
1266: }
1267: }
1268:
1269: private void writeImageDescriptor(
1270: GIFWritableImageMetadata imageMetadata, int bitsPerPixel)
1271: throws IOException {
1272:
1273: writeImageDescriptor(imageMetadata.imageLeftPosition,
1274: imageMetadata.imageTopPosition,
1275: imageMetadata.imageWidth, imageMetadata.imageHeight,
1276: imageMetadata.interlaceFlag, imageMetadata.sortFlag,
1277: bitsPerPixel, imageMetadata.localColorTable);
1278: }
1279:
1280: private void writeTrailer() throws IOException {
1281: stream.write(0x3b);
1282: }
1283: }
1284:
1285: class GIFImageWriteParam extends ImageWriteParam {
1286: GIFImageWriteParam(Locale locale) {
1287: super (locale);
1288: this .canWriteCompressed = true;
1289: this .canWriteProgressive = true;
1290: this .compressionTypes = new String[] { "LZW", "lzw" };
1291: this .compressionType = compressionTypes[0];
1292: }
1293:
1294: public void setCompressionMode(int mode) {
1295: if (mode == MODE_DISABLED) {
1296: throw new UnsupportedOperationException(
1297: "MODE_DISABLED is not supported.");
1298: }
1299: super.setCompressionMode(mode);
1300: }
1301: }
|