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