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