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