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