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