Source code: Acme/JPM/Encoders/GifEncoder.java
1 // GifEncoder - write out an image as a GIF
2 //
3 // Transparency handling and variable bit size courtesy of Jack Palevich.
4 //
5 // Copyright (C)1996,1998 by Jef Poskanzer <jef@acme.com>. All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions
9 // are met:
10 // 1. Redistributions of source code must retain the above copyright
11 // notice, this list of conditions and the following disclaimer.
12 // 2. Redistributions in binary form must reproduce the above copyright
13 // notice, this list of conditions and the following disclaimer in the
14 // documentation and/or other materials provided with the distribution.
15 //
16 // THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17 // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 // ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20 // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22 // OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23 // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26 // SUCH DAMAGE.
27 //
28 // Visit the ACME Labs Java page for up-to-date versions of this and other
29 // fine Java utilities: http://www.acme.com/java/
30 package Acme.JPM.Encoders;
31
32 import java.awt.Image;
33 import java.awt.image.*;
34
35 import java.io.*;
36
37 import java.util.*;
38
39
40 /**
41 * Write out an image as a GIF.
42 * <P>
43 * <A HREF="/resources/classes/Acme/JPM/Encoders/GifEncoder.java">Fetch the software.</A><BR>
44 * <A HREF="/resources/classes/Acme.tar.gz">Fetch the entire Acme package.</A>
45 * <P>
46 * @see ToGif
47 * @cvsversion $Revision: 1.3 $, $Date: 2003/10/27 10:10:19 $
48 */
49 public class GifEncoder extends ImageEncoder
50 {
51 //~ Static fields/initializers /////////////////////////////////////////////
52
53 static final int EOF = -1;
54
55 // GIFCOMPR.C - GIF Image compression routines
56 //
57 // Lempel-Ziv compression based on 'compress'. GIF modifications by
58 // David Rowley (mgardi@watdcsu.waterloo.edu)
59 // General DEFINEs
60 static final int BITS = 12;
61 static final int HSIZE = 5003; // 80% occupancy
62
63 //~ Instance fields ////////////////////////////////////////////////////////
64
65 Acme.IntHashtable colorHash;
66
67 // Define the storage for the packet accumulator
68 byte[] accum = new byte[256];
69 int[] codetab = new int[HSIZE];
70 int[] htab = new int[HSIZE];
71 int[] masks =
72 {
73 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF,
74 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF
75 };
76 int[][] rgbPixels;
77 boolean Interlace;
78 boolean clear_flg = false;
79 int ClearCode;
80 int CountDown;
81 int EOFCode;
82
83 // Adapted from ppmtogif, which is based on GIFENCOD by David
84 // Rowley <mgardi@watdscu.waterloo.edu>. Lempel-Zim compression
85 // based on "compress".
86 int Height;
87 int Pass = 0;
88
89 // Adapted from ppmtogif, which is based on GIFENCOD by David
90 // Rowley <mgardi@watdscu.waterloo.edu>. Lempel-Zim compression
91 // based on "compress".
92 int Width;
93
94 // GIF Specific routines
95 // Number of characters so far in this 'packet'
96 int a_count;
97
98 // output
99 //
100 // Output the given code.
101 // Inputs:
102 // code: A n_bits-bit integer. If == -1, then EOF. This assumes
103 // that n_bits =< wordsize - 1.
104 // Outputs:
105 // Outputs code to the file.
106 // Assumptions:
107 // Chars are 8 bits long.
108 // Algorithm:
109 // Maintain a BITS character long buffer (so that 8 codes will
110 // fit in it exactly). Use the VAX insv instruction to insert each
111 // code in turn. When the buffer fills up empty it and start over.
112 int cur_accum = 0;
113 int cur_bits = 0;
114 int curx;
115 int cury;
116 int free_ent = 0; // first unused entry
117 int g_init_bits;
118 int height;
119 int hsize = HSIZE; // for dynamic table sizing
120 int maxbits = BITS; // user settable max # bits/code
121 int maxcode; // maximum code, given n_bits
122 int maxmaxcode = 1 << BITS; // should NEVER generate this code
123 int n_bits; // number of bits/code
124 int width;
125 private boolean interlace = false;
126
127 //~ Constructors ///////////////////////////////////////////////////////////
128
129 /// Constructor from Image.
130 // @param img The image to encode.
131 // @param out The stream to write the GIF to.
132 public GifEncoder(Image img, OutputStream out) throws IOException
133 {
134 super(img, out);
135 }
136
137 /// Constructor from Image with interlace setting.
138 // @param img The image to encode.
139 // @param out The stream to write the GIF to.
140 // @param interlace Whether to interlace.
141 public GifEncoder(Image img, OutputStream out, boolean interlace)
142 throws IOException
143 {
144 super(img, out);
145 this.interlace = interlace;
146 }
147
148 /// Constructor from ImageProducer.
149 // @param prod The ImageProducer to encode.
150 // @param out The stream to write the GIF to.
151 public GifEncoder(ImageProducer prod, OutputStream out)
152 throws IOException
153 {
154 super(prod, out);
155 }
156
157 /// Constructor from ImageProducer with interlace setting.
158 // @param prod The ImageProducer to encode.
159 // @param out The stream to write the GIF to.
160 public GifEncoder(ImageProducer prod, OutputStream out, boolean interlace)
161 throws IOException
162 {
163 super(prod, out);
164 this.interlace = interlace;
165 }
166
167 //~ Methods ////////////////////////////////////////////////////////////////
168
169 byte GetPixel(int x, int y) throws IOException
170 {
171 GifEncoderHashitem item = (GifEncoderHashitem) colorHash.get(rgbPixels[y][x]);
172
173 if (item == null)
174 {
175 throw new IOException("color not found");
176 }
177
178 return (byte) item.index;
179 }
180
181 final int MAXCODE(int n_bits)
182 {
183 return (1 << n_bits) - 1;
184 }
185
186 // Write out a byte to the GIF file
187 void Putbyte(byte b, OutputStream outs) throws IOException
188 {
189 outs.write(b);
190 }
191
192 // Set up the 'byte output' routine
193 void char_init()
194 {
195 a_count = 0;
196 }
197
198 // Add a character to the end of the current packet, and if it is 254
199 // characters, flush the packet to disk.
200 void char_out(byte c, OutputStream outs) throws IOException
201 {
202 accum[a_count++] = c;
203
204 if (a_count >= 254)
205 {
206 flush_char(outs);
207 }
208 }
209
210 // Clear out the hash table
211 // table clear for block compress
212 void cl_block(OutputStream outs) throws IOException
213 {
214 cl_hash(hsize);
215 free_ent = ClearCode + 2;
216 clear_flg = true;
217
218 output(ClearCode, outs);
219 }
220
221 // reset code table
222 void cl_hash(int hsize)
223 {
224 for (int i = 0; i < hsize; ++i)
225 {
226 htab[i] = -1;
227 }
228 }
229
230 void compress(int init_bits, OutputStream outs) throws IOException
231 {
232 int fcode;
233 int i /* = 0 */;
234 int c;
235 int ent;
236 int disp;
237 int hsize_reg;
238 int hshift;
239
240 // Set up the globals: g_init_bits - initial number of bits
241 g_init_bits = init_bits;
242
243 // Set up the necessary values
244 clear_flg = false;
245 n_bits = g_init_bits;
246 maxcode = MAXCODE(n_bits);
247
248 ClearCode = 1 << (init_bits - 1);
249 EOFCode = ClearCode + 1;
250 free_ent = ClearCode + 2;
251
252 char_init();
253
254 ent = GIFNextPixel();
255
256 hshift = 0;
257
258 for (fcode = hsize; fcode < 65536; fcode *= 2)
259 {
260 ++hshift;
261 }
262
263 hshift = 8 - hshift; // set hash code range bound
264
265 hsize_reg = hsize;
266 cl_hash(hsize_reg); // clear hash table
267
268 output(ClearCode, outs);
269
270 outer_loop:
271 while ((c = GIFNextPixel()) != EOF)
272 {
273 fcode = (c << maxbits) + ent;
274 i = (c << hshift) ^ ent; // xor hashing
275
276 if (htab[i] == fcode)
277 {
278 ent = codetab[i];
279
280 continue;
281 }
282 else if (htab[i] >= 0) // non-empty slot
283 {
284 disp = hsize_reg - i; // secondary hash (after G. Knott)
285
286 if (i == 0)
287 {
288 disp = 1;
289 }
290
291 do
292 {
293 if ((i -= disp) < 0)
294 {
295 i += hsize_reg;
296 }
297
298 if (htab[i] == fcode)
299 {
300 ent = codetab[i];
301
302 continue outer_loop;
303 }
304 }
305 while (htab[i] >= 0);
306 }
307
308 output(ent, outs);
309 ent = c;
310
311 if (free_ent < maxmaxcode)
312 {
313 codetab[i] = free_ent++; // code -> hashtable
314 htab[i] = fcode;
315 }
316 else
317 {
318 cl_block(outs);
319 }
320 }
321
322 // Put out the final code.
323 output(ent, outs);
324 output(EOFCode, outs);
325 }
326
327 void encodeDone() throws IOException
328 {
329 int transparentIndex = -1;
330 int transparentRgb = -1;
331
332 // Put all the pixels into a hash table.
333 colorHash = new Acme.IntHashtable();
334
335 int index = 0;
336
337 for (int row = 0; row < height; ++row)
338 {
339 int rowOffset = row * width;
340
341 for (int col = 0; col < width; ++col)
342 {
343 int rgb = rgbPixels[row][col];
344 boolean isTransparent = ((rgb >>> 24) < 0x80);
345
346 if (isTransparent)
347 {
348 if (transparentIndex < 0)
349 {
350 // First transparent color; remember it.
351 transparentIndex = index;
352 transparentRgb = rgb;
353 }
354 else if (rgb != transparentRgb)
355 {
356 // A second transparent color; replace it with
357 // the first one.
358 rgbPixels[row][col] = rgb = transparentRgb;
359 }
360 }
361
362 GifEncoderHashitem item = (GifEncoderHashitem) colorHash.get(rgb);
363
364 if (item == null)
365 {
366 if (index >= 256)
367 {
368 throw new IOException("too many colors for a GIF");
369 }
370
371 item = new GifEncoderHashitem(rgb, 1, index, isTransparent);
372 ++index;
373 colorHash.put(rgb, item);
374 }
375 else
376 {
377 ++item.count;
378 }
379 }
380 }
381
382 // Figure out how many bits to use.
383 int logColors;
384
385 if (index <= 2)
386 {
387 logColors = 1;
388 }
389 else if (index <= 4)
390 {
391 logColors = 2;
392 }
393 else if (index <= 16)
394 {
395 logColors = 4;
396 }
397 else
398 {
399 logColors = 8;
400 }
401
402 // Turn colors into colormap entries.
403 int mapSize = 1 << logColors;
404 byte[] reds = new byte[mapSize];
405 byte[] grns = new byte[mapSize];
406 byte[] blus = new byte[mapSize];
407
408 for (Enumeration e = colorHash.elements(); e.hasMoreElements();)
409 {
410 GifEncoderHashitem item = (GifEncoderHashitem) e.nextElement();
411 reds[item.index] = (byte) ((item.rgb >> 16) & 0xff);
412 grns[item.index] = (byte) ((item.rgb >> 8) & 0xff);
413 blus[item.index] = (byte) (item.rgb & 0xff);
414 }
415
416 GIFEncode(out, width, height, interlace, (byte) 0, transparentIndex,
417 logColors, reds, grns, blus);
418 }
419
420 void encodePixels(int x, int y, int w, int h, int[] rgbPixels, int off,
421 int scansize) throws IOException
422 {
423 // Save the pixels.
424 for (int row = 0; row < h; ++row)
425 {
426 System.arraycopy(rgbPixels, (row * scansize) + off,
427 this.rgbPixels[y + row], x, w);
428 }
429 }
430
431 void encodeStart(int width, int height) throws IOException
432 {
433 this.width = width;
434 this.height = height;
435 rgbPixels = new int[height][width];
436 }
437
438 static void writeString(OutputStream out, String str)
439 throws IOException
440 {
441 byte[] buf = str.getBytes();
442 out.write(buf);
443 }
444
445 // Bump the 'curx' and 'cury' to point to the next pixel
446 void BumpPixel()
447 {
448 ++curx;
449
450 // If we are at the end of a scan line, set curx back to the beginning
451 // If we are interlaced, bump the cury to the appropriate spot,
452 // otherwise, just increment it.
453 if (curx == Width)
454 {
455 curx = 0;
456
457 if (!Interlace)
458 {
459 ++cury;
460 }
461 else
462 {
463 switch (Pass)
464 {
465 case 0:
466 cury += 8;
467
468 if (cury >= Height)
469 {
470 ++Pass;
471 cury = 4;
472 }
473
474 break;
475
476 case 1:
477 cury += 8;
478
479 if (cury >= Height)
480 {
481 ++Pass;
482 cury = 2;
483 }
484
485 break;
486
487 case 2:
488 cury += 4;
489
490 if (cury >= Height)
491 {
492 ++Pass;
493 cury = 1;
494 }
495
496 break;
497
498 case 3:
499 cury += 2;
500
501 break;
502 }
503 }
504 }
505 }
506
507 void GIFEncode(OutputStream outs, int Width, int Height, boolean Interlace,
508 byte Background, int Transparent, int BitsPerPixel, byte[] Red,
509 byte[] Green, byte[] Blue) throws IOException
510 {
511 byte B;
512 int LeftOfs;
513 int TopOfs;
514 int ColorMapSize;
515 int InitCodeSize;
516 int i;
517
518 this.Width = Width;
519 this.Height = Height;
520 this.Interlace = Interlace;
521 ColorMapSize = 1 << BitsPerPixel;
522 LeftOfs = TopOfs = 0;
523
524 // Calculate number of bits we are expecting
525 CountDown = Width * Height;
526
527 // Indicate which pass we are on (if interlace)
528 Pass = 0;
529
530 // The initial code size
531 if (BitsPerPixel <= 1)
532 {
533 InitCodeSize = 2;
534 }
535 else
536 {
537 InitCodeSize = BitsPerPixel;
538 }
539
540 // Set up the current x and y position
541 curx = 0;
542 cury = 0;
543
544 // Write the Magic header
545 writeString(outs, "GIF89a");
546
547 // Write out the screen width and height
548 Putword(Width, outs);
549 Putword(Height, outs);
550
551 // Indicate that there is a global colour map
552 B = (byte) 0x80; // Yes, there is a color map
553
554 // OR in the resolution
555 B |= (byte) ((8 - 1) << 4);
556
557 // Not sorted
558 // OR in the Bits per Pixel
559 B |= (byte) ((BitsPerPixel - 1));
560
561 // Write it out
562 Putbyte(B, outs);
563
564 // Write out the Background colour
565 Putbyte(Background, outs);
566
567 // Pixel aspect ratio - 1:1.
568 //Putbyte( (byte) 49, outs );
569 // Java's GIF reader currently has a bug, if the aspect ratio byte is
570 // not zero it throws an ImageFormatException. It doesn't know that
571 // 49 means a 1:1 aspect ratio. Well, whatever, zero works with all
572 // the other decoders I've tried so it probably doesn't hurt.
573 Putbyte((byte) 0, outs);
574
575 // Write out the Global Colour Map
576 for (i = 0; i < ColorMapSize; ++i)
577 {
578 Putbyte(Red[i], outs);
579 Putbyte(Green[i], outs);
580 Putbyte(Blue[i], outs);
581 }
582
583 // Write out extension for transparent colour index, if necessary.
584 if (Transparent != -1)
585 {
586 Putbyte((byte) '!', outs);
587 Putbyte((byte) 0xf9, outs);
588 Putbyte((byte) 4, outs);
589 Putbyte((byte) 1, outs);
590 Putbyte((byte) 0, outs);
591 Putbyte((byte) 0, outs);
592 Putbyte((byte) Transparent, outs);
593 Putbyte((byte) 0, outs);
594 }
595
596 // Write an Image separator
597 Putbyte((byte) ',', outs);
598
599 // Write the Image header
600 Putword(LeftOfs, outs);
601 Putword(TopOfs, outs);
602 Putword(Width, outs);
603 Putword(Height, outs);
604
605 // Write out whether or not the image is interlaced
606 if (Interlace)
607 {
608 Putbyte((byte) 0x40, outs);
609 }
610 else
611 {
612 Putbyte((byte) 0x00, outs);
613 }
614
615 // Write out the initial code size
616 Putbyte((byte) InitCodeSize, outs);
617
618 // Go and actually compress the data
619 compress(InitCodeSize + 1, outs);
620
621 // Write out a Zero-length packet (to end the series)
622 Putbyte((byte) 0, outs);
623
624 // Write the GIF file terminator
625 Putbyte((byte) ';', outs);
626 }
627
628 // Return the next pixel from the image
629 int GIFNextPixel() throws IOException
630 {
631 byte r;
632
633 if (CountDown == 0)
634 {
635 return EOF;
636 }
637
638 --CountDown;
639
640 r = GetPixel(curx, cury);
641
642 BumpPixel();
643
644 return r & 0xff;
645 }
646
647 // Write out a word to the GIF file
648 void Putword(int w, OutputStream outs) throws IOException
649 {
650 Putbyte((byte) (w & 0xff), outs);
651 Putbyte((byte) ((w >> 8) & 0xff), outs);
652 }
653
654 // Flush the packet to disk, and reset the accumulator
655 void flush_char(OutputStream outs) throws IOException
656 {
657 if (a_count > 0)
658 {
659 outs.write(a_count);
660 outs.write(accum, 0, a_count);
661 a_count = 0;
662 }
663 }
664
665 void output(int code, OutputStream outs) throws IOException
666 {
667 cur_accum &= masks[cur_bits];
668
669 if (cur_bits > 0)
670 {
671 cur_accum |= (code << cur_bits);
672 }
673 else
674 {
675 cur_accum = code;
676 }
677
678 cur_bits += n_bits;
679
680 while (cur_bits >= 8)
681 {
682 char_out((byte) (cur_accum & 0xff), outs);
683 cur_accum >>= 8;
684 cur_bits -= 8;
685 }
686
687 // If the next entry is going to be too big for the code size,
688 // then increase it, if possible.
689 if ((free_ent > maxcode) || clear_flg)
690 {
691 if (clear_flg)
692 {
693 maxcode = MAXCODE(n_bits = g_init_bits);
694 clear_flg = false;
695 }
696 else
697 {
698 ++n_bits;
699
700 if (n_bits == maxbits)
701 {
702 maxcode = maxmaxcode;
703 }
704 else
705 {
706 maxcode = MAXCODE(n_bits);
707 }
708 }
709 }
710
711 if (code == EOFCode)
712 {
713 // At EOF, write the rest of the buffer.
714 while (cur_bits > 0)
715 {
716 char_out((byte) (cur_accum & 0xff), outs);
717 cur_accum >>= 8;
718 cur_bits -= 8;
719 }
720
721 flush_char(outs);
722 }
723 }
724 }
725
726 class GifEncoderHashitem
727 {
728 //~ Instance fields ////////////////////////////////////////////////////////
729
730 public boolean isTransparent;
731 public int count;
732 public int index;
733 public int rgb;
734
735 //~ Constructors ///////////////////////////////////////////////////////////
736
737 public GifEncoderHashitem(int rgb, int count, int index,
738 boolean isTransparent)
739 {
740 this.rgb = rgb;
741 this.count = count;
742 this.index = index;
743 this.isTransparent = isTransparent;
744 }
745 }
746 ///////////////////////////////////////////////////////////////////////////////
747 // END OF FILE.
748 ///////////////////////////////////////////////////////////////////////////////