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 */ getNumBits(int value)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 */ computeRegions(Rectangle sourceBounds, Dimension destSize, ImageWriteParam p)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 */ createColorTable(ColorModel colorModel, SampleModel sampleModel)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 */ getGifPaletteSize(int x)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 GIFImageWriter(GIFImageWriterSpi originatingProvider)263 public GIFImageWriter(GIFImageWriterSpi originatingProvider) { 264 super(originatingProvider); 265 if (DEBUG) { 266 System.err.println("GIF Writer is created"); 267 } 268 } 269 canWriteSequence()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 */ convertMetadata(String metadataFormatName, IIOMetadata inData, IIOMetadata outData)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 */ convertStreamMetadata(IIOMetadata inData, ImageWriteParam param)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 */ convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param)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 endWriteSequence()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 getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param)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 getDefaultStreamMetadata(ImageWriteParam param)425 public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { 426 GIFWritableStreamMetadata streamMetadata = 427 new GIFWritableStreamMetadata(); 428 streamMetadata.version = "89a"; 429 return streamMetadata; 430 } 431 getDefaultWriteParam()432 public ImageWriteParam getDefaultWriteParam() { 433 return new GIFImageWriteParam(getLocale()); 434 } 435 prepareWriteSequence(IIOMetadata streamMetadata)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 reset()458 public void reset() { 459 super.reset(); 460 resetLocal(); 461 } 462 463 /** 464 * Resets locally defined instance variables. 465 */ resetLocal()466 private void resetLocal() { 467 this.isWritingSequence = false; 468 this.wroteSequenceHeader = false; 469 this.theStreamMetadata = null; 470 this.imageIndex = 0; 471 } 472 setOutput(Object output)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 write(IIOMetadata sm, IIOImage iioimage, ImageWriteParam p)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 writeToSequence(IIOImage image, ImageWriteParam param)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 needToCreateIndex(RenderedImage image)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 */ write(boolean writeHeader, boolean writeTrailer, IIOMetadata sm, IIOImage iioimage, ImageWriteParam p)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 image The image. 733 * @param imageMetadata The image metadata. 734 * @param param The write parameters. 735 * @param globalColorTable The Global Color Table. 736 * @param sourceBounds The source region. 737 * @param destSize The destination dimensions. 738 */ writeImage(RenderedImage image, GIFWritableImageMetadata imageMetadata, ImageWriteParam param, byte[] globalColorTable, Rectangle sourceBounds, Dimension destSize)739 private void writeImage(RenderedImage image, 740 GIFWritableImageMetadata imageMetadata, 741 ImageWriteParam param, byte[] globalColorTable, 742 Rectangle sourceBounds, Dimension destSize) 743 throws IOException { 744 ColorModel colorModel = image.getColorModel(); 745 SampleModel sampleModel = image.getSampleModel(); 746 747 boolean writeGraphicsControlExtension; 748 if (imageMetadata == null) { 749 // Create default metadata. 750 imageMetadata = (GIFWritableImageMetadata)getDefaultImageMetadata( 751 new ImageTypeSpecifier(image), param); 752 753 // Set GraphicControlExtension flag only if there is 754 // transparency. 755 writeGraphicsControlExtension = imageMetadata.transparentColorFlag; 756 } else { 757 // Check for GraphicControlExtension element. 758 NodeList list = null; 759 try { 760 IIOMetadataNode root = (IIOMetadataNode) 761 imageMetadata.getAsTree(IMAGE_METADATA_NAME); 762 list = root.getElementsByTagName("GraphicControlExtension"); 763 } catch(IllegalArgumentException iae) { 764 // Should never happen. 765 } 766 767 // Set GraphicControlExtension flag if element present. 768 writeGraphicsControlExtension = 769 list != null && list.getLength() > 0; 770 771 // If progressive mode is not MODE_COPY_FROM_METADATA, ensure 772 // the interlacing is set per the ImageWriteParam mode setting. 773 if (param != null && param.canWriteProgressive()) { 774 if (param.getProgressiveMode() == 775 ImageWriteParam.MODE_DISABLED) { 776 imageMetadata.interlaceFlag = false; 777 } else if (param.getProgressiveMode() == 778 ImageWriteParam.MODE_DEFAULT) { 779 imageMetadata.interlaceFlag = true; 780 } 781 } 782 } 783 784 // Unset local color table if equal to global color table. 785 if (Arrays.equals(globalColorTable, imageMetadata.localColorTable)) { 786 imageMetadata.localColorTable = null; 787 } 788 789 // Override dimensions 790 imageMetadata.imageWidth = destSize.width; 791 imageMetadata.imageHeight = destSize.height; 792 793 // Write Graphics Control Extension. 794 if (writeGraphicsControlExtension) { 795 writeGraphicControlExtension(imageMetadata); 796 } 797 798 // Write extension blocks. 799 writePlainTextExtension(imageMetadata); 800 writeApplicationExtension(imageMetadata); 801 writeCommentExtension(imageMetadata); 802 803 // Write Image Descriptor 804 int bitsPerPixel = 805 getNumBits(imageMetadata.localColorTable == null ? 806 (globalColorTable == null ? 807 sampleModel.getSampleSize(0) : 808 globalColorTable.length/3) : 809 imageMetadata.localColorTable.length/3); 810 writeImageDescriptor(imageMetadata, bitsPerPixel); 811 812 // Write image data 813 writeRasterData(image, sourceBounds, destSize, 814 param, imageMetadata.interlaceFlag); 815 } 816 writeRows(RenderedImage image, LZWCompressor compressor, int sx, int sdx, int sy, int sdy, int sw, int dy, int ddy, int dw, int dh, int numRowsWritten, int progressReportRowPeriod)817 private void writeRows(RenderedImage image, LZWCompressor compressor, 818 int sx, int sdx, int sy, int sdy, int sw, 819 int dy, int ddy, int dw, int dh, 820 int numRowsWritten, int progressReportRowPeriod) 821 throws IOException { 822 if (DEBUG) System.out.println("Writing unoptimized"); 823 824 int[] sbuf = new int[sw]; 825 byte[] dbuf = new byte[dw]; 826 827 Raster raster = 828 image.getNumXTiles() == 1 && image.getNumYTiles() == 1 ? 829 image.getTile(0, 0) : image.getData(); 830 for (int y = dy; y < dh; y += ddy) { 831 if (numRowsWritten % progressReportRowPeriod == 0) { 832 processImageProgress((numRowsWritten*100.0F)/dh); 833 if (abortRequested()) { 834 processWriteAborted(); 835 return; 836 } 837 } 838 839 raster.getSamples(sx, sy, sw, 1, 0, sbuf); 840 for (int i = 0, j = 0; i < dw; i++, j += sdx) { 841 dbuf[i] = (byte)sbuf[j]; 842 } 843 compressor.compress(dbuf, 0, dw); 844 numRowsWritten++; 845 sy += sdy; 846 } 847 } 848 writeRowsOpt(byte[] data, int offset, int lineStride, LZWCompressor compressor, int dy, int ddy, int dw, int dh, int numRowsWritten, int progressReportRowPeriod)849 private void writeRowsOpt(byte[] data, int offset, int lineStride, 850 LZWCompressor compressor, 851 int dy, int ddy, int dw, int dh, 852 int numRowsWritten, int progressReportRowPeriod) 853 throws IOException { 854 if (DEBUG) System.out.println("Writing optimized"); 855 856 offset += dy*lineStride; 857 lineStride *= ddy; 858 for (int y = dy; y < dh; y += ddy) { 859 if (numRowsWritten % progressReportRowPeriod == 0) { 860 processImageProgress((numRowsWritten*100.0F)/dh); 861 if (abortRequested()) { 862 processWriteAborted(); 863 return; 864 } 865 } 866 867 compressor.compress(data, offset, dw); 868 numRowsWritten++; 869 offset += lineStride; 870 } 871 } 872 writeRasterData(RenderedImage image, Rectangle sourceBounds, Dimension destSize, ImageWriteParam param, boolean interlaceFlag)873 private void writeRasterData(RenderedImage image, 874 Rectangle sourceBounds, 875 Dimension destSize, 876 ImageWriteParam param, 877 boolean interlaceFlag) throws IOException { 878 879 int sourceXOffset = sourceBounds.x; 880 int sourceYOffset = sourceBounds.y; 881 int sourceWidth = sourceBounds.width; 882 int sourceHeight = sourceBounds.height; 883 884 int destWidth = destSize.width; 885 int destHeight = destSize.height; 886 887 int periodX; 888 int periodY; 889 if (param == null) { 890 periodX = 1; 891 periodY = 1; 892 } else { 893 periodX = param.getSourceXSubsampling(); 894 periodY = param.getSourceYSubsampling(); 895 } 896 897 SampleModel sampleModel = image.getSampleModel(); 898 int bitsPerPixel = sampleModel.getSampleSize()[0]; 899 900 int initCodeSize = bitsPerPixel; 901 if (initCodeSize == 1) { 902 initCodeSize++; 903 } 904 stream.write(initCodeSize); 905 906 LZWCompressor compressor = 907 new LZWCompressor(stream, initCodeSize, false); 908 909 /* At this moment we know that input image is indexed image. 910 * We can directly copy data iff: 911 * - no subsampling required (periodX = 1, periodY = 0) 912 * - we can access data directly (image is non-tiled, 913 * i.e. image data are in single block) 914 * - we can calculate offset in data buffer (next 3 lines) 915 */ 916 boolean isOptimizedCase = 917 periodX == 1 && periodY == 1 && 918 image.getNumXTiles() == 1 && image.getNumYTiles() == 1 && 919 sampleModel instanceof ComponentSampleModel && 920 image.getTile(0, 0) instanceof ByteComponentRaster && 921 image.getTile(0, 0).getDataBuffer() instanceof DataBufferByte; 922 923 int numRowsWritten = 0; 924 925 int progressReportRowPeriod = Math.max(destHeight/20, 1); 926 927 clearAbortRequest(); 928 processImageStarted(imageIndex); 929 if (abortRequested()) { 930 processWriteAborted(); 931 return; 932 } 933 934 if (interlaceFlag) { 935 if (DEBUG) System.out.println("Writing interlaced"); 936 937 if (isOptimizedCase) { 938 ByteComponentRaster tile = 939 (ByteComponentRaster)image.getTile(0, 0); 940 byte[] data = ((DataBufferByte)tile.getDataBuffer()).getData(); 941 ComponentSampleModel csm = 942 (ComponentSampleModel)tile.getSampleModel(); 943 int offset = csm.getOffset(sourceXOffset, sourceYOffset, 0); 944 // take into account the raster data offset 945 offset += tile.getDataOffset(0); 946 int lineStride = csm.getScanlineStride(); 947 948 writeRowsOpt(data, offset, lineStride, compressor, 949 0, 8, destWidth, destHeight, 950 numRowsWritten, progressReportRowPeriod); 951 952 if (abortRequested()) { 953 return; 954 } 955 956 numRowsWritten += destHeight/8; 957 958 writeRowsOpt(data, offset, lineStride, compressor, 959 4, 8, destWidth, destHeight, 960 numRowsWritten, progressReportRowPeriod); 961 962 if (abortRequested()) { 963 return; 964 } 965 966 numRowsWritten += (destHeight - 4)/8; 967 968 writeRowsOpt(data, offset, lineStride, compressor, 969 2, 4, destWidth, destHeight, 970 numRowsWritten, progressReportRowPeriod); 971 972 if (abortRequested()) { 973 return; 974 } 975 976 numRowsWritten += (destHeight - 2)/4; 977 978 writeRowsOpt(data, offset, lineStride, compressor, 979 1, 2, destWidth, destHeight, 980 numRowsWritten, progressReportRowPeriod); 981 if (abortRequested()) { 982 return; 983 } 984 } else { 985 writeRows(image, compressor, 986 sourceXOffset, periodX, 987 sourceYOffset, 8*periodY, 988 sourceWidth, 989 0, 8, destWidth, destHeight, 990 numRowsWritten, progressReportRowPeriod); 991 992 if (abortRequested()) { 993 return; 994 } 995 996 numRowsWritten += destHeight/8; 997 998 writeRows(image, compressor, sourceXOffset, periodX, 999 sourceYOffset + 4*periodY, 8*periodY, 1000 sourceWidth, 1001 4, 8, destWidth, destHeight, 1002 numRowsWritten, progressReportRowPeriod); 1003 1004 if (abortRequested()) { 1005 return; 1006 } 1007 1008 numRowsWritten += (destHeight - 4)/8; 1009 1010 writeRows(image, compressor, sourceXOffset, periodX, 1011 sourceYOffset + 2*periodY, 4*periodY, 1012 sourceWidth, 1013 2, 4, destWidth, destHeight, 1014 numRowsWritten, progressReportRowPeriod); 1015 1016 if (abortRequested()) { 1017 return; 1018 } 1019 1020 numRowsWritten += (destHeight - 2)/4; 1021 1022 writeRows(image, compressor, sourceXOffset, periodX, 1023 sourceYOffset + periodY, 2*periodY, 1024 sourceWidth, 1025 1, 2, destWidth, destHeight, 1026 numRowsWritten, progressReportRowPeriod); 1027 if (abortRequested()) { 1028 return; 1029 } 1030 } 1031 } else { 1032 if (DEBUG) System.out.println("Writing non-interlaced"); 1033 1034 if (isOptimizedCase) { 1035 Raster tile = image.getTile(0, 0); 1036 byte[] data = ((DataBufferByte)tile.getDataBuffer()).getData(); 1037 ComponentSampleModel csm = 1038 (ComponentSampleModel)tile.getSampleModel(); 1039 int offset = csm.getOffset(sourceXOffset, sourceYOffset, 0); 1040 int lineStride = csm.getScanlineStride(); 1041 1042 writeRowsOpt(data, offset, lineStride, compressor, 1043 0, 1, destWidth, destHeight, 1044 numRowsWritten, progressReportRowPeriod); 1045 if (abortRequested()) { 1046 return; 1047 } 1048 } else { 1049 writeRows(image, compressor, 1050 sourceXOffset, periodX, 1051 sourceYOffset, periodY, 1052 sourceWidth, 1053 0, 1, destWidth, destHeight, 1054 numRowsWritten, progressReportRowPeriod); 1055 if (abortRequested()) { 1056 return; 1057 } 1058 } 1059 } 1060 1061 compressor.flush(); 1062 1063 stream.write(0x00); 1064 1065 processImageComplete(); 1066 } 1067 writeHeader(String version, int logicalScreenWidth, int logicalScreenHeight, int colorResolution, int pixelAspectRatio, int backgroundColorIndex, boolean sortFlag, int bitsPerPixel, byte[] globalColorTable)1068 private void writeHeader(String version, 1069 int logicalScreenWidth, 1070 int logicalScreenHeight, 1071 int colorResolution, 1072 int pixelAspectRatio, 1073 int backgroundColorIndex, 1074 boolean sortFlag, 1075 int bitsPerPixel, 1076 byte[] globalColorTable) throws IOException { 1077 try { 1078 // Signature 1079 stream.writeBytes("GIF"+version); 1080 1081 // Screen Descriptor 1082 // Width 1083 stream.writeShort((short)logicalScreenWidth); 1084 1085 // Height 1086 stream.writeShort((short)logicalScreenHeight); 1087 1088 // Global Color Table 1089 // Packed fields 1090 int packedFields = globalColorTable != null ? 0x80 : 0x00; 1091 packedFields |= ((colorResolution - 1) & 0x7) << 4; 1092 if (sortFlag) { 1093 packedFields |= 0x8; 1094 } 1095 packedFields |= (bitsPerPixel - 1); 1096 stream.write(packedFields); 1097 1098 // Background color index 1099 stream.write(backgroundColorIndex); 1100 1101 // Pixel aspect ratio 1102 stream.write(pixelAspectRatio); 1103 1104 // Global Color Table 1105 if (globalColorTable != null) { 1106 stream.write(globalColorTable); 1107 } 1108 } catch (IOException e) { 1109 throw new IIOException("I/O error writing header!", e); 1110 } 1111 } 1112 writeHeader(IIOMetadata streamMetadata, int bitsPerPixel)1113 private void writeHeader(IIOMetadata streamMetadata, int bitsPerPixel) 1114 throws IOException { 1115 1116 GIFWritableStreamMetadata sm; 1117 if (streamMetadata instanceof GIFWritableStreamMetadata) { 1118 sm = (GIFWritableStreamMetadata)streamMetadata; 1119 } else { 1120 sm = new GIFWritableStreamMetadata(); 1121 Node root = 1122 streamMetadata.getAsTree(STREAM_METADATA_NAME); 1123 sm.setFromTree(STREAM_METADATA_NAME, root); 1124 } 1125 1126 writeHeader(sm.version, 1127 sm.logicalScreenWidth, 1128 sm.logicalScreenHeight, 1129 sm.colorResolution, 1130 sm.pixelAspectRatio, 1131 sm.backgroundColorIndex, 1132 sm.sortFlag, 1133 bitsPerPixel, 1134 sm.globalColorTable); 1135 } 1136 writeGraphicControlExtension(int disposalMethod, boolean userInputFlag, boolean transparentColorFlag, int delayTime, int transparentColorIndex)1137 private void writeGraphicControlExtension(int disposalMethod, 1138 boolean userInputFlag, 1139 boolean transparentColorFlag, 1140 int delayTime, 1141 int transparentColorIndex) 1142 throws IOException { 1143 try { 1144 stream.write(0x21); 1145 stream.write(0xf9); 1146 1147 stream.write(4); 1148 1149 int packedFields = (disposalMethod & 0x3) << 2; 1150 if (userInputFlag) { 1151 packedFields |= 0x2; 1152 } 1153 if (transparentColorFlag) { 1154 packedFields |= 0x1; 1155 } 1156 stream.write(packedFields); 1157 1158 stream.writeShort((short)delayTime); 1159 1160 stream.write(transparentColorIndex); 1161 stream.write(0x00); 1162 } catch (IOException e) { 1163 throw new IIOException("I/O error writing Graphic Control Extension!", e); 1164 } 1165 } 1166 writeGraphicControlExtension(GIFWritableImageMetadata im)1167 private void writeGraphicControlExtension(GIFWritableImageMetadata im) 1168 throws IOException { 1169 writeGraphicControlExtension(im.disposalMethod, 1170 im.userInputFlag, 1171 im.transparentColorFlag, 1172 im.delayTime, 1173 im.transparentColorIndex); 1174 } 1175 writeBlocks(byte[] data)1176 private void writeBlocks(byte[] data) throws IOException { 1177 if (data != null && data.length > 0) { 1178 int offset = 0; 1179 while (offset < data.length) { 1180 int len = Math.min(data.length - offset, 255); 1181 stream.write(len); 1182 stream.write(data, offset, len); 1183 offset += len; 1184 } 1185 } 1186 } 1187 writePlainTextExtension(GIFWritableImageMetadata im)1188 private void writePlainTextExtension(GIFWritableImageMetadata im) 1189 throws IOException { 1190 if (im.hasPlainTextExtension) { 1191 try { 1192 stream.write(0x21); 1193 stream.write(0x1); 1194 1195 stream.write(12); 1196 1197 stream.writeShort(im.textGridLeft); 1198 stream.writeShort(im.textGridTop); 1199 stream.writeShort(im.textGridWidth); 1200 stream.writeShort(im.textGridHeight); 1201 stream.write(im.characterCellWidth); 1202 stream.write(im.characterCellHeight); 1203 stream.write(im.textForegroundColor); 1204 stream.write(im.textBackgroundColor); 1205 1206 writeBlocks(im.text); 1207 1208 stream.write(0x00); 1209 } catch (IOException e) { 1210 throw new IIOException("I/O error writing Plain Text Extension!", e); 1211 } 1212 } 1213 } 1214 writeApplicationExtension(GIFWritableImageMetadata im)1215 private void writeApplicationExtension(GIFWritableImageMetadata im) 1216 throws IOException { 1217 if (im.applicationIDs != null) { 1218 Iterator<byte[]> iterIDs = im.applicationIDs.iterator(); 1219 Iterator<byte[]> iterCodes = im.authenticationCodes.iterator(); 1220 Iterator<byte[]> iterData = im.applicationData.iterator(); 1221 1222 while (iterIDs.hasNext()) { 1223 try { 1224 stream.write(0x21); 1225 stream.write(0xff); 1226 1227 stream.write(11); 1228 stream.write(iterIDs.next(), 0, 8); 1229 stream.write(iterCodes.next(), 0, 3); 1230 1231 writeBlocks(iterData.next()); 1232 1233 stream.write(0x00); 1234 } catch (IOException e) { 1235 throw new IIOException("I/O error writing Application Extension!", e); 1236 } 1237 } 1238 } 1239 } 1240 writeCommentExtension(GIFWritableImageMetadata im)1241 private void writeCommentExtension(GIFWritableImageMetadata im) 1242 throws IOException { 1243 if (im.comments != null) { 1244 try { 1245 Iterator<byte[]> iter = im.comments.iterator(); 1246 while (iter.hasNext()) { 1247 stream.write(0x21); 1248 stream.write(0xfe); 1249 writeBlocks(iter.next()); 1250 stream.write(0x00); 1251 } 1252 } catch (IOException e) { 1253 throw new IIOException("I/O error writing Comment Extension!", e); 1254 } 1255 } 1256 } 1257 writeImageDescriptor(int imageLeftPosition, int imageTopPosition, int imageWidth, int imageHeight, boolean interlaceFlag, boolean sortFlag, int bitsPerPixel, byte[] localColorTable)1258 private void writeImageDescriptor(int imageLeftPosition, 1259 int imageTopPosition, 1260 int imageWidth, 1261 int imageHeight, 1262 boolean interlaceFlag, 1263 boolean sortFlag, 1264 int bitsPerPixel, 1265 byte[] localColorTable) 1266 throws IOException { 1267 1268 try { 1269 stream.write(0x2c); 1270 1271 stream.writeShort((short)imageLeftPosition); 1272 stream.writeShort((short)imageTopPosition); 1273 stream.writeShort((short)imageWidth); 1274 stream.writeShort((short)imageHeight); 1275 1276 int packedFields = localColorTable != null ? 0x80 : 0x00; 1277 if (interlaceFlag) { 1278 packedFields |= 0x40; 1279 } 1280 if (sortFlag) { 1281 packedFields |= 0x8; 1282 } 1283 packedFields |= (bitsPerPixel - 1); 1284 stream.write(packedFields); 1285 1286 if (localColorTable != null) { 1287 stream.write(localColorTable); 1288 } 1289 } catch (IOException e) { 1290 throw new IIOException("I/O error writing Image Descriptor!", e); 1291 } 1292 } 1293 writeImageDescriptor(GIFWritableImageMetadata imageMetadata, int bitsPerPixel)1294 private void writeImageDescriptor(GIFWritableImageMetadata imageMetadata, 1295 int bitsPerPixel) 1296 throws IOException { 1297 1298 writeImageDescriptor(imageMetadata.imageLeftPosition, 1299 imageMetadata.imageTopPosition, 1300 imageMetadata.imageWidth, 1301 imageMetadata.imageHeight, 1302 imageMetadata.interlaceFlag, 1303 imageMetadata.sortFlag, 1304 bitsPerPixel, 1305 imageMetadata.localColorTable); 1306 } 1307 writeTrailer()1308 private void writeTrailer() throws IOException { 1309 stream.write(0x3b); 1310 } 1311 } 1312 1313 class GIFImageWriteParam extends ImageWriteParam { GIFImageWriteParam(Locale locale)1314 GIFImageWriteParam(Locale locale) { 1315 super(locale); 1316 this.canWriteCompressed = true; 1317 this.canWriteProgressive = true; 1318 this.compressionTypes = new String[] {"LZW"}; 1319 this.compressionType = compressionTypes[0]; 1320 } 1321 setCompressionMode(int mode)1322 public void setCompressionMode(int mode) { 1323 if (mode == MODE_DISABLED) { 1324 throw new UnsupportedOperationException("MODE_DISABLED is not supported."); 1325 } 1326 super.setCompressionMode(mode); 1327 } 1328 } 1329