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