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