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