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