1 /* 2 * Copyright (c) 1995, 2014, 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 /*- 27 * Reads GIF images from an InputStream and reports the 28 * image data to an InputStreamImageSource object. 29 * 30 * The algorithm is copyright of CompuServe. 31 */ 32 package sun.awt.image; 33 34 import java.util.Vector; 35 import java.util.Hashtable; 36 import java.io.InputStream; 37 import java.io.IOException; 38 import java.awt.image.*; 39 40 /** 41 * Gif Image converter 42 * 43 * @author Arthur van Hoff 44 * @author Jim Graham 45 */ 46 public class GifImageDecoder extends ImageDecoder { 47 private static final boolean verbose = false; 48 49 private static final int IMAGESEP = 0x2c; 50 private static final int EXBLOCK = 0x21; 51 private static final int EX_GRAPHICS_CONTROL= 0xf9; 52 private static final int EX_COMMENT = 0xfe; 53 private static final int EX_APPLICATION = 0xff; 54 private static final int TERMINATOR = 0x3b; 55 private static final int TRANSPARENCYMASK = 0x01; 56 private static final int INTERLACEMASK = 0x40; 57 private static final int COLORMAPMASK = 0x80; 58 59 int num_global_colors; 60 byte[] global_colormap; 61 int trans_pixel = -1; 62 IndexColorModel global_model; 63 64 Hashtable<String, Object> props = new Hashtable<>(); 65 66 byte[] saved_image; 67 IndexColorModel saved_model; 68 69 int global_width; 70 int global_height; 71 int global_bgpixel; 72 73 GifFrame curframe; 74 75 public GifImageDecoder(InputStreamImageSource src, InputStream is) { 76 super(src, is); 77 } 78 79 /** 80 * An error has occurred. Throw an exception. 81 */ 82 private static void error(String s1) throws ImageFormatException { 83 throw new ImageFormatException(s1); 84 } 85 86 /** 87 * Read a number of bytes into a buffer. 88 * @return number of bytes that were not read due to EOF or error 89 */ 90 private int readBytes(byte buf[], int off, int len) { 91 while (len > 0) { 92 try { 93 int n = input.read(buf, off, len); 94 if (n < 0) { 95 break; 96 } 97 off += n; 98 len -= n; 99 } catch (IOException e) { 100 break; 101 } 102 } 103 return len; 104 } 105 106 private static final int ExtractByte(byte buf[], int off) { 107 return (buf[off] & 0xFF); 108 } 109 110 private static final int ExtractWord(byte buf[], int off) { 111 return (buf[off] & 0xFF) | ((buf[off + 1] & 0xFF) << 8); 112 } 113 114 /** 115 * produce an image from the stream. 116 */ 117 @SuppressWarnings({"fallthrough", "deprecation"}) 118 public void produceImage() throws IOException, ImageFormatException { 119 try { 120 readHeader(); 121 122 int totalframes = 0; 123 int frameno = 0; 124 int nloops = -1; 125 int disposal_method = 0; 126 int delay = -1; 127 boolean loopsRead = false; 128 boolean isAnimation = false; 129 130 while (!aborted) { 131 int code; 132 133 switch (code = input.read()) { 134 case EXBLOCK: 135 switch (code = input.read()) { 136 case EX_GRAPHICS_CONTROL: { 137 byte buf[] = new byte[6]; 138 if (readBytes(buf, 0, 6) != 0) { 139 return;//error("corrupt GIF file"); 140 } 141 if ((buf[0] != 4) || (buf[5] != 0)) { 142 return;//error("corrupt GIF file (GCE size)"); 143 } 144 // Get the index of the transparent color 145 delay = ExtractWord(buf, 2) * 10; 146 if (delay > 0 && !isAnimation) { 147 isAnimation = true; 148 ImageFetcher.startingAnimation(); 149 } 150 disposal_method = (buf[1] >> 2) & 7; 151 if ((buf[1] & TRANSPARENCYMASK) != 0) { 152 trans_pixel = ExtractByte(buf, 4); 153 } else { 154 trans_pixel = -1; 155 } 156 break; 157 } 158 159 case EX_COMMENT: 160 case EX_APPLICATION: 161 default: 162 boolean loop_tag = false; 163 String comment = ""; 164 while (true) { 165 int n = input.read(); 166 if (n <= 0) { 167 break; 168 } 169 byte buf[] = new byte[n]; 170 if (readBytes(buf, 0, n) != 0) { 171 return;//error("corrupt GIF file"); 172 } 173 if (code == EX_COMMENT) { 174 comment += new String(buf, 0); 175 } else if (code == EX_APPLICATION) { 176 if (loop_tag) { 177 if (n == 3 && buf[0] == 1) { 178 if (loopsRead) { 179 ExtractWord(buf, 1); 180 } 181 else { 182 nloops = ExtractWord(buf, 1); 183 loopsRead = true; 184 } 185 } else { 186 loop_tag = false; 187 } 188 } 189 if ("NETSCAPE2.0".equals(new String(buf, 0))) { 190 loop_tag = true; 191 } 192 } 193 } 194 if (code == EX_COMMENT) { 195 props.put("comment", comment); 196 } 197 if (loop_tag && !isAnimation) { 198 isAnimation = true; 199 ImageFetcher.startingAnimation(); 200 } 201 break; 202 203 case -1: 204 return; //error("corrupt GIF file"); 205 } 206 break; 207 208 case IMAGESEP: 209 if (!isAnimation) { 210 input.mark(0); // we don't need the mark buffer 211 } 212 try { 213 if (!readImage(totalframes == 0, 214 disposal_method, 215 delay)) { 216 return; 217 } 218 } catch (Exception e) { 219 if (verbose) { 220 e.printStackTrace(); 221 } 222 return; 223 } 224 frameno++; 225 totalframes++; 226 break; 227 228 default: 229 case -1: 230 if (verbose) { 231 if (code == -1) { 232 System.err.println("Premature EOF in GIF file," + 233 " frame " + frameno); 234 } else { 235 System.err.println("corrupt GIF file (parse) [" 236 + code + "]."); 237 } 238 } 239 if (frameno == 0) { 240 return; 241 } 242 // Fall through 243 244 case TERMINATOR: 245 if (nloops == 0 || nloops-- >= 0) { 246 try { 247 if (curframe != null) { 248 curframe.dispose(); 249 curframe = null; 250 } 251 input.reset(); 252 saved_image = null; 253 saved_model = null; 254 frameno = 0; 255 break; 256 } catch (IOException e) { 257 return; // Unable to reset input buffer 258 } 259 } 260 if (verbose && frameno != 1) { 261 System.out.println("processing GIF terminator," 262 + " frames: " + frameno 263 + " total: " + totalframes); 264 } 265 imageComplete(ImageConsumer.STATICIMAGEDONE, true); 266 return; 267 } 268 } 269 } finally { 270 close(); 271 } 272 } 273 274 /** 275 * Read Image header 276 */ 277 private void readHeader() throws IOException, ImageFormatException { 278 // Create a buffer 279 byte buf[] = new byte[13]; 280 281 // Read the header 282 if (readBytes(buf, 0, 13) != 0) { 283 throw new IOException(); 284 } 285 286 // Check header 287 if ((buf[0] != 'G') || (buf[1] != 'I') || (buf[2] != 'F')) { 288 error("not a GIF file."); 289 } 290 291 // Global width&height 292 global_width = ExtractWord(buf, 6); 293 global_height = ExtractWord(buf, 8); 294 295 // colormap info 296 int ch = ExtractByte(buf, 10); 297 if ((ch & COLORMAPMASK) == 0) { 298 // no global colormap so make up our own 299 // If there is a local colormap, it will override what we 300 // have here. If there is not a local colormap, the rules 301 // for GIF89 say that we can use whatever colormap we want. 302 // This means that we should probably put in a full 256 colormap 303 // at some point. REMIND! 304 num_global_colors = 2; 305 global_bgpixel = 0; 306 global_colormap = new byte[2*3]; 307 global_colormap[0] = global_colormap[1] = global_colormap[2] = (byte)0; 308 global_colormap[3] = global_colormap[4] = global_colormap[5] = (byte)255; 309 310 } 311 else { 312 num_global_colors = 1 << ((ch & 0x7) + 1); 313 314 global_bgpixel = ExtractByte(buf, 11); 315 316 if (buf[12] != 0) { 317 props.put("aspectratio", ""+((ExtractByte(buf, 12) + 15) / 64.0)); 318 } 319 320 // Read colors 321 global_colormap = new byte[num_global_colors * 3]; 322 if (readBytes(global_colormap, 0, num_global_colors * 3) != 0) { 323 throw new IOException(); 324 } 325 } 326 input.mark(Integer.MAX_VALUE); // set this mark in case this is an animated GIF 327 } 328 329 /** 330 * The ImageConsumer hints flag for a non-interlaced GIF image. 331 */ 332 private static final int normalflags = 333 ImageConsumer.TOPDOWNLEFTRIGHT | ImageConsumer.COMPLETESCANLINES | 334 ImageConsumer.SINGLEPASS | ImageConsumer.SINGLEFRAME; 335 336 /** 337 * The ImageConsumer hints flag for an interlaced GIF image. 338 */ 339 private static final int interlaceflags = 340 ImageConsumer.RANDOMPIXELORDER | ImageConsumer.COMPLETESCANLINES | 341 ImageConsumer.SINGLEPASS | ImageConsumer.SINGLEFRAME; 342 343 private short prefix[] = new short[4096]; 344 private byte suffix[] = new byte[4096]; 345 private byte outCode[] = new byte[4097]; 346 347 private static native void initIDs(); 348 349 static { 350 /* ensure that the necessary native libraries are loaded */ 351 NativeLibLoader.loadLibraries(); 352 initIDs(); 353 } 354 355 private native boolean parseImage(int x, int y, int width, int height, 356 boolean interlace, int initCodeSize, 357 byte block[], byte rasline[], 358 IndexColorModel model); 359 360 private int sendPixels(int x, int y, int width, int height, 361 byte rasline[], ColorModel model) { 362 int rasbeg, rasend, x2; 363 if (y < 0) { 364 height += y; 365 y = 0; 366 } 367 if (y + height > global_height) { 368 height = global_height - y; 369 } 370 if (height <= 0) { 371 return 1; 372 } 373 // rasline[0] == pixel at coordinate (x,y) 374 // rasline[width] == pixel at coordinate (x+width, y) 375 if (x < 0) { 376 rasbeg = -x; 377 width += x; // same as (width -= rasbeg) 378 x2 = 0; // same as (x2 = x + rasbeg) 379 } else { 380 rasbeg = 0; 381 // width -= 0; // same as (width -= rasbeg) 382 x2 = x; // same as (x2 = x + rasbeg) 383 } 384 // rasline[rasbeg] == pixel at coordinate (x2,y) 385 // rasline[width] == pixel at coordinate (x+width, y) 386 // rasline[rasbeg + width] == pixel at coordinate (x2+width, y) 387 if (x2 + width > global_width) { 388 width = global_width - x2; 389 } 390 if (width <= 0) { 391 return 1; 392 } 393 rasend = rasbeg + width; 394 // rasline[rasbeg] == pixel at coordinate (x2,y) 395 // rasline[rasend] == pixel at coordinate (x2+width, y) 396 int off = y * global_width + x2; 397 boolean save = (curframe.disposal_method == GifFrame.DISPOSAL_SAVE); 398 if (trans_pixel >= 0 && !curframe.initialframe) { 399 if (saved_image != null && model.equals(saved_model)) { 400 for (int i = rasbeg; i < rasend; i++, off++) { 401 byte pixel = rasline[i]; 402 if ((pixel & 0xff) == trans_pixel) { 403 rasline[i] = saved_image[off]; 404 } else if (save) { 405 saved_image[off] = pixel; 406 } 407 } 408 } else { 409 // We have to do this the hard way - only transmit 410 // the non-transparent sections of the line... 411 // Fix for 6301050: the interlacing is ignored in this case 412 // in order to avoid artefacts in case of animated images. 413 int runstart = -1; 414 int count = 1; 415 for (int i = rasbeg; i < rasend; i++, off++) { 416 byte pixel = rasline[i]; 417 if ((pixel & 0xff) == trans_pixel) { 418 if (runstart >= 0) { 419 count = setPixels(x + runstart, y, 420 i - runstart, 1, 421 model, rasline, 422 runstart, 0); 423 if (count == 0) { 424 break; 425 } 426 } 427 runstart = -1; 428 } else { 429 if (runstart < 0) { 430 runstart = i; 431 } 432 if (save) { 433 saved_image[off] = pixel; 434 } 435 } 436 } 437 if (runstart >= 0) { 438 count = setPixels(x + runstart, y, 439 rasend - runstart, 1, 440 model, rasline, 441 runstart, 0); 442 } 443 return count; 444 } 445 } else if (save) { 446 System.arraycopy(rasline, rasbeg, saved_image, off, width); 447 } 448 int count = setPixels(x2, y, width, height, model, 449 rasline, rasbeg, 0); 450 return count; 451 } 452 453 /** 454 * Read Image data 455 */ 456 private boolean readImage(boolean first, int disposal_method, int delay) 457 throws IOException 458 { 459 if (curframe != null && !curframe.dispose()) { 460 abort(); 461 return false; 462 } 463 464 long tm = 0; 465 466 if (verbose) { 467 tm = System.currentTimeMillis(); 468 } 469 470 // Allocate the buffer 471 byte block[] = new byte[256 + 3]; 472 473 // Read the image descriptor 474 if (readBytes(block, 0, 10) != 0) { 475 throw new IOException(); 476 } 477 int x = ExtractWord(block, 0); 478 int y = ExtractWord(block, 2); 479 int width = ExtractWord(block, 4); 480 int height = ExtractWord(block, 6); 481 482 /* 483 * Majority of gif images have 484 * same logical screen and frame dimensions. 485 * Also, Photoshop and Mozilla seem to use the logical 486 * screen dimension (from the global stream header) 487 * if frame dimension is invalid. 488 * 489 * We use similar heuristic and trying to recover 490 * frame width from logical screen dimension and 491 * frame offset. 492 */ 493 if (width == 0 && global_width != 0) { 494 width = global_width - x; 495 } 496 if (height == 0 && global_height != 0) { 497 height = global_height - y; 498 } 499 500 boolean interlace = (block[8] & INTERLACEMASK) != 0; 501 502 IndexColorModel model = global_model; 503 504 if ((block[8] & COLORMAPMASK) != 0) { 505 // We read one extra byte above so now when we must 506 // transfer that byte as the first colormap byte 507 // and manually read the code size when we are done 508 int num_local_colors = 1 << ((block[8] & 0x7) + 1); 509 510 // Read local colors 511 byte[] local_colormap = new byte[num_local_colors * 3]; 512 local_colormap[0] = block[9]; 513 if (readBytes(local_colormap, 1, num_local_colors * 3 - 1) != 0) { 514 throw new IOException(); 515 } 516 517 // Now read the "real" code size byte which follows 518 // the local color table 519 if (readBytes(block, 9, 1) != 0) { 520 throw new IOException(); 521 } 522 if (trans_pixel >= num_local_colors) { 523 // Fix for 4233748: extend colormap to contain transparent pixel 524 num_local_colors = trans_pixel + 1; 525 local_colormap = grow_colormap(local_colormap, num_local_colors); 526 } 527 model = new IndexColorModel(8, num_local_colors, local_colormap, 528 0, false, trans_pixel); 529 } else if (model == null 530 || trans_pixel != model.getTransparentPixel()) { 531 if (trans_pixel >= num_global_colors) { 532 // Fix for 4233748: extend colormap to contain transparent pixel 533 num_global_colors = trans_pixel + 1; 534 global_colormap = grow_colormap(global_colormap, num_global_colors); 535 } 536 model = new IndexColorModel(8, num_global_colors, global_colormap, 537 0, false, trans_pixel); 538 global_model = model; 539 } 540 541 // Notify the consumers 542 if (first) { 543 if (global_width == 0) global_width = width; 544 if (global_height == 0) global_height = height; 545 546 setDimensions(global_width, global_height); 547 setProperties(props); 548 setColorModel(model); 549 headerComplete(); 550 } 551 552 if (disposal_method == GifFrame.DISPOSAL_SAVE && saved_image == null) { 553 saved_image = new byte[global_width * global_height]; 554 /* 555 * If height of current image is smaller than the global height, 556 * fill the gap with transparent pixels. 557 */ 558 if ((height < global_height) && (model != null)) { 559 byte tpix = (byte)model.getTransparentPixel(); 560 if (tpix >= 0) { 561 byte trans_rasline[] = new byte[global_width]; 562 for (int i=0; i<global_width;i++) { 563 trans_rasline[i] = tpix; 564 } 565 566 setPixels(0, 0, global_width, y, 567 model, trans_rasline, 0, 0); 568 setPixels(0, y+height, global_width, 569 global_height-height-y, model, trans_rasline, 570 0, 0); 571 } 572 } 573 } 574 575 int hints = (interlace ? interlaceflags : normalflags); 576 setHints(hints); 577 578 curframe = new GifFrame(this, disposal_method, delay, 579 (curframe == null), model, 580 x, y, width, height); 581 582 // allocate the raster data 583 byte rasline[] = new byte[width]; 584 585 if (verbose) { 586 System.out.print("Reading a " + width + " by " + height + " " + 587 (interlace ? "" : "non-") + "interlaced image..."); 588 } 589 int initCodeSize = ExtractByte(block, 9); 590 if (initCodeSize >= 12) { 591 if (verbose) { 592 System.out.println("Invalid initial code size: " + 593 initCodeSize); 594 } 595 return false; 596 } 597 boolean ret = parseImage(x, y, width, height, 598 interlace, initCodeSize, 599 block, rasline, model); 600 601 if (!ret) { 602 abort(); 603 } 604 605 if (verbose) { 606 System.out.println("done in " 607 + (System.currentTimeMillis() - tm) 608 + "ms"); 609 } 610 611 return ret; 612 } 613 614 public static byte[] grow_colormap(byte[] colormap, int newlen) { 615 byte[] newcm = new byte[newlen * 3]; 616 System.arraycopy(colormap, 0, newcm, 0, colormap.length); 617 return newcm; 618 } 619 } 620 621 class GifFrame { 622 private static final boolean verbose = false; 623 private static IndexColorModel trans_model; 624 625 static final int DISPOSAL_NONE = 0x00; 626 static final int DISPOSAL_SAVE = 0x01; 627 static final int DISPOSAL_BGCOLOR = 0x02; 628 static final int DISPOSAL_PREVIOUS = 0x03; 629 630 GifImageDecoder decoder; 631 632 int disposal_method; 633 int delay; 634 635 IndexColorModel model; 636 637 int x; 638 int y; 639 int width; 640 int height; 641 642 boolean initialframe; 643 644 public GifFrame(GifImageDecoder id, int dm, int dl, boolean init, 645 IndexColorModel cm, int x, int y, int w, int h) { 646 this.decoder = id; 647 this.disposal_method = dm; 648 this.delay = dl; 649 this.model = cm; 650 this.initialframe = init; 651 this.x = x; 652 this.y = y; 653 this.width = w; 654 this.height = h; 655 } 656 657 private void setPixels(int x, int y, int w, int h, 658 ColorModel cm, byte[] pix, int off, int scan) { 659 decoder.setPixels(x, y, w, h, cm, pix, off, scan); 660 } 661 662 public boolean dispose() { 663 if (decoder.imageComplete(ImageConsumer.SINGLEFRAMEDONE, false) == 0) { 664 return false; 665 } else { 666 if (delay > 0) { 667 try { 668 if (verbose) { 669 System.out.println("sleeping: "+delay); 670 } 671 Thread.sleep(delay); 672 } catch (InterruptedException e) { 673 return false; 674 } 675 } else { 676 Thread.yield(); 677 } 678 679 if (verbose && disposal_method != 0) { 680 System.out.println("disposal method: "+disposal_method); 681 } 682 683 int global_width = decoder.global_width; 684 int global_height = decoder.global_height; 685 686 if (x < 0) { 687 width += x; 688 x = 0; 689 } 690 if (x + width > global_width) { 691 width = global_width - x; 692 } 693 if (width <= 0) { 694 disposal_method = DISPOSAL_NONE; 695 } else { 696 if (y < 0) { 697 height += y; 698 y = 0; 699 } 700 if (y + height > global_height) { 701 height = global_height - y; 702 } 703 if (height <= 0) { 704 disposal_method = DISPOSAL_NONE; 705 } 706 } 707 708 switch (disposal_method) { 709 case DISPOSAL_PREVIOUS: 710 byte[] saved_image = decoder.saved_image; 711 IndexColorModel saved_model = decoder.saved_model; 712 if (saved_image != null) { 713 setPixels(x, y, width, height, 714 saved_model, saved_image, 715 y * global_width + x, global_width); 716 } 717 break; 718 case DISPOSAL_BGCOLOR: 719 byte tpix; 720 if (model.getTransparentPixel() < 0) { 721 model = trans_model; 722 if (model == null) { 723 model = new IndexColorModel(8, 1, 724 new byte[4], 0, true); 725 trans_model = model; 726 } 727 tpix = 0; 728 } else { 729 tpix = (byte) model.getTransparentPixel(); 730 } 731 byte[] rasline = new byte[width]; 732 if (tpix != 0) { 733 for (int i = 0; i < width; i++) { 734 rasline[i] = tpix; 735 } 736 } 737 738 // clear saved_image using transparent pixels 739 // this will be used as the background in the next display 740 if( decoder.saved_image != null ) { 741 for( int i = 0; i < global_width * global_height; i ++ ) 742 decoder.saved_image[i] = tpix; 743 } 744 745 setPixels(x, y, width, height, model, rasline, 0, 0); 746 break; 747 case DISPOSAL_SAVE: 748 decoder.saved_model = model; 749 break; 750 } 751 } 752 return true; 753 } 754 }