1
2
3
4
5
6 package net.wotonomy.ui.swing.util;
7
8 import java.awt.AWTException;
9 import java.awt.Image;
10 import java.awt.image.PixelGrabber;
11 import java.io.IOException;
12 import java.io.OutputStream;
13
14 /***
15 * GIFEncoder is a class which takes an image and saves it to a stream
16 * using the GIF file format. A GIFEncoder is constructed with either
17 * an AWT Image (which must be fully loaded) or a set of RGB arrays.
18 * The image can be written out with a call to <CODE>write</CODE>.<br><br>
19 *
20 * Three caveats:
21 * <UL>
22 * <LI>GIFEncoder will convert the image to indexed color upon
23 * construction, and this is not fast.
24 *
25 * <LI>The image cannot have more than 256 colors, since GIF is an 8
26 * bit format.
27 *
28 * <LI>Since the image must be completely loaded into memory,
29 * there may be problems with large images.
30 * </UL>
31 *
32 * This implementation is heavily based on code made available by
33 * Adam Doppelt, which was based upon gifsave.c, which was written
34 * and released by Sverre H. Huseby.
35 *
36 * @author amd@brown.edu
37 * @author sverrehu@ifi.uio.no
38 * @author michael@mpowers.net
39 */
40 public class GIFEncoder
41 {
42 short width_, height_;
43 int numColors_;
44 byte pixels_[], colors_[];
45
46 ScreenDescriptor sd_;
47 ImageDescriptor id_;
48
49 /***
50 * Construct a GIFEncoder. The constructor will convert the image to
51 * an indexed color array. This may take some time. If more than 256
52 * colors are encountered, all subsequent colors are mapped to the first
53 * color encountered.
54 * @param image The image to encode. The image must be completely loaded.
55 * @exception AWTException Will be thrown if the pixel grab fails. This
56 * can happen if Java runs out of memory.
57 * */
58 public GIFEncoder(Image image) throws AWTException
59 {
60 width_ = (short)image.getWidth(null);
61 height_ = (short)image.getHeight(null);
62
63 int values[] = new int[width_ * height_];
64 PixelGrabber grabber = new PixelGrabber(
65 image, 0, 0, width_, height_, values, 0, width_);
66
67 try
68 {
69 if(grabber.grabPixels() != true)
70 throw new AWTException("Grabber returned false: " +
71 grabber.status());
72 }
73 catch (InterruptedException e)
74 {
75 }
76
77 byte r[][] = new byte[width_][height_];
78 byte g[][] = new byte[width_][height_];
79 byte b[][] = new byte[width_][height_];
80 int index = 0;
81 for (int y = 0; y < height_; ++y)
82 {
83 for (int x = 0; x < width_; ++x)
84 {
85 r[x][y] = (byte)((values[index] >> 16) & 0xFF);
86 g[x][y] = (byte)((values[index] >> 8) & 0xFF);
87 b[x][y] = (byte)((values[index]) & 0xFF);
88 ++index;
89 }
90 }
91 toIndexedColor(r, g, b);
92 }
93
94 /***
95 * Construct a GIFEncoder. The constructor will convert the image to
96 * an indexed color array. This may take some time. <br><br>
97 * Each array stores intensity values for the image. In other words,
98 * r[x][y] refers to the red intensity of the pixel at column x, row y.
99 * @param r An array containing the red intensity values.
100 * @param g An array containing the green intensity values.
101 * @param b An array containing the blue intensity values.
102 *
103 * @exception AWTException Will be thrown if the image contains more than
104 * 256 colors.
105 * */
106 public GIFEncoder(byte r[][], byte g[][], byte b[][]) throws AWTException
107 {
108 width_ = (short)(r.length);
109 height_ = (short)(r[0].length);
110
111 toIndexedColor(r, g, b);
112 }
113
114 /***
115 * Writes the image out to a stream in the GIF file format. This will
116 * be a single GIF87a image, non-interlaced, with no background color.
117 * <B>This may take some time.</B><P>
118 *
119 * @param output The stream to output to. This should probably be a
120 * buffered stream.
121 *
122 * @exception IOException Will be thrown if a write operation fails.
123 * */
124 public void write(OutputStream output) throws IOException
125 {
126 BitUtils.writeString(output, "GIF87a");
127 ScreenDescriptor sd = new ScreenDescriptor(width_, height_,
128 numColors_);
129 sd.write(output);
130
131 output.write(colors_, 0, colors_.length);
132
133 ImageDescriptor id = new ImageDescriptor(width_, height_, ',');
134 id.write(output);
135
136 byte codesize = BitUtils.bitsNeeded(numColors_);
137 if (codesize == 1)
138 ++codesize;
139 output.write(codesize);
140
141 LZWCompressor.LZWCompress(output, codesize, pixels_);
142 output.write(0);
143 id = new ImageDescriptor((byte)0, (byte)0, ';');
144 id.write(output);
145 output.flush();
146 }
147
148 void toIndexedColor(byte r[][], byte g[][],
149 byte b[][]) throws AWTException
150 {
151 pixels_ = new byte[width_ * height_];
152 colors_ = new byte[256 * 3];
153 int colornum = 0;
154 for (int x = 0; x < width_; ++x)
155 {
156 for (int y = 0; y < height_; ++y)
157 {
158 int search;
159 for (search = 0; search < colornum; ++search)
160 if (colors_[search * 3] == r[x][y] &&
161 colors_[search * 3 + 1] == g[x][y] &&
162 colors_[search * 3 + 2] == b[x][y])
163 break;
164
165 if (search > 255)
166 search = 0;
167
168
169 pixels_[y * width_ + x] = (byte)search;
170
171 if (search == colornum) {
172 colors_[search * 3] = r[x][y];
173 colors_[search * 3 + 1] = g[x][y];
174 colors_[search * 3 + 2] = b[x][y];
175 ++colornum;
176 }
177 }
178 }
179
180 numColors_ = 1 << BitUtils.bitsNeeded(colornum);
181 byte copy[] = new byte[numColors_ * 3];
182 System.arraycopy(colors_, 0, copy, 0, numColors_ * 3);
183 colors_ = copy;
184 }
185
186 }
187
188 class BitFile
189 {
190 OutputStream output_;
191 byte buffer_[];
192 int index_, bitsLeft_;
193
194 public BitFile(OutputStream output)
195 {
196 output_ = output;
197 buffer_ = new byte[256];
198 index_ = 0;
199 bitsLeft_ = 8;
200 }
201
202 public void flush() throws IOException
203 {
204 int numBytes = index_ + (bitsLeft_ == 8 ? 0 : 1);
205 if (numBytes > 0)
206 {
207 output_.write(numBytes);
208 output_.write(buffer_, 0, numBytes);
209 buffer_[0] = 0;
210 index_ = 0;
211 bitsLeft_ = 8;
212 }
213 }
214
215 public void writeBits(int bits, int numbits) throws IOException {
216 int bitsWritten = 0;
217 int numBytes = 255;
218 do
219 {
220 if ((index_ == 254 && bitsLeft_ == 0) || index_ > 254)
221 {
222 output_.write(numBytes);
223 output_.write(buffer_, 0, numBytes);
224
225 buffer_[0] = 0;
226 index_ = 0;
227 bitsLeft_ = 8;
228 }
229
230 if (numbits <= bitsLeft_)
231 {
232 buffer_[index_] |= (bits & ((1 << numbits) - 1)) <<
233 (8 - bitsLeft_);
234 bitsWritten += numbits;
235 bitsLeft_ -= numbits;
236 numbits = 0;
237 }
238 else
239 {
240 buffer_[index_] |= (bits & ((1 << bitsLeft_) - 1)) <<
241 (8 - bitsLeft_);
242 bitsWritten += bitsLeft_;
243 bits >>= bitsLeft_;
244 numbits -= bitsLeft_;
245 buffer_[++index_] = 0;
246 bitsLeft_ = 8;
247 }
248
249 }
250 while (numbits != 0);
251 }
252 }
253
254 class LZWStringTable
255 {
256 private final static int RES_CODES = 2;
257 private final static short HASH_FREE = (short)0xFFFF;
258 private final static short NEXT_FIRST = (short)0xFFFF;
259 private final static int MAXBITS = 12;
260 private final static int MAXSTR = (1 << MAXBITS);
261 private final static short HASHSIZE = 9973;
262 private final static short HASHSTEP = 2039;
263
264 byte strChr_[];
265 short strNxt_[];
266 short strHsh_[];
267 short numStrings_;
268
269 public LZWStringTable()
270 {
271 strChr_ = new byte[MAXSTR];
272 strNxt_ = new short[MAXSTR];
273 strHsh_ = new short[HASHSIZE];
274 }
275
276 public int addCharString(short index, byte b)
277 {
278 int hshidx;
279
280 if (numStrings_ >= MAXSTR)
281 return 0xFFFF;
282
283 hshidx = Hash(index, b);
284 while (strHsh_[hshidx] != HASH_FREE)
285 hshidx = (hshidx + HASHSTEP) % HASHSIZE;
286
287 strHsh_[hshidx] = numStrings_;
288 strChr_[numStrings_] = b;
289 strNxt_[numStrings_] = (index != HASH_FREE) ? index : NEXT_FIRST;
290
291 return numStrings_++;
292 }
293
294 public short findCharString(short index, byte b)
295 {
296 int hshidx, nxtidx;
297
298 if (index == HASH_FREE)
299 return b;
300
301 hshidx = Hash(index, b);
302 while ((nxtidx = strHsh_[hshidx]) != HASH_FREE)
303 {
304 if (strNxt_[nxtidx] == index && strChr_[nxtidx] == b)
305 return (short)nxtidx;
306 hshidx = (hshidx + HASHSTEP) % HASHSIZE;
307 }
308
309 return (short)0xFFFF;
310 }
311
312 public void clearTable(int codesize)
313 {
314 numStrings_ = 0;
315
316 for (int q = 0; q < HASHSIZE; q++)
317 {
318 strHsh_[q] = HASH_FREE;
319 }
320
321 int w = (1 << codesize) + RES_CODES;
322 for (int q = 0; q < w; q++)
323 {
324 addCharString((short)0xFFFF, (byte)q);
325 }
326 }
327
328 static public int Hash(short index, byte lastbyte)
329 {
330 return ((int)((short)(lastbyte << 8) ^ index) & 0xFFFF) % HASHSIZE;
331 }
332 }
333
334 class LZWCompressor {
335
336 public static void LZWCompress(OutputStream output, int codesize,
337 byte toCompress[]) throws IOException
338 {
339 byte c;
340 short index;
341 int clearcode, endofinfo, numbits, limit, errcode;
342 short prefix = (short)0xFFFF;
343
344 BitFile bitFile = new BitFile(output);
345 LZWStringTable strings = new LZWStringTable();
346
347 clearcode = 1 << codesize;
348 endofinfo = clearcode + 1;
349
350 numbits = codesize + 1;
351 limit = (1 << numbits) - 1;
352 strings.clearTable(codesize);
353 bitFile.writeBits(clearcode, numbits);
354
355 for (int loop = 0; loop < toCompress.length; ++loop)
356 {
357 c = toCompress[loop];
358 if ((index = strings.findCharString(prefix, c)) != -1)
359 {
360 prefix = index;
361 }
362 else
363 {
364 bitFile.writeBits(prefix, numbits);
365 if (strings.addCharString(prefix, c) > limit) {
366 if (++numbits > 12) {
367 bitFile.writeBits(clearcode, numbits - 1);
368 strings.clearTable(codesize);
369 numbits = codesize + 1;
370 }
371 limit = (1 << numbits) - 1;
372 }
373
374 prefix = (short)((short)c & 0xFF);
375 }
376 }
377
378 if (prefix != -1)
379 bitFile.writeBits(prefix, numbits);
380
381 bitFile.writeBits(endofinfo, numbits);
382 bitFile.flush();
383 }
384 }
385
386 class ScreenDescriptor
387 {
388 public short localScreenWidth_, localScreenHeight_;
389 private byte byte_;
390 public byte backgroundColorIndex_, pixelAspectRatio_;
391
392 public ScreenDescriptor(short width, short height, int numColors)
393 {
394 localScreenWidth_ = width;
395 localScreenHeight_ = height;
396 setGlobalColorTableSize((byte)(BitUtils.bitsNeeded(numColors) - 1));
397 setGlobalColorTableFlag((byte)1);
398 setSortFlag((byte)0);
399 setColorResolution((byte)7);
400 backgroundColorIndex_ = 0;
401 pixelAspectRatio_ = 0;
402 }
403
404 public void write(OutputStream output) throws IOException
405 {
406 BitUtils.writeWord(output, localScreenWidth_);
407 BitUtils.writeWord(output, localScreenHeight_);
408 output.write(byte_);
409 output.write(backgroundColorIndex_);
410 output.write(pixelAspectRatio_);
411 }
412
413 public void setGlobalColorTableSize(byte num)
414 {
415 byte_ |= (num & 7);
416 }
417
418 public void setSortFlag(byte num)
419 {
420 byte_ |= (num & 1) << 3;
421 }
422
423 public void setColorResolution(byte num)
424 {
425 byte_ |= (num & 7) << 4;
426 }
427
428 public void setGlobalColorTableFlag(byte num)
429 {
430 byte_ |= (num & 1) << 7;
431 }
432 }
433
434 class ImageDescriptor
435 {
436 public byte separator_;
437 public short leftPosition_, topPosition_, width_, height_;
438 private byte byte_;
439
440 public ImageDescriptor(short width, short height, char separator)
441 {
442 separator_ = (byte)separator;
443 leftPosition_ = 0;
444 topPosition_ = 0;
445 width_ = width;
446 height_ = height;
447 setLocalColorTableSize((byte)0);
448 setReserved((byte)0);
449 setSortFlag((byte)0);
450 setInterlaceFlag((byte)0);
451 setLocalColorTableFlag((byte)0);
452 }
453
454 public void write(OutputStream output) throws IOException
455 {
456 output.write(separator_);
457 BitUtils.writeWord(output, leftPosition_);
458 BitUtils.writeWord(output, topPosition_);
459 BitUtils.writeWord(output, width_);
460 BitUtils.writeWord(output, height_);
461 output.write(byte_);
462 }
463
464 public void setLocalColorTableSize(byte num)
465 {
466 byte_ |= (num & 7);
467 }
468
469 public void setReserved(byte num)
470 {
471 byte_ |= (num & 3) << 3;
472 }
473
474 public void setSortFlag(byte num)
475 {
476 byte_ |= (num & 1) << 5;
477 }
478
479 public void setInterlaceFlag(byte num)
480 {
481 byte_ |= (num & 1) << 6;
482 }
483
484 public void setLocalColorTableFlag(byte num)
485 {
486 byte_ |= (num & 1) << 7;
487 }
488 }
489
490 class BitUtils
491 {
492 public static byte bitsNeeded(int n)
493 {
494 byte ret = 1;
495
496 if (n-- == 0)
497 return 0;
498
499 while ((n >>= 1) != 0)
500 ++ret;
501
502 return ret;
503 }
504
505 public static void writeWord(OutputStream output,
506 short w) throws IOException
507 {
508 output.write(w & 0xFF);
509 output.write((w >> 8) & 0xFF);
510 }
511
512 static void writeString(OutputStream output,
513 String string) throws IOException
514 {
515 for (int loop = 0; loop < string.length(); ++loop)
516 output.write((byte)(string.charAt(loop)));
517 }
518 }
519
520