001/*
002 * $RCSfile: GIFImageWriter.java,v $
003 *
004 * 
005 * Copyright (c) 2005 Sun Microsystems, Inc. All  Rights Reserved.
006 * 
007 * Redistribution and use in source and binary forms, with or without
008 * modification, are permitted provided that the following conditions
009 * are met: 
010 * 
011 * - Redistribution of source code must retain the above copyright 
012 *   notice, this  list of conditions and the following disclaimer.
013 * 
014 * - Redistribution in binary form must reproduce the above copyright
015 *   notice, this list of conditions and the following disclaimer in 
016 *   the documentation and/or other materials provided with the
017 *   distribution.
018 * 
019 * Neither the name of Sun Microsystems, Inc. or the names of 
020 * contributors may be used to endorse or promote products derived 
021 * from this software without specific prior written permission.
022 * 
023 * This software is provided "AS IS," without a warranty of any 
024 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND 
025 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 
026 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
027 * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL 
028 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF 
029 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
030 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR 
031 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
032 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
033 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
034 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
035 * POSSIBILITY OF SUCH DAMAGES. 
036 * 
037 * You acknowledge that this software is not designed or intended for 
038 * use in the design, construction, operation or maintenance of any 
039 * nuclear facility. 
040 *
041 * $Revision: 1.3 $
042 * $Date: 2006/03/24 22:30:10 $
043 * $State: Exp $
044 */
045
046package com.github.jaiimageio.impl.plugins.gif;
047
048import java.awt.Dimension;
049import java.awt.Rectangle;
050import java.awt.image.ColorModel;
051import java.awt.image.ComponentSampleModel;
052import java.awt.image.DataBufferByte;
053import java.awt.image.IndexColorModel;
054import java.awt.image.Raster;
055import java.awt.image.RenderedImage;
056import java.awt.image.SampleModel;
057import java.io.IOException;
058import java.nio.ByteOrder;
059import java.util.Arrays;
060import java.util.Iterator;
061import java.util.Locale;
062
063import javax.imageio.IIOException;
064import javax.imageio.IIOImage;
065import javax.imageio.ImageTypeSpecifier;
066import javax.imageio.ImageWriteParam;
067import javax.imageio.ImageWriter;
068import javax.imageio.metadata.IIOInvalidTreeException;
069import javax.imageio.metadata.IIOMetadata;
070import javax.imageio.metadata.IIOMetadataFormatImpl;
071import javax.imageio.metadata.IIOMetadataNode;
072import javax.imageio.stream.ImageOutputStream;
073
074import org.w3c.dom.Node;
075import org.w3c.dom.NodeList;
076
077import com.github.jaiimageio.impl.common.LZWCompressor;
078import com.github.jaiimageio.impl.common.PaletteBuilder;
079
080public class GIFImageWriter extends ImageWriter {
081    private static final boolean DEBUG = false; // XXX false for release!
082    
083    static final String STANDARD_METADATA_NAME =
084    IIOMetadataFormatImpl.standardMetadataFormatName;
085
086    static final String STREAM_METADATA_NAME =
087    GIFWritableStreamMetadata.NATIVE_FORMAT_NAME;
088    
089    static final String IMAGE_METADATA_NAME =
090    GIFWritableImageMetadata.NATIVE_FORMAT_NAME;
091    
092    /**
093     * The <code>output</code> case to an <code>ImageOutputStream</code>.
094     */
095    private ImageOutputStream stream = null;
096
097    /**
098     * Whether a sequence is being written.
099     */
100    private boolean isWritingSequence = false;
101
102    /**
103     * Whether the header has been written.
104     */
105    private boolean wroteSequenceHeader = false;
106
107    /**
108     * The stream metadata of a sequence.
109     */
110    private GIFWritableStreamMetadata theStreamMetadata = null;
111
112    /**
113     * The index of the image being written.
114     */
115    private int imageIndex = 0;
116
117    /**
118     * The number of bits represented by the value which should be a
119     * legal length for a color table.
120     */
121    private static int getNumBits(int value) throws IOException {
122        int numBits;
123        switch(value) {
124        case 2:
125            numBits = 1;
126            break;
127        case 4:
128            numBits = 2;
129            break;
130        case 8:
131            numBits = 3;
132            break;
133        case 16:
134            numBits = 4;
135            break;
136        case 32:
137            numBits = 5;
138            break;
139        case 64:
140            numBits = 6;
141            break;
142        case 128:
143            numBits = 7;
144            break;
145        case 256:
146            numBits = 8;
147            break;
148        default:
149            throw new IOException("Bad palette length: "+value+"!");
150        }
151        
152        return numBits;
153    }
154    
155    /**
156     * Compute the source region and destination dimensions taking any
157     * parameter settings into account.
158     */
159    private static void computeRegions(Rectangle sourceBounds,
160                                       Dimension destSize,
161                                       ImageWriteParam p) {
162        ImageWriteParam param;
163        int periodX = 1;
164        int periodY = 1;
165        if (p != null) {
166            int[] sourceBands = p.getSourceBands();
167            if (sourceBands != null &&
168                (sourceBands.length != 1 ||
169                 sourceBands[0] != 0)) {
170                throw new IllegalArgumentException("Cannot sub-band image!");
171            }
172            
173            // Get source region and subsampling factors
174            Rectangle sourceRegion = p.getSourceRegion();
175            if (sourceRegion != null) {
176                // Clip to actual image bounds
177                sourceRegion = sourceRegion.intersection(sourceBounds);
178                sourceBounds.setBounds(sourceRegion);
179            }
180            
181            // Adjust for subsampling offsets
182            int gridX = p.getSubsamplingXOffset();
183            int gridY = p.getSubsamplingYOffset();
184            sourceBounds.x += gridX;
185            sourceBounds.y += gridY;
186            sourceBounds.width -= gridX;
187            sourceBounds.height -= gridY;
188            
189            // Get subsampling factors
190            periodX = p.getSourceXSubsampling();
191            periodY = p.getSourceYSubsampling();
192        }
193        
194        // Compute output dimensions
195        destSize.setSize((sourceBounds.width + periodX - 1)/periodX,
196                         (sourceBounds.height + periodY - 1)/periodY);
197        if (destSize.width <= 0 || destSize.height <= 0) {
198            throw new IllegalArgumentException("Empty source region!");
199        }
200    }
201
202    /**
203     * Create a color table from the image ColorModel and SampleModel.
204     */
205    private static byte[] createColorTable(ColorModel colorModel,
206                                           SampleModel sampleModel)
207    {
208        byte[] colorTable;
209        if (colorModel instanceof IndexColorModel) {
210            IndexColorModel icm = (IndexColorModel)colorModel;
211            int mapSize = icm.getMapSize();
212
213            /**
214             * The GIF image format assumes that size of image palette
215             * is power of two. We will use closest larger power of two 
216             * as size of color table.
217             */
218            int ctSize = getGifPaletteSize(mapSize);
219            
220            byte[] reds = new byte[ctSize];
221            byte[] greens = new byte[ctSize];
222            byte[] blues = new byte[ctSize];
223            icm.getReds(reds);
224            icm.getGreens(greens);
225            icm.getBlues(blues);
226
227            /**
228             * fill tail of color component arrays by replica of first color
229             * in order to avoid appearance of extra colors in the color table
230             */
231            for (int i = mapSize; i < ctSize; i++) {
232                reds[i] = reds[0];
233                greens[i] = greens[0];
234                blues[i] = blues[0];
235            }
236
237            colorTable = new byte[3*ctSize];
238            int idx = 0;
239            for (int i = 0; i < ctSize; i++) {
240                colorTable[idx++] = reds[i];
241                colorTable[idx++] = greens[i];
242                colorTable[idx++] = blues[i];
243            }
244        } else if (sampleModel.getNumBands() == 1) {
245            // create gray-scaled color table for single-banded images
246            int numBits = sampleModel.getSampleSize()[0];
247            if (numBits > 8) {
248                numBits = 8;
249            }
250            int colorTableLength = 3*(1 << numBits);
251            colorTable = new byte[colorTableLength];
252            for (int i = 0; i < colorTableLength; i++) {
253                colorTable[i] = (byte)(i/3);
254            }
255        } else {
256            // We do not have enough information here 
257            // to create well-fit color table for RGB image.
258            colorTable = null;
259        }
260        
261        return colorTable;
262    }
263    
264    /**
265     * According do GIF specification size of clor table (palette here) 
266     * must be in range from 2 to 256 and must be power of 2.
267     */
268    private static int getGifPaletteSize(int x) {
269        if (x <= 2) {
270            return 2;
271        }
272        x = x - 1;
273        x = x | (x >> 1);
274        x = x | (x >> 2);
275        x = x | (x >> 4);
276        x = x | (x >> 8);
277        x = x | (x >> 16);
278        return x + 1;
279    }
280
281
282                
283    public GIFImageWriter(GIFImageWriterSpi originatingProvider) {
284        super(originatingProvider);
285        if (DEBUG) {
286            System.err.println("GIF Writer is created");
287        }
288    }
289    
290    public boolean canWriteSequence() {
291        return true;
292    }
293    
294    /**
295     * Merges <code>inData</code> into <code>outData</code>. The supplied
296     * metadata format name is attempted first and failing that the standard
297     * metadata format name is attempted.
298     */
299    private void convertMetadata(String metadataFormatName,
300                                 IIOMetadata inData,
301                                 IIOMetadata outData) {
302        String formatName = null;
303        
304        String nativeFormatName = inData.getNativeMetadataFormatName();
305        if (nativeFormatName != null &&
306            nativeFormatName.equals(metadataFormatName)) {
307            formatName = metadataFormatName;
308        } else {
309            String[] extraFormatNames = inData.getExtraMetadataFormatNames();
310            
311            if (extraFormatNames != null) {
312                for (int i = 0; i < extraFormatNames.length; i++) {
313                    if (extraFormatNames[i].equals(metadataFormatName)) {
314                        formatName = metadataFormatName;
315                        break;
316                    }
317                }
318            }
319        }
320        
321        if (formatName == null &&
322            inData.isStandardMetadataFormatSupported()) {
323            formatName = STANDARD_METADATA_NAME;
324        }
325        
326        if (formatName != null) {
327            try {
328                Node root = inData.getAsTree(formatName);
329                outData.mergeTree(formatName, root);
330            } catch(IIOInvalidTreeException e) {
331                // ignore
332            }
333        }
334    }
335    
336    /**
337     * Creates a default stream metadata object and merges in the
338     * supplied metadata.
339     */
340    public IIOMetadata convertStreamMetadata(IIOMetadata inData,
341                                             ImageWriteParam param) {
342        if (inData == null) {
343            throw new IllegalArgumentException("inData == null!");
344        }
345        
346        IIOMetadata sm = getDefaultStreamMetadata(param);
347        
348        convertMetadata(STREAM_METADATA_NAME, inData, sm);
349        
350        return sm;
351    }
352    
353    /**
354     * Creates a default image metadata object and merges in the
355     * supplied metadata.
356     */
357    public IIOMetadata convertImageMetadata(IIOMetadata inData,
358                                            ImageTypeSpecifier imageType,
359                                            ImageWriteParam param) {
360        if (inData == null) {
361            throw new IllegalArgumentException("inData == null!");
362        }
363        if (imageType == null) {
364            throw new IllegalArgumentException("imageType == null!");
365        }
366        
367        GIFWritableImageMetadata im =
368            (GIFWritableImageMetadata)getDefaultImageMetadata(imageType,
369                                                              param);
370        
371        // Save interlace flag state.
372        
373        boolean isProgressive = im.interlaceFlag;
374        
375        convertMetadata(IMAGE_METADATA_NAME, inData, im);
376        
377        // Undo change to interlace flag if not MODE_COPY_FROM_METADATA.
378        
379        if (param != null && param.canWriteProgressive() &&
380            param.getProgressiveMode() != param.MODE_COPY_FROM_METADATA) {
381            im.interlaceFlag = isProgressive;
382        }
383        
384        return im;
385    }
386    
387    public void endWriteSequence() throws IOException {
388        if (stream == null) {
389            throw new IllegalStateException("output == null!");
390        }
391        if (!isWritingSequence) {
392            throw new IllegalStateException("prepareWriteSequence() was not invoked!");
393        }
394        writeTrailer();
395        resetLocal();      
396    }
397    
398    public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
399                                               ImageWriteParam param) {
400        GIFWritableImageMetadata imageMetadata =
401            new GIFWritableImageMetadata();
402        
403        // Image dimensions
404        
405        SampleModel sampleModel = imageType.getSampleModel();
406        
407        Rectangle sourceBounds = new Rectangle(sampleModel.getWidth(),
408                                               sampleModel.getHeight());
409        Dimension destSize = new Dimension();
410        computeRegions(sourceBounds, destSize, param);
411        
412        imageMetadata.imageWidth = destSize.width;
413        imageMetadata.imageHeight = destSize.height;
414        
415        // Interlacing
416
417        if (param != null && param.canWriteProgressive() &&
418            param.getProgressiveMode() == ImageWriteParam.MODE_DISABLED) {
419            imageMetadata.interlaceFlag = false;
420        } else {
421            imageMetadata.interlaceFlag = true;
422        }
423        
424        // Local color table
425
426        ColorModel colorModel = imageType.getColorModel();
427        
428        imageMetadata.localColorTable =
429            createColorTable(colorModel, sampleModel);
430        
431        // Transparency
432        
433        if (colorModel instanceof IndexColorModel) {
434            int transparentIndex =
435                ((IndexColorModel)colorModel).getTransparentPixel();
436            if (transparentIndex != -1) {
437                imageMetadata.transparentColorFlag = true;
438                imageMetadata.transparentColorIndex = transparentIndex;
439            }
440        }
441        
442        return imageMetadata;
443    }
444    
445    public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
446        GIFWritableStreamMetadata streamMetadata =
447            new GIFWritableStreamMetadata();
448        streamMetadata.version = "89a";
449        return streamMetadata;
450    }
451    
452    public ImageWriteParam getDefaultWriteParam() {
453        return new GIFImageWriteParam(getLocale());
454    }
455    
456    public void prepareWriteSequence(IIOMetadata streamMetadata)
457      throws IOException {
458
459        if (stream == null) {
460            throw new IllegalStateException("Output is not set.");
461        }
462        
463        resetLocal();
464
465        // Save the possibly converted stream metadata as an instance variable.
466        if (streamMetadata == null) {
467            this.theStreamMetadata =
468                (GIFWritableStreamMetadata)getDefaultStreamMetadata(null);
469        } else {
470            this.theStreamMetadata = new GIFWritableStreamMetadata();
471            convertMetadata(STREAM_METADATA_NAME, streamMetadata,
472                            theStreamMetadata);
473        }
474
475        this.isWritingSequence = true;
476    }
477
478    public void reset() {
479        super.reset();
480        resetLocal();
481    }
482    
483    /**
484     * Resets locally defined instance variables.
485     */
486    private void resetLocal() {
487        this.isWritingSequence = false;
488        this.wroteSequenceHeader = false;
489        this.theStreamMetadata = null;
490        this.imageIndex = 0;
491    }
492
493    public void setOutput(Object output) {
494        super.setOutput(output);
495        if (output != null) {
496            if (!(output instanceof ImageOutputStream)) {
497                throw new 
498                    IllegalArgumentException("output is not an ImageOutputStream");
499            }
500            this.stream = (ImageOutputStream)output;
501            this.stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
502        } else {
503            this.stream = null;
504        }
505    }
506
507    public void write(IIOMetadata sm,
508                      IIOImage iioimage,
509                      ImageWriteParam p) throws IOException {
510        if (stream == null) {
511            throw new IllegalStateException("output == null!");
512        }
513        if (iioimage == null) {
514            throw new IllegalArgumentException("iioimage == null!");
515        }
516        if (iioimage.hasRaster()) {
517            throw new UnsupportedOperationException("canWriteRasters() == false!");
518        }
519        
520        resetLocal();
521        
522        GIFWritableStreamMetadata streamMetadata;
523        if (sm == null) {
524            streamMetadata =
525                (GIFWritableStreamMetadata)getDefaultStreamMetadata(p);
526        } else {
527            streamMetadata =
528                (GIFWritableStreamMetadata)convertStreamMetadata(sm, p);
529        }
530        
531        write(true, true, streamMetadata, iioimage, p);
532    }
533    
534    public void writeToSequence(IIOImage image, ImageWriteParam param)
535      throws IOException {
536        if (stream == null) {
537            throw new IllegalStateException("output == null!");
538        }
539        if (image == null) {
540            throw new IllegalArgumentException("image == null!");
541        }
542        if (image.hasRaster()) {
543            throw new UnsupportedOperationException("canWriteRasters() == false!");
544        }
545        if (!isWritingSequence) {
546            throw new IllegalStateException("prepareWriteSequence() was not invoked!");
547        }
548        
549        write(!wroteSequenceHeader, false, theStreamMetadata,
550              image, param);
551        
552        if (!wroteSequenceHeader) {
553            wroteSequenceHeader = true;
554        }
555        
556        this.imageIndex++;
557    }
558    
559    
560    private boolean needToCreateIndex(RenderedImage image) {
561        
562        SampleModel sampleModel = image.getSampleModel();
563        ColorModel colorModel = image.getColorModel();
564        
565        return sampleModel.getNumBands() != 1 ||
566            sampleModel.getSampleSize()[0] > 8 ||
567            colorModel.getComponentSize()[0] > 8;
568    }
569    
570    /**
571     * Writes any extension blocks, the Image Descriptor, the image data,
572     * and optionally the header (Signature and Logical Screen Descriptor)
573     * and trailer (Block Terminator).
574     *
575     * @param writeHeader Whether to write the header.
576     * @param writeTrailer Whether to write the trailer.
577     * @param sm The stream metadata or <code>null</code> if
578     * <code>writeHeader</code> is <code>false</code>.
579     * @param iioimage The image and image metadata.
580     * @param p The write parameters.
581     *
582     * @throws IllegalArgumentException if the number of bands is not 1.
583     * @throws IllegalArgumentException if the number of bits per sample is
584     * greater than 8.
585     * @throws IllegalArgumentException if the color component size is
586     * greater than 8.
587     * @throws IllegalArgumentException if <code>writeHeader</code> is
588     * <code>true</code> and <code>sm</code> is <code>null</code>.
589     * @throws IllegalArgumentException if <code>writeHeader</code> is
590     * <code>false</code> and a sequence is not being written.
591     */
592    private void write(boolean writeHeader,
593                       boolean writeTrailer,
594                       IIOMetadata sm,
595                       IIOImage iioimage,
596                       ImageWriteParam p) throws IOException {
597        clearAbortRequest();
598
599        RenderedImage image = iioimage.getRenderedImage();
600        
601        // Check for ability to encode image.
602        if (needToCreateIndex(image)) {
603            image = PaletteBuilder.createIndexedImage(image);
604            iioimage.setRenderedImage(image);
605        }
606
607        ColorModel colorModel = image.getColorModel();
608        SampleModel sampleModel = image.getSampleModel();
609        
610        // Determine source region and destination dimensions.
611        Rectangle sourceBounds = new Rectangle(image.getMinX(),
612                                               image.getMinY(),
613                                               image.getWidth(),
614                                               image.getHeight());
615        Dimension destSize = new Dimension();
616        computeRegions(sourceBounds, destSize, p);
617        
618        // Convert any provided image metadata.
619        GIFWritableImageMetadata imageMetadata = null;
620        if (iioimage.getMetadata() != null) {
621            imageMetadata = new GIFWritableImageMetadata();
622            convertMetadata(IMAGE_METADATA_NAME, iioimage.getMetadata(),
623                            imageMetadata);
624            // Converted rgb image can use palette different from global.
625            // In order to avoid color artefacts we want to be sure we use
626            // appropriate palette. For this we initialize local color table
627            // from current color and sample models.
628            // At this point we can guarantee that local color table can be
629            // build because image was already converted to indexed or
630            // gray-scale representations
631            if (imageMetadata.localColorTable == null) {
632                imageMetadata.localColorTable =
633                    createColorTable(colorModel, sampleModel);
634                
635                // in case of indexed image we should take care of 
636                // transparent pixels
637                if (colorModel instanceof IndexColorModel) {
638                    IndexColorModel icm =
639                        (IndexColorModel)colorModel;
640                    int index = icm.getTransparentPixel();
641                    imageMetadata.transparentColorFlag = (index != -1);
642                    if (imageMetadata.transparentColorFlag) {
643                        imageMetadata.transparentColorIndex = index;
644                    }
645                    /* NB: transparentColorFlag might have not beed reset for
646                       greyscale images but explicitly reseting it here
647                       is potentially not right thing to do until we have way
648                       to find whether current value was explicitly set by
649                       the user.
650                    */
651                }
652            }
653        }
654        
655        // Global color table values.
656        byte[] globalColorTable = null;
657        
658        // Write the header (Signature+Logical Screen Descriptor+
659        // Global Color Table).
660        if (writeHeader) {
661            if (sm == null) {
662                throw new IllegalArgumentException("Cannot write null header!");
663            }
664            
665            GIFWritableStreamMetadata streamMetadata =
666                (GIFWritableStreamMetadata)sm;
667            
668            // Set the version if not set.
669            if (streamMetadata.version == null) {
670                streamMetadata.version = "89a";
671            }
672            
673            // Set the Logical Screen Desriptor if not set.
674            if (streamMetadata.logicalScreenWidth == 
675                GIFMetadata.UNDEFINED_INTEGER_VALUE) 
676            {
677                streamMetadata.logicalScreenWidth = destSize.width;
678            }
679
680            if (streamMetadata.logicalScreenHeight == 
681                GIFMetadata.UNDEFINED_INTEGER_VALUE)
682            {
683                streamMetadata.logicalScreenHeight = destSize.height;
684            }
685
686            if (streamMetadata.colorResolution ==
687                GIFMetadata.UNDEFINED_INTEGER_VALUE)
688            {
689                streamMetadata.colorResolution = colorModel != null ?
690                    colorModel.getComponentSize()[0] :
691                    sampleModel.getSampleSize()[0];
692            }
693            
694            // Set the Global Color Table if not set, i.e., if not
695            // provided in the stream metadata.
696            if (streamMetadata.globalColorTable == null) {
697                if (isWritingSequence && imageMetadata != null &&
698                    imageMetadata.localColorTable != null) {
699                    // Writing a sequence and a local color table was
700                    // provided in the metadata of the first image: use it.
701                    streamMetadata.globalColorTable =
702                        imageMetadata.localColorTable;
703                } else if (imageMetadata == null ||
704                           imageMetadata.localColorTable == null) {
705                    // Create a color table.
706                    streamMetadata.globalColorTable =
707                        createColorTable(colorModel, sampleModel);
708                }
709            }
710
711            // Set the Global Color Table. At this point it should be
712            // A) the global color table provided in stream metadata, if any;
713            // B) the local color table of the image metadata, if any, if
714            //    writing a sequence;
715            // C) a table created on the basis of the first image ColorModel
716            //    and SampleModel if no local color table is available; or
717            // D) null if none of the foregoing conditions obtain (which
718            //    should only be if a sequence is not being written and
719            //    a local color table is provided in image metadata).
720            globalColorTable = streamMetadata.globalColorTable;
721            
722            // Write the header.
723            int bitsPerPixel;
724            if (globalColorTable != null) {
725                bitsPerPixel = getNumBits(globalColorTable.length/3);
726            } else if (imageMetadata != null &&
727                       imageMetadata.localColorTable != null) {
728                bitsPerPixel =
729                    getNumBits(imageMetadata.localColorTable.length/3);
730            } else {
731                bitsPerPixel = sampleModel.getSampleSize(0);
732            }
733            writeHeader(streamMetadata, bitsPerPixel);
734        } else if (isWritingSequence) {
735            globalColorTable = theStreamMetadata.globalColorTable;
736        } else {
737            throw new IllegalArgumentException("Must write header for single image!");
738        }
739        
740        // Write extension blocks, Image Descriptor, and image data.
741        writeImage(iioimage.getRenderedImage(), imageMetadata, p,
742                   globalColorTable, sourceBounds, destSize);
743        
744        // Write the trailer.
745        if (writeTrailer) {
746            writeTrailer();
747        }
748    }
749    
750    /**
751     * Writes any extension blocks, the Image Descriptor, and the image data
752     *
753     * @param image The image.
754     * @param imageMetadata The Image metadata.
755     * @param param The write parameters.
756     * @param globalColorTable The Global Color Table.
757     * @param sourceBounds The source region.
758     * @param destSize The destination dimensions.
759     */
760    private void writeImage(RenderedImage image,
761                            GIFWritableImageMetadata imageMetadata,
762                            ImageWriteParam param, byte[] globalColorTable,
763                            Rectangle sourceBounds, Dimension destSize)
764      throws IOException {
765        ColorModel colorModel = image.getColorModel();
766        SampleModel sampleModel = image.getSampleModel();
767        
768        boolean writeGraphicsControlExtension;
769        if (imageMetadata == null) {
770            // Create default metadata.
771            imageMetadata = (GIFWritableImageMetadata)getDefaultImageMetadata(
772                new ImageTypeSpecifier(image), param);
773            
774            // Set GraphicControlExtension flag only if there is
775            // transparency.
776            writeGraphicsControlExtension = imageMetadata.transparentColorFlag;
777        } else {
778            // Check for GraphicControlExtension element.
779            NodeList list = null;
780            try {
781                IIOMetadataNode root = (IIOMetadataNode)
782                    imageMetadata.getAsTree(IMAGE_METADATA_NAME);
783                list = root.getElementsByTagName("GraphicControlExtension");
784            } catch(IllegalArgumentException iae) {
785                // Should never happen.
786            }
787            
788            // Set GraphicControlExtension flag if element present.
789            writeGraphicsControlExtension =
790                list != null && list.getLength() > 0;
791            
792            // If progressive mode is not MODE_COPY_FROM_METADATA, ensure
793            // the interlacing is set per the ImageWriteParam mode setting.
794            if (param != null && param.canWriteProgressive()) {
795                if (param.getProgressiveMode() ==
796                    ImageWriteParam.MODE_DISABLED) {
797                    imageMetadata.interlaceFlag = false;
798                } else if (param.getProgressiveMode() ==
799                           ImageWriteParam.MODE_DEFAULT) {
800                    imageMetadata.interlaceFlag = true;
801                }
802            }
803        }
804        
805        // Unset local color table if equal to global color table.
806        if (Arrays.equals(globalColorTable, imageMetadata.localColorTable)) {
807            imageMetadata.localColorTable = null;
808        }
809      
810        // Override dimensions
811        imageMetadata.imageWidth = destSize.width;
812        imageMetadata.imageHeight = destSize.height;
813        
814        // Write Graphics Control Extension.
815        if (writeGraphicsControlExtension) {
816            writeGraphicControlExtension(imageMetadata);
817        }
818        
819        // Write extension blocks.
820        writePlainTextExtension(imageMetadata);
821        writeApplicationExtension(imageMetadata);
822        writeCommentExtension(imageMetadata);
823        
824        // Write Image Descriptor
825        int bitsPerPixel =
826            getNumBits(imageMetadata.localColorTable == null ?
827                       (globalColorTable == null ?
828                        sampleModel.getSampleSize(0) :
829                        globalColorTable.length/3) :
830                       imageMetadata.localColorTable.length/3);
831        writeImageDescriptor(imageMetadata, bitsPerPixel);
832        
833        // Write image data
834        writeRasterData(image, sourceBounds, destSize,
835                        param, imageMetadata.interlaceFlag);
836    }
837    
838    private void writeRows(RenderedImage image, LZWCompressor compressor,
839                           int sx, int sdx, int sy, int sdy, int sw,
840                           int dy, int ddy, int dw, int dh,
841                           int numRowsWritten, int progressReportRowPeriod)
842      throws IOException {
843        if (DEBUG) System.out.println("Writing unoptimized");
844        
845        int[] sbuf = new int[sw];
846        byte[] dbuf = new byte[dw];
847        
848        Raster raster =
849            image.getNumXTiles() == 1 && image.getNumYTiles() == 1 ?
850            image.getTile(0, 0) : image.getData();
851        for (int y = dy; y < dh; y += ddy) {
852            if (numRowsWritten % progressReportRowPeriod == 0) {
853                if (abortRequested()) {
854                    processWriteAborted();
855                    return;
856                }
857                processImageProgress((numRowsWritten*100.0F)/dh);
858            }
859            
860            raster.getSamples(sx, sy, sw, 1, 0, sbuf);
861            for (int i = 0, j = 0; i < dw; i++, j += sdx) {
862                dbuf[i] = (byte)sbuf[j];
863            }
864            compressor.compress(dbuf, 0, dw);
865            numRowsWritten++;
866            sy += sdy;
867        }
868    }
869
870    private void writeRowsOpt(byte[] data, int offset, int lineStride,
871                              LZWCompressor compressor,
872                              int dy, int ddy, int dw, int dh,
873                              int numRowsWritten, int progressReportRowPeriod)
874      throws IOException {
875        if (DEBUG) System.out.println("Writing optimized");
876        
877        offset += dy*lineStride;
878        lineStride *= ddy;
879        for (int y = dy; y < dh; y += ddy) {
880            if (numRowsWritten % progressReportRowPeriod == 0) {
881                if (abortRequested()) {
882                    processWriteAborted();
883                    return;
884                }
885                processImageProgress((numRowsWritten*100.0F)/dh);
886            }
887            
888            compressor.compress(data, offset, dw);
889            numRowsWritten++;
890            offset += lineStride;
891        }
892    }
893
894    private void writeRasterData(RenderedImage image,
895                                 Rectangle sourceBounds,
896                                 Dimension destSize,
897                                 ImageWriteParam param,
898                                 boolean interlaceFlag) throws IOException {
899        
900        int sourceXOffset = sourceBounds.x;
901        int sourceYOffset = sourceBounds.y;
902        int sourceWidth = sourceBounds.width;
903        int sourceHeight = sourceBounds.height;
904        
905        int destWidth = destSize.width;
906        int destHeight = destSize.height;
907        
908        int periodX;
909        int periodY;
910        if (param == null) {
911            periodX = 1;
912            periodY = 1;
913        } else {
914            periodX = param.getSourceXSubsampling();
915            periodY = param.getSourceYSubsampling();
916        }
917        
918        SampleModel sampleModel = image.getSampleModel();
919        int bitsPerPixel = sampleModel.getSampleSize()[0];
920        
921        int initCodeSize = bitsPerPixel;
922        if (initCodeSize == 1) {
923            initCodeSize++;
924        }
925        stream.write(initCodeSize);
926        
927        LZWCompressor compressor =
928            new LZWCompressor(stream, initCodeSize, false);
929        
930        boolean isOptimizedCase =
931            periodX == 1 && periodY == 1 &&
932            sampleModel instanceof ComponentSampleModel &&
933            image.getNumXTiles() == 1 && image.getNumYTiles() == 1 &&
934            image.getTile(0, 0).getDataBuffer() instanceof DataBufferByte;
935        
936        int numRowsWritten = 0;
937        
938        int progressReportRowPeriod = Math.max(destHeight/20, 1);
939        
940        processImageStarted(imageIndex);
941        
942        if (interlaceFlag) {
943            if (DEBUG) System.out.println("Writing interlaced");
944            
945            if (isOptimizedCase) {
946                Raster tile = image.getTile(0, 0);
947                byte[] data = ((DataBufferByte)tile.getDataBuffer()).getData();
948                ComponentSampleModel csm =
949                    (ComponentSampleModel)tile.getSampleModel();
950                int offset = csm.getOffset(sourceXOffset -
951                                           tile.getSampleModelTranslateX(),
952                                           sourceYOffset -
953                                           tile.getSampleModelTranslateY(),
954                                           0);
955                int lineStride = csm.getScanlineStride();
956                
957                writeRowsOpt(data, offset, lineStride, compressor,
958                             0, 8, destWidth, destHeight,
959                             numRowsWritten, progressReportRowPeriod);
960                
961                if (abortRequested()) {
962                    return;
963                }
964                
965                numRowsWritten += destHeight/8;
966                
967                writeRowsOpt(data, offset, lineStride, compressor,
968                             4, 8, destWidth, destHeight,
969                             numRowsWritten, progressReportRowPeriod);
970                
971                if (abortRequested()) {
972                    return;
973                }
974                
975                numRowsWritten += (destHeight - 4)/8;
976                
977                writeRowsOpt(data, offset, lineStride, compressor,
978                             2, 4, destWidth, destHeight,
979                             numRowsWritten, progressReportRowPeriod);
980                
981                if (abortRequested()) {
982                    return;
983                }
984                
985                numRowsWritten += (destHeight - 2)/4;
986                
987                writeRowsOpt(data, offset, lineStride, compressor,
988                             1, 2, destWidth, destHeight,
989                             numRowsWritten, progressReportRowPeriod);
990            } else {
991                writeRows(image, compressor,
992                          sourceXOffset, periodX,
993                          sourceYOffset, 8*periodY,
994                          sourceWidth,
995                          0, 8, destWidth, destHeight,
996                          numRowsWritten, progressReportRowPeriod);
997                
998                if (abortRequested()) {
999                    return;
1000                }
1001                
1002                numRowsWritten += destHeight/8;
1003                
1004                writeRows(image, compressor, sourceXOffset, periodX,
1005                          sourceYOffset + 4*periodY, 8*periodY,
1006                          sourceWidth,
1007                          4, 8, destWidth, destHeight,
1008                          numRowsWritten, progressReportRowPeriod);
1009                
1010                if (abortRequested()) {
1011                    return;
1012                }
1013                
1014                numRowsWritten += (destHeight - 4)/8;
1015                
1016                writeRows(image, compressor, sourceXOffset, periodX,
1017                          sourceYOffset + 2*periodY, 4*periodY,
1018                          sourceWidth,
1019                          2, 4, destWidth, destHeight,
1020                          numRowsWritten, progressReportRowPeriod);
1021                
1022                if (abortRequested()) {
1023                    return;
1024                }
1025                
1026                numRowsWritten += (destHeight - 2)/4;
1027                
1028                writeRows(image, compressor, sourceXOffset, periodX,
1029                          sourceYOffset + periodY, 2*periodY,
1030                          sourceWidth,
1031                          1, 2, destWidth, destHeight,
1032                          numRowsWritten, progressReportRowPeriod);
1033            }
1034        } else {
1035            if (DEBUG) System.out.println("Writing non-interlaced");
1036            
1037            if (isOptimizedCase) {
1038                Raster tile = image.getTile(0, 0);
1039                byte[] data = ((DataBufferByte)tile.getDataBuffer()).getData();
1040                ComponentSampleModel csm =
1041                    (ComponentSampleModel)tile.getSampleModel();
1042                int offset = csm.getOffset(sourceXOffset -
1043                                           tile.getSampleModelTranslateX(),
1044                                           sourceYOffset -
1045                                           tile.getSampleModelTranslateY(),
1046                                           0);
1047                int lineStride = csm.getScanlineStride();
1048                
1049                writeRowsOpt(data, offset, lineStride, compressor,
1050                             0, 1, destWidth, destHeight,
1051                             numRowsWritten, progressReportRowPeriod);
1052            } else {
1053                writeRows(image, compressor,
1054                          sourceXOffset, periodX,
1055                          sourceYOffset, periodY,
1056                          sourceWidth,
1057                          0, 1, destWidth, destHeight,
1058                          numRowsWritten, progressReportRowPeriod);
1059            }
1060        }
1061        
1062        if (abortRequested()) {
1063            return;
1064        }
1065        
1066        processImageProgress(100.0F);
1067
1068        compressor.flush();
1069        
1070        stream.write(0x00);
1071        
1072        processImageComplete();
1073    }
1074
1075    private void writeHeader(String version,
1076                             int logicalScreenWidth,
1077                             int logicalScreenHeight,
1078                             int colorResolution,
1079                             int pixelAspectRatio,
1080                             int backgroundColorIndex,
1081                             boolean sortFlag,
1082                             int bitsPerPixel,
1083                             byte[] globalColorTable) throws IOException {
1084        try {
1085            // Signature
1086            stream.writeBytes("GIF"+version);
1087            
1088            // Screen Descriptor
1089            // Width
1090            stream.writeShort((short)logicalScreenWidth);
1091            
1092            // Height
1093            stream.writeShort((short)logicalScreenHeight);
1094            
1095            // Global Color Table
1096            // Packed fields
1097            int packedFields = globalColorTable != null ? 0x80 : 0x00;
1098            packedFields |= ((colorResolution - 1) & 0x7) << 4;
1099            if (sortFlag) {
1100                packedFields |= 0x8;
1101            }
1102            packedFields |= (bitsPerPixel - 1);
1103            stream.write(packedFields);
1104            
1105            // Background color index
1106            stream.write(backgroundColorIndex);
1107
1108            // Pixel aspect ratio
1109            stream.write(pixelAspectRatio);
1110            
1111            // Global Color Table
1112            if (globalColorTable != null) {
1113                stream.write(globalColorTable);
1114            }
1115        } catch (IOException e) {
1116            throw new IIOException("I/O error writing header!", e);
1117        }
1118    }
1119    
1120    private void writeHeader(IIOMetadata streamMetadata, int bitsPerPixel)
1121      throws IOException {
1122
1123        GIFWritableStreamMetadata sm;
1124        if (streamMetadata instanceof GIFWritableStreamMetadata) {
1125            sm = (GIFWritableStreamMetadata)streamMetadata;
1126        } else {
1127            sm = new GIFWritableStreamMetadata();
1128            Node root =
1129                streamMetadata.getAsTree(STREAM_METADATA_NAME);
1130            sm.setFromTree(STREAM_METADATA_NAME, root);
1131        }
1132        
1133        writeHeader(sm.version,
1134                    sm.logicalScreenWidth,
1135                    sm.logicalScreenHeight,
1136                    sm.colorResolution,
1137                    sm.pixelAspectRatio,
1138                    sm.backgroundColorIndex,
1139                    sm.sortFlag,
1140                    bitsPerPixel,
1141                    sm.globalColorTable);
1142    }
1143    
1144    private void writeGraphicControlExtension(int disposalMethod,
1145                                              boolean userInputFlag,
1146                                              boolean transparentColorFlag,
1147                                              int delayTime,
1148                                              int transparentColorIndex)
1149      throws IOException {
1150        try {
1151            stream.write(0x21);
1152            stream.write(0xf9);
1153            
1154            stream.write(4);
1155            
1156            int packedFields = (disposalMethod & 0x3) << 2;
1157            if (userInputFlag) {
1158                packedFields |= 0x2;
1159            }
1160            if (transparentColorFlag) {
1161                packedFields |= 0x1;
1162            }
1163            stream.write(packedFields);
1164            
1165            stream.writeShort((short)delayTime);
1166            
1167            stream.write(transparentColorIndex);
1168            stream.write(0x00);
1169        } catch (IOException e) {
1170            throw new IIOException("I/O error writing Graphic Control Extension!", e);
1171        }
1172    }
1173    
1174    private void writeGraphicControlExtension(GIFWritableImageMetadata im)
1175      throws IOException {
1176        writeGraphicControlExtension(im.disposalMethod,
1177                                     im.userInputFlag,
1178                                     im.transparentColorFlag,
1179                                     im.delayTime,
1180                                     im.transparentColorIndex);
1181    }
1182    
1183    private void writeBlocks(byte[] data) throws IOException {
1184        if (data != null && data.length > 0) {
1185            int offset = 0;
1186            while (offset < data.length) {
1187                int len = Math.min(data.length - offset, 255);
1188                stream.write(len);
1189                stream.write(data, offset, len);
1190                offset += len;
1191            }
1192        }
1193    }
1194    
1195    private void writePlainTextExtension(GIFWritableImageMetadata im)
1196      throws IOException {
1197        if (im.hasPlainTextExtension) {
1198            try {
1199                stream.write(0x21);
1200                stream.write(0x1);
1201                
1202                stream.write(12);
1203                
1204                stream.writeShort(im.textGridLeft);
1205                stream.writeShort(im.textGridTop);
1206                stream.writeShort(im.textGridWidth);
1207                stream.writeShort(im.textGridHeight);
1208                stream.write(im.characterCellWidth);
1209                stream.write(im.characterCellHeight);
1210                stream.write(im.textForegroundColor);
1211                stream.write(im.textBackgroundColor);
1212                
1213                writeBlocks(im.text);
1214                
1215                stream.write(0x00);
1216            } catch (IOException e) {
1217                throw new IIOException("I/O error writing Plain Text Extension!", e);
1218            }
1219        }
1220    }
1221    
1222    private void writeApplicationExtension(GIFWritableImageMetadata im)
1223      throws IOException {
1224        if (im.applicationIDs != null) {
1225            Iterator iterIDs = im.applicationIDs.iterator();
1226            Iterator iterCodes = im.authenticationCodes.iterator();
1227            Iterator iterData = im.applicationData.iterator();
1228            
1229            while (iterIDs.hasNext()) {
1230                try {
1231                    stream.write(0x21);
1232                    stream.write(0xff);
1233                    
1234                    stream.write(11);
1235                    stream.write((byte[])iterIDs.next(), 0, 8);
1236                    stream.write((byte[])iterCodes.next(), 0, 3);
1237                    
1238                    writeBlocks((byte[])iterData.next());
1239                    
1240                    stream.write(0x00);
1241                } catch (IOException e) {
1242                    throw new IIOException("I/O error writing Application Extension!", e);
1243                }
1244            }
1245        }
1246    }
1247
1248    private void writeCommentExtension(GIFWritableImageMetadata im)
1249      throws IOException {
1250        if (im.comments != null) {
1251            try {
1252                Iterator iter = im.comments.iterator();
1253                while (iter.hasNext()) {
1254                    stream.write(0x21);
1255                    stream.write(0xfe);
1256                    writeBlocks((byte[])iter.next());
1257                    stream.write(0x00);
1258                }
1259            } catch (IOException e) {
1260                throw new IIOException("I/O error writing Comment Extension!", e);
1261            }
1262        }
1263    }
1264    
1265    private void writeImageDescriptor(int imageLeftPosition,
1266                                      int imageTopPosition,
1267                                      int imageWidth,
1268                                      int imageHeight,
1269                                      boolean interlaceFlag,
1270                                      boolean sortFlag,
1271                                      int bitsPerPixel,
1272                                      byte[] localColorTable)
1273      throws IOException {
1274        
1275        try {
1276            stream.write(0x2c);
1277            
1278            stream.writeShort((short)imageLeftPosition);
1279            stream.writeShort((short)imageTopPosition);
1280            stream.writeShort((short)imageWidth);
1281            stream.writeShort((short)imageHeight);
1282            
1283            int packedFields = localColorTable != null ? 0x80 : 0x00;
1284            if (interlaceFlag) {
1285                packedFields |= 0x40;
1286            }
1287            if (sortFlag) {
1288                packedFields |= 0x8;
1289            }
1290            packedFields |= (bitsPerPixel - 1);
1291            stream.write(packedFields);
1292            
1293            if (localColorTable != null) {
1294                stream.write(localColorTable);
1295            }
1296        } catch (IOException e) {
1297            throw new IIOException("I/O error writing Image Descriptor!", e);
1298        }
1299    }
1300    
1301    private void writeImageDescriptor(GIFWritableImageMetadata imageMetadata,
1302                                      int bitsPerPixel)
1303      throws IOException {
1304        
1305        writeImageDescriptor(imageMetadata.imageLeftPosition,
1306                             imageMetadata.imageTopPosition,
1307                             imageMetadata.imageWidth,
1308                             imageMetadata.imageHeight,
1309                             imageMetadata.interlaceFlag,
1310                             imageMetadata.sortFlag,
1311                             bitsPerPixel,
1312                             imageMetadata.localColorTable);
1313    }
1314
1315    private void writeTrailer() throws IOException {
1316        stream.write(0x3b);
1317    }
1318}
1319
1320class GIFImageWriteParam extends ImageWriteParam {
1321    GIFImageWriteParam(Locale locale) {
1322        super(locale);
1323        this.canWriteCompressed = true;
1324        this.canWriteProgressive = true;
1325        this.compressionTypes = new String[] {"LZW", "lzw"};
1326        this.compressionType = compressionTypes[0];
1327    }
1328
1329    public void setCompressionMode(int mode) {
1330        if (mode == MODE_DISABLED) {
1331            throw new UnsupportedOperationException("MODE_DISABLED is not supported.");
1332        }
1333        super.setCompressionMode(mode);
1334    }
1335}