1 /*
   2  * Copyright (c) 2005, 2014, 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 
  26 package com.sun.imageio.plugins.gif;
  27 
  28 import java.awt.Dimension;
  29 import java.awt.Rectangle;
  30 import java.awt.image.ColorModel;
  31 import java.awt.image.ComponentSampleModel;
  32 import java.awt.image.DataBufferByte;
  33 import java.awt.image.IndexColorModel;
  34 import java.awt.image.Raster;
  35 import java.awt.image.RenderedImage;
  36 import java.awt.image.SampleModel;
  37 import java.awt.image.WritableRaster;
  38 import java.io.IOException;
  39 import java.nio.ByteOrder;
  40 import java.util.Arrays;
  41 import java.util.Iterator;
  42 import java.util.Locale;
  43 import javax.imageio.IIOException;
  44 import javax.imageio.IIOImage;
  45 import javax.imageio.ImageTypeSpecifier;
  46 import javax.imageio.ImageWriteParam;
  47 import javax.imageio.ImageWriter;
  48 import javax.imageio.spi.ImageWriterSpi;
  49 import javax.imageio.metadata.IIOInvalidTreeException;
  50 import javax.imageio.metadata.IIOMetadata;
  51 import javax.imageio.metadata.IIOMetadataFormatImpl;
  52 import javax.imageio.metadata.IIOMetadataNode;
  53 import javax.imageio.stream.ImageOutputStream;
  54 import org.w3c.dom.Node;
  55 import org.w3c.dom.NodeList;
  56 import com.sun.imageio.plugins.common.LZWCompressor;
  57 import com.sun.imageio.plugins.common.PaletteBuilder;
  58 import sun.awt.image.ByteComponentRaster;
  59 
  60 public 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</code> case to an <code>ImageOutputStream</code>.
  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</code> into <code>outData</code>. 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</code> if
 558      * <code>writeHeader</code> is <code>false</code>.
 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</code> is
 568      * <code>true</code> and <code>sm</code> is <code>null</code>.
 569      * @throws IllegalArgumentException if <code>writeHeader</code> is
 570      * <code>false</code> 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         clearAbortRequest();
 578 
 579         RenderedImage image = iioimage.getRenderedImage();
 580 
 581         // Check for ability to encode image.
 582         if (needToCreateIndex(image)) {
 583             image = PaletteBuilder.createIndexedImage(image);
 584             iioimage.setRenderedImage(image);
 585         }
 586 
 587         ColorModel colorModel = image.getColorModel();
 588         SampleModel sampleModel = image.getSampleModel();
 589 
 590         // Determine source region and destination dimensions.
 591         Rectangle sourceBounds = new Rectangle(image.getMinX(),
 592                                                image.getMinY(),
 593                                                image.getWidth(),
 594                                                image.getHeight());
 595         Dimension destSize = new Dimension();
 596         computeRegions(sourceBounds, destSize, p);
 597 
 598         // Convert any provided image metadata.
 599         GIFWritableImageMetadata imageMetadata = null;
 600         if (iioimage.getMetadata() != null) {
 601             imageMetadata = new GIFWritableImageMetadata();
 602             convertMetadata(IMAGE_METADATA_NAME, iioimage.getMetadata(),
 603                             imageMetadata);
 604             // Converted rgb image can use palette different from global.
 605             // In order to avoid color artefacts we want to be sure we use
 606             // appropriate palette. For this we initialize local color table
 607             // from current color and sample models.
 608             // At this point we can guarantee that local color table can be
 609             // build because image was already converted to indexed or
 610             // gray-scale representations
 611             if (imageMetadata.localColorTable == null) {
 612                 imageMetadata.localColorTable =
 613                     createColorTable(colorModel, sampleModel);
 614 
 615                 // in case of indexed image we should take care of
 616                 // transparent pixels
 617                 if (colorModel instanceof IndexColorModel) {
 618                     IndexColorModel icm =
 619                         (IndexColorModel)colorModel;
 620                     int index = icm.getTransparentPixel();
 621                     imageMetadata.transparentColorFlag = (index != -1);
 622                     if (imageMetadata.transparentColorFlag) {
 623                         imageMetadata.transparentColorIndex = index;
 624                     }
 625                     /* NB: transparentColorFlag might have not beed reset for
 626                        greyscale images but explicitly reseting it here
 627                        is potentially not right thing to do until we have way
 628                        to find whether current value was explicitly set by
 629                        the user.
 630                     */
 631                 }
 632             }
 633         }
 634 
 635         // Global color table values.
 636         byte[] globalColorTable = null;
 637 
 638         // Write the header (Signature+Logical Screen Descriptor+
 639         // Global Color Table).
 640         if (writeHeader) {
 641             if (sm == null) {
 642                 throw new IllegalArgumentException("Cannot write null header!");
 643             }
 644 
 645             GIFWritableStreamMetadata streamMetadata =
 646                 (GIFWritableStreamMetadata)sm;
 647 
 648             // Set the version if not set.
 649             if (streamMetadata.version == null) {
 650                 streamMetadata.version = "89a";
 651             }
 652 
 653             // Set the Logical Screen Desriptor if not set.
 654             if (streamMetadata.logicalScreenWidth ==
 655                 GIFMetadata.UNDEFINED_INTEGER_VALUE)
 656             {
 657                 streamMetadata.logicalScreenWidth = destSize.width;
 658             }
 659 
 660             if (streamMetadata.logicalScreenHeight ==
 661                 GIFMetadata.UNDEFINED_INTEGER_VALUE)
 662             {
 663                 streamMetadata.logicalScreenHeight = destSize.height;
 664             }
 665 
 666             if (streamMetadata.colorResolution ==
 667                 GIFMetadata.UNDEFINED_INTEGER_VALUE)
 668             {
 669                 streamMetadata.colorResolution = colorModel != null ?
 670                     colorModel.getComponentSize()[0] :
 671                     sampleModel.getSampleSize()[0];
 672             }
 673 
 674             // Set the Global Color Table if not set, i.e., if not
 675             // provided in the stream metadata.
 676             if (streamMetadata.globalColorTable == null) {
 677                 if (isWritingSequence && imageMetadata != null &&
 678                     imageMetadata.localColorTable != null) {
 679                     // Writing a sequence and a local color table was
 680                     // provided in the metadata of the first image: use it.
 681                     streamMetadata.globalColorTable =
 682                         imageMetadata.localColorTable;
 683                 } else if (imageMetadata == null ||
 684                            imageMetadata.localColorTable == null) {
 685                     // Create a color table.
 686                     streamMetadata.globalColorTable =
 687                         createColorTable(colorModel, sampleModel);
 688                 }
 689             }
 690 
 691             // Set the Global Color Table. At this point it should be
 692             // A) the global color table provided in stream metadata, if any;
 693             // B) the local color table of the image metadata, if any, if
 694             //    writing a sequence;
 695             // C) a table created on the basis of the first image ColorModel
 696             //    and SampleModel if no local color table is available; or
 697             // D) null if none of the foregoing conditions obtain (which
 698             //    should only be if a sequence is not being written and
 699             //    a local color table is provided in image metadata).
 700             globalColorTable = streamMetadata.globalColorTable;
 701 
 702             // Write the header.
 703             int bitsPerPixel;
 704             if (globalColorTable != null) {
 705                 bitsPerPixel = getNumBits(globalColorTable.length/3);
 706             } else if (imageMetadata != null &&
 707                        imageMetadata.localColorTable != null) {
 708                 bitsPerPixel =
 709                     getNumBits(imageMetadata.localColorTable.length/3);
 710             } else {
 711                 bitsPerPixel = sampleModel.getSampleSize(0);
 712             }
 713             writeHeader(streamMetadata, bitsPerPixel);
 714         } else if (isWritingSequence) {
 715             globalColorTable = theStreamMetadata.globalColorTable;
 716         } else {
 717             throw new IllegalArgumentException("Must write header for single image!");
 718         }
 719 
 720         // Write extension blocks, Image Descriptor, and image data.
 721         writeImage(iioimage.getRenderedImage(), imageMetadata, p,
 722                    globalColorTable, sourceBounds, destSize);
 723 
 724         // Write the trailer.
 725         if (writeTrailer) {
 726             writeTrailer();
 727         }
 728     }
 729 
 730     /**
 731      * Writes any extension blocks, the Image Descriptor, and the image data
 732      *
 733      * @param iioimage The image and 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                 if (abortRequested()) {
 833                     processWriteAborted();
 834                     return;
 835                 }
 836                 processImageProgress((numRowsWritten*100.0F)/dh);
 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                 if (abortRequested()) {
 861                     processWriteAborted();
 862                     return;
 863                 }
 864                 processImageProgress((numRowsWritten*100.0F)/dh);
 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         processImageStarted(imageIndex);
 928 
 929         if (interlaceFlag) {
 930             if (DEBUG) System.out.println("Writing interlaced");
 931 
 932             if (isOptimizedCase) {
 933                 ByteComponentRaster tile =
 934                     (ByteComponentRaster)image.getTile(0, 0);
 935                 byte[] data = ((DataBufferByte)tile.getDataBuffer()).getData();
 936                 ComponentSampleModel csm =
 937                     (ComponentSampleModel)tile.getSampleModel();
 938                 int offset = csm.getOffset(sourceXOffset, sourceYOffset, 0);
 939                 // take into account the raster data offset
 940                 offset += tile.getDataOffset(0);
 941                 int lineStride = csm.getScanlineStride();
 942 
 943                 writeRowsOpt(data, offset, lineStride, compressor,
 944                              0, 8, destWidth, destHeight,
 945                              numRowsWritten, progressReportRowPeriod);
 946 
 947                 if (abortRequested()) {
 948                     return;
 949                 }
 950 
 951                 numRowsWritten += destHeight/8;
 952 
 953                 writeRowsOpt(data, offset, lineStride, compressor,
 954                              4, 8, destWidth, destHeight,
 955                              numRowsWritten, progressReportRowPeriod);
 956 
 957                 if (abortRequested()) {
 958                     return;
 959                 }
 960 
 961                 numRowsWritten += (destHeight - 4)/8;
 962 
 963                 writeRowsOpt(data, offset, lineStride, compressor,
 964                              2, 4, destWidth, destHeight,
 965                              numRowsWritten, progressReportRowPeriod);
 966 
 967                 if (abortRequested()) {
 968                     return;
 969                 }
 970 
 971                 numRowsWritten += (destHeight - 2)/4;
 972 
 973                 writeRowsOpt(data, offset, lineStride, compressor,
 974                              1, 2, destWidth, destHeight,
 975                              numRowsWritten, progressReportRowPeriod);
 976             } else {
 977                 writeRows(image, compressor,
 978                           sourceXOffset, periodX,
 979                           sourceYOffset, 8*periodY,
 980                           sourceWidth,
 981                           0, 8, destWidth, destHeight,
 982                           numRowsWritten, progressReportRowPeriod);
 983 
 984                 if (abortRequested()) {
 985                     return;
 986                 }
 987 
 988                 numRowsWritten += destHeight/8;
 989 
 990                 writeRows(image, compressor, sourceXOffset, periodX,
 991                           sourceYOffset + 4*periodY, 8*periodY,
 992                           sourceWidth,
 993                           4, 8, destWidth, destHeight,
 994                           numRowsWritten, progressReportRowPeriod);
 995 
 996                 if (abortRequested()) {
 997                     return;
 998                 }
 999 
1000                 numRowsWritten += (destHeight - 4)/8;
1001 
1002                 writeRows(image, compressor, sourceXOffset, periodX,
1003                           sourceYOffset + 2*periodY, 4*periodY,
1004                           sourceWidth,
1005                           2, 4, destWidth, destHeight,
1006                           numRowsWritten, progressReportRowPeriod);
1007 
1008                 if (abortRequested()) {
1009                     return;
1010                 }
1011 
1012                 numRowsWritten += (destHeight - 2)/4;
1013 
1014                 writeRows(image, compressor, sourceXOffset, periodX,
1015                           sourceYOffset + periodY, 2*periodY,
1016                           sourceWidth,
1017                           1, 2, destWidth, destHeight,
1018                           numRowsWritten, progressReportRowPeriod);
1019             }
1020         } else {
1021             if (DEBUG) System.out.println("Writing non-interlaced");
1022 
1023             if (isOptimizedCase) {
1024                 Raster tile = image.getTile(0, 0);
1025                 byte[] data = ((DataBufferByte)tile.getDataBuffer()).getData();
1026                 ComponentSampleModel csm =
1027                     (ComponentSampleModel)tile.getSampleModel();
1028                 int offset = csm.getOffset(sourceXOffset, sourceYOffset, 0);
1029                 int lineStride = csm.getScanlineStride();
1030 
1031                 writeRowsOpt(data, offset, lineStride, compressor,
1032                              0, 1, destWidth, destHeight,
1033                              numRowsWritten, progressReportRowPeriod);
1034             } else {
1035                 writeRows(image, compressor,
1036                           sourceXOffset, periodX,
1037                           sourceYOffset, periodY,
1038                           sourceWidth,
1039                           0, 1, destWidth, destHeight,
1040                           numRowsWritten, progressReportRowPeriod);
1041             }
1042         }
1043 
1044         if (abortRequested()) {
1045             return;
1046         }
1047 
1048         processImageProgress(100.0F);
1049 
1050         compressor.flush();
1051 
1052         stream.write(0x00);
1053 
1054         processImageComplete();
1055     }
1056 
1057     private void writeHeader(String version,
1058                              int logicalScreenWidth,
1059                              int logicalScreenHeight,
1060                              int colorResolution,
1061                              int pixelAspectRatio,
1062                              int backgroundColorIndex,
1063                              boolean sortFlag,
1064                              int bitsPerPixel,
1065                              byte[] globalColorTable) throws IOException {
1066         try {
1067             // Signature
1068             stream.writeBytes("GIF"+version);
1069 
1070             // Screen Descriptor
1071             // Width
1072             stream.writeShort((short)logicalScreenWidth);
1073 
1074             // Height
1075             stream.writeShort((short)logicalScreenHeight);
1076 
1077             // Global Color Table
1078             // Packed fields
1079             int packedFields = globalColorTable != null ? 0x80 : 0x00;
1080             packedFields |= ((colorResolution - 1) & 0x7) << 4;
1081             if (sortFlag) {
1082                 packedFields |= 0x8;
1083             }
1084             packedFields |= (bitsPerPixel - 1);
1085             stream.write(packedFields);
1086 
1087             // Background color index
1088             stream.write(backgroundColorIndex);
1089 
1090             // Pixel aspect ratio
1091             stream.write(pixelAspectRatio);
1092 
1093             // Global Color Table
1094             if (globalColorTable != null) {
1095                 stream.write(globalColorTable);
1096             }
1097         } catch (IOException e) {
1098             throw new IIOException("I/O error writing header!", e);
1099         }
1100     }
1101 
1102     private void writeHeader(IIOMetadata streamMetadata, int bitsPerPixel)
1103       throws IOException {
1104 
1105         GIFWritableStreamMetadata sm;
1106         if (streamMetadata instanceof GIFWritableStreamMetadata) {
1107             sm = (GIFWritableStreamMetadata)streamMetadata;
1108         } else {
1109             sm = new GIFWritableStreamMetadata();
1110             Node root =
1111                 streamMetadata.getAsTree(STREAM_METADATA_NAME);
1112             sm.setFromTree(STREAM_METADATA_NAME, root);
1113         }
1114 
1115         writeHeader(sm.version,
1116                     sm.logicalScreenWidth,
1117                     sm.logicalScreenHeight,
1118                     sm.colorResolution,
1119                     sm.pixelAspectRatio,
1120                     sm.backgroundColorIndex,
1121                     sm.sortFlag,
1122                     bitsPerPixel,
1123                     sm.globalColorTable);
1124     }
1125 
1126     private void writeGraphicControlExtension(int disposalMethod,
1127                                               boolean userInputFlag,
1128                                               boolean transparentColorFlag,
1129                                               int delayTime,
1130                                               int transparentColorIndex)
1131       throws IOException {
1132         try {
1133             stream.write(0x21);
1134             stream.write(0xf9);
1135 
1136             stream.write(4);
1137 
1138             int packedFields = (disposalMethod & 0x3) << 2;
1139             if (userInputFlag) {
1140                 packedFields |= 0x2;
1141             }
1142             if (transparentColorFlag) {
1143                 packedFields |= 0x1;
1144             }
1145             stream.write(packedFields);
1146 
1147             stream.writeShort((short)delayTime);
1148 
1149             stream.write(transparentColorIndex);
1150             stream.write(0x00);
1151         } catch (IOException e) {
1152             throw new IIOException("I/O error writing Graphic Control Extension!", e);
1153         }
1154     }
1155 
1156     private void writeGraphicControlExtension(GIFWritableImageMetadata im)
1157       throws IOException {
1158         writeGraphicControlExtension(im.disposalMethod,
1159                                      im.userInputFlag,
1160                                      im.transparentColorFlag,
1161                                      im.delayTime,
1162                                      im.transparentColorIndex);
1163     }
1164 
1165     private void writeBlocks(byte[] data) throws IOException {
1166         if (data != null && data.length > 0) {
1167             int offset = 0;
1168             while (offset < data.length) {
1169                 int len = Math.min(data.length - offset, 255);
1170                 stream.write(len);
1171                 stream.write(data, offset, len);
1172                 offset += len;
1173             }
1174         }
1175     }
1176 
1177     private void writePlainTextExtension(GIFWritableImageMetadata im)
1178       throws IOException {
1179         if (im.hasPlainTextExtension) {
1180             try {
1181                 stream.write(0x21);
1182                 stream.write(0x1);
1183 
1184                 stream.write(12);
1185 
1186                 stream.writeShort(im.textGridLeft);
1187                 stream.writeShort(im.textGridTop);
1188                 stream.writeShort(im.textGridWidth);
1189                 stream.writeShort(im.textGridHeight);
1190                 stream.write(im.characterCellWidth);
1191                 stream.write(im.characterCellHeight);
1192                 stream.write(im.textForegroundColor);
1193                 stream.write(im.textBackgroundColor);
1194 
1195                 writeBlocks(im.text);
1196 
1197                 stream.write(0x00);
1198             } catch (IOException e) {
1199                 throw new IIOException("I/O error writing Plain Text Extension!", e);
1200             }
1201         }
1202     }
1203 
1204     private void writeApplicationExtension(GIFWritableImageMetadata im)
1205       throws IOException {
1206         if (im.applicationIDs != null) {
1207             Iterator iterIDs = im.applicationIDs.iterator();
1208             Iterator iterCodes = im.authenticationCodes.iterator();
1209             Iterator iterData = im.applicationData.iterator();
1210 
1211             while (iterIDs.hasNext()) {
1212                 try {
1213                     stream.write(0x21);
1214                     stream.write(0xff);
1215 
1216                     stream.write(11);
1217                     stream.write((byte[])iterIDs.next(), 0, 8);
1218                     stream.write((byte[])iterCodes.next(), 0, 3);
1219 
1220                     writeBlocks((byte[])iterData.next());
1221 
1222                     stream.write(0x00);
1223                 } catch (IOException e) {
1224                     throw new IIOException("I/O error writing Application Extension!", e);
1225                 }
1226             }
1227         }
1228     }
1229 
1230     private void writeCommentExtension(GIFWritableImageMetadata im)
1231       throws IOException {
1232         if (im.comments != null) {
1233             try {
1234                 Iterator iter = im.comments.iterator();
1235                 while (iter.hasNext()) {
1236                     stream.write(0x21);
1237                     stream.write(0xfe);
1238                     writeBlocks((byte[])iter.next());
1239                     stream.write(0x00);
1240                 }
1241             } catch (IOException e) {
1242                 throw new IIOException("I/O error writing Comment Extension!", e);
1243             }
1244         }
1245     }
1246 
1247     private void writeImageDescriptor(int imageLeftPosition,
1248                                       int imageTopPosition,
1249                                       int imageWidth,
1250                                       int imageHeight,
1251                                       boolean interlaceFlag,
1252                                       boolean sortFlag,
1253                                       int bitsPerPixel,
1254                                       byte[] localColorTable)
1255       throws IOException {
1256 
1257         try {
1258             stream.write(0x2c);
1259 
1260             stream.writeShort((short)imageLeftPosition);
1261             stream.writeShort((short)imageTopPosition);
1262             stream.writeShort((short)imageWidth);
1263             stream.writeShort((short)imageHeight);
1264 
1265             int packedFields = localColorTable != null ? 0x80 : 0x00;
1266             if (interlaceFlag) {
1267                 packedFields |= 0x40;
1268             }
1269             if (sortFlag) {
1270                 packedFields |= 0x8;
1271             }
1272             packedFields |= (bitsPerPixel - 1);
1273             stream.write(packedFields);
1274 
1275             if (localColorTable != null) {
1276                 stream.write(localColorTable);
1277             }
1278         } catch (IOException e) {
1279             throw new IIOException("I/O error writing Image Descriptor!", e);
1280         }
1281     }
1282 
1283     private void writeImageDescriptor(GIFWritableImageMetadata imageMetadata,
1284                                       int bitsPerPixel)
1285       throws IOException {
1286 
1287         writeImageDescriptor(imageMetadata.imageLeftPosition,
1288                              imageMetadata.imageTopPosition,
1289                              imageMetadata.imageWidth,
1290                              imageMetadata.imageHeight,
1291                              imageMetadata.interlaceFlag,
1292                              imageMetadata.sortFlag,
1293                              bitsPerPixel,
1294                              imageMetadata.localColorTable);
1295     }
1296 
1297     private void writeTrailer() throws IOException {
1298         stream.write(0x3b);
1299     }
1300 }
1301 
1302 class GIFImageWriteParam extends ImageWriteParam {
1303     GIFImageWriteParam(Locale locale) {
1304         super(locale);
1305         this.canWriteCompressed = true;
1306         this.canWriteProgressive = true;
1307         this.compressionTypes = new String[] {"LZW", "lzw"};
1308         this.compressionType = compressionTypes[0];
1309     }
1310 
1311     public void setCompressionMode(int mode) {
1312         if (mode == MODE_DISABLED) {
1313             throw new UnsupportedOperationException("MODE_DISABLED is not supported.");
1314         }
1315         super.setCompressionMode(mode);
1316     }
1317 }