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