1 package org.catacomb.movie.gif;
2
3
4
5 import java.awt.Color;
6 import java.awt.Graphics2D;
7 import java.awt.image.BufferedImage;
8 import java.awt.image.DataBufferByte;
9 import java.io.*;
10
11 /**
12 * Class AnimatedGifEncoder - Encodes a GIF file consisting of one or
13 * more frames.
14 * <pre>
15 * Example:
16 * AnimatedGifEncoder e = new AnimatedGifEncoder();
17 * e.start(outputFileName);
18 * e.setDelay(1000); // 1 frame per sec
19 * e.addFrame(image1);
20 * e.addFrame(image2);
21 * e.finish();
22 * </pre>
23 * No copyright asserted on the source code of this class. May be used
24 * for any purpose, however, refer to the Unisys LZW patent for restrictions
25 * on use of the associated LZWEncoder class. Please forward any corrections
26 * to kweiner@fmsware.com.
27 *
28 * @author Kevin Weiner, FM Software
29 * @version 1.03 November 2003
30 *
31 */
32
33 public class AnimatedGifEncoder {
34
35 protected int width; // image size
36 protected int height;
37 protected Color transparent = null; // transparent color if given
38 protected int transIndex; // transparent index in color table
39 protected int repeat = -1; // no repeat
40 protected int delay = 0; // frame delay (hundredths)
41 protected boolean started = false; // ready to output frames
42 protected OutputStream out;
43 protected BufferedImage image; // current frame
44 protected byte[] pixels; // BGR byte array from frame
45 protected byte[] indexedPixels; // converted frame indexed to palette
46 protected int colorDepth; // number of bit planes
47 protected byte[] colorTab; // RGB palette
48 protected boolean[] usedEntry = new boolean[256]; // active palette entries
49 protected int palSize = 7; // color table size (bits-1)
50 protected int dispose = -1; // disposal code (-1 = use default)
51 protected boolean closeStream = false; // close stream when finished
52 protected boolean firstFrame = true;
53 protected boolean sizeSet = false; // if false, get size from first frame
54 protected int sample = 10; // default sample interval for quantizer
55
56 /**
57 * Sets the delay time between each frame, or changes it
58 * for subsequent frames (applies to last frame added).
59 *
60 * @param ms int delay time in milliseconds
61 */
62 public void setDelay(int ms) {
63 delay = Math.round(ms / 10.0f);
64 }
65
66 /**
67 * Sets the GIF frame disposal code for the last added frame
68 * and any subsequent frames. Default is 0 if no transparent
69 * color has been set, otherwise 2.
70 * @param code int disposal code.
71 */
72 public void setDispose(int code) {
73 if (code >= 0) {
74 dispose = code;
75 }
76 }
77
78 /**
79 * Sets the number of times the set of GIF frames
80 * should be played. Default is 1; 0 means play
81 * indefinitely. Must be invoked before the first
82 * image is added.
83 *
84 * @param iter int number of iterations.
85 * @return
86 */
87 public void setRepeat(int iter) {
88 if (iter >= 0) {
89 repeat = iter;
90 }
91 }
92
93 /**
94 * Sets the transparent color for the last added frame
95 * and any subsequent frames.
96 * Since all colors are subject to modification
97 * in the quantization process, the color in the final
98 * palette for each frame closest to the given color
99 * becomes the transparent color for that frame.
100 * May be set to null to indicate no transparent color.
101 *
102 * @param c Color to be treated as transparent on display.
103 */
104 public void setTransparent(Color c) {
105 transparent = c;
106 }
107
108 /**
109 * Adds next GIF frame. The frame is not written immediately, but is
110 * actually deferred until the next frame is received so that timing
111 * data can be inserted. Invoking <code>finish()</code> flushes all
112 * frames. If <code>setSize</code> was not invoked, the size of the
113 * first image is used for all subsequent frames.
114 *
115 * @param im BufferedImage containing frame to write.
116 * @return true if successful.
117 */
118 public boolean addFrame(BufferedImage im) {
119 if ((im == null) || !started) {
120 return false;
121 }
122 boolean ok = true;
123 try {
124 if (!sizeSet) {
125 // use first frame's size
126 setSize(im.getWidth(), im.getHeight());
127 }
128 image = im;
129 getImagePixels(); // convert to correct format if necessary
130 analyzePixels(); // build color table & map pixels
131 if (firstFrame) {
132 writeLSD(); // logical screen descriptior
133 writePalette(); // global color table
134 if (repeat >= 0) {
135 // use NS app extension to indicate reps
136 writeNetscapeExt();
137 }
138 }
139 writeGraphicCtrlExt(); // write graphic control extension
140 writeImageDesc(); // image descriptor
141 if (!firstFrame) {
142 writePalette(); // local color table
143 }
144 writePixels(); // encode and write pixel data
145 firstFrame = false;
146 } catch (IOException e) {
147 ok = false;
148 }
149
150 return ok;
151 }
152
153 /**
154 * Flushes any pending data and closes output file.
155 * If writing to an OutputStream, the stream is not
156 * closed.
157 */
158 public boolean finish() {
159 if (!started) {
160 return false;
161 }
162 boolean ok = true;
163 started = false;
164 try {
165 out.write(0x3b); // gif trailer
166 out.flush();
167 if (closeStream) {
168 out.close();
169 }
170 } catch (IOException e) {
171 ok = false;
172 }
173
174 // reset for subsequent use
175 transIndex = 0;
176 out = null;
177 image = null;
178 pixels = null;
179 indexedPixels = null;
180 colorTab = null;
181 closeStream = false;
182 firstFrame = true;
183
184 return ok;
185 }
186
187 /**
188 * Sets frame rate in frames per second. Equivalent to
189 * <code>setDelay(1000/fps)</code>.
190 *
191 * @param fps float frame rate (frames per second)
192 */
193 public void setFrameRate(float fps) {
194 if (fps != 0f) {
195 delay = Math.round(100f / fps);
196 }
197 }
198
199 /**
200 * Sets quality of color quantization (conversion of images
201 * to the maximum 256 colors allowed by the GIF specification).
202 * Lower values (minimum = 1) produce better colors, but slow
203 * processing significantly. 10 is the default, and produces
204 * good color mapping at reasonable speeds. Values greater
205 * than 20 do not yield significant improvements in speed.
206 *
207 * @param quality int greater than 0.
208 * @return
209 */
210 public void setQuality(int q) {
211
212 if (q >= 1) {
213 sample = q;
214 } else {
215 sample = 1;
216 }
217 }
218
219 /**
220 * Sets the GIF frame size. The default size is the
221 * size of the first frame added if this method is
222 * not invoked.
223 *
224 * @param w int frame width.
225 * @param h int frame width.
226 */
227 public void setSize(int w, int h) {
228 if (started && !firstFrame) {
229 return;
230 }
231 width = w;
232 height = h;
233 if (width < 1) {
234 width = 320;
235 }
236 if (height < 1) {
237 height = 240;
238 }
239 sizeSet = true;
240 }
241
242 /**
243 * Initiates GIF file creation on the given stream. The stream
244 * is not closed automatically.
245 *
246 * @param os OutputStream on which GIF images are written.
247 * @return false if initial write failed.
248 */
249 public boolean start(OutputStream os) {
250 if (os == null) {
251 return false;
252 }
253 boolean ok = true;
254 closeStream = false;
255 out = os;
256 try {
257 writeString("GIF89a"); // header
258 } catch (IOException e) {
259 ok = false;
260 }
261 return started = ok;
262 }
263
264 /**
265 * Initiates writing of a GIF file with the specified name.
266 *
267 * @param file String containing output file name.
268 * @return false if open or initial write failed.
269 */
270 public boolean start(File file) {
271 boolean ok = true;
272 try {
273 out = new BufferedOutputStream(new FileOutputStream(file));
274 ok = start(out);
275 closeStream = true;
276 } catch (IOException e) {
277 ok = false;
278 }
279 return started = ok;
280 }
281
282 /**
283 * Analyzes image colors and creates color map.
284 */
285 protected void analyzePixels() {
286 int len = pixels.length;
287 int nPix = len / 3;
288 indexedPixels = new byte[nPix];
289 NeuQuant nq = new NeuQuant(pixels, len, sample);
290 // initialize quantizer
291 colorTab = nq.process(); // create reduced palette
292 // convert map from BGR to RGB
293 for (int i = 0; i < colorTab.length; i += 3) {
294 byte temp = colorTab[i];
295 colorTab[i] = colorTab[i + 2];
296 colorTab[i + 2] = temp;
297 usedEntry[i / 3] = false;
298 }
299 // map image pixels to new palette
300 int k = 0;
301 for (int i = 0; i < nPix; i++) {
302 int index =
303 nq.map(pixels[k++] & 0xff,
304 pixels[k++] & 0xff,
305 pixels[k++] & 0xff);
306 usedEntry[index] = true;
307 indexedPixels[i] = (byte) index;
308 }
309 pixels = null;
310 colorDepth = 8;
311 palSize = 7;
312 // get closest match to transparent color if specified
313 if (transparent != null) {
314 transIndex = findClosest(transparent);
315 }
316 }
317
318 /**
319 * Returns index of palette color closest to c
320 *
321 */
322 protected int findClosest(Color c) {
323 if (colorTab == null) {
324 return -1;
325 }
326 int r = c.getRed();
327 int g = c.getGreen();
328 int b = c.getBlue();
329 int minpos = 0;
330 int dmin = 256 * 256 * 256;
331 int len = colorTab.length;
332 for (int i = 0; i < len;) {
333 int dr = r - (colorTab[i++] & 0xff);
334 int dg = g - (colorTab[i++] & 0xff);
335 int db = b - (colorTab[i] & 0xff);
336 int d = dr * dr + dg * dg + db * db;
337 int index = i / 3;
338 if (usedEntry[index] && (d < dmin)) {
339 dmin = d;
340 minpos = index;
341 }
342 i++;
343 }
344 return minpos;
345 }
346
347 /**
348 * Extracts image pixels into byte array "pixels"
349 */
350 protected void getImagePixels() {
351 int w = image.getWidth();
352 int h = image.getHeight();
353 int type = image.getType();
354 if ((w != width)
355 || (h != height)
356 || (type != BufferedImage.TYPE_3BYTE_BGR)) {
357 // create new image with right size/format
358 BufferedImage temp =
359 new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
360 Graphics2D g = temp.createGraphics();
361 g.drawImage(image, 0, 0, null);
362 image = temp;
363 }
364 pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
365 }
366
367 /**
368 * Writes Graphic Control Extension
369 */
370 protected void writeGraphicCtrlExt() throws IOException {
371 out.write(0x21); // extension introducer
372 out.write(0xf9); // GCE label
373 out.write(4); // data block size
374 int transp, disp;
375 if (transparent == null) {
376 transp = 0;
377 disp = 0; // dispose = no action
378 } else {
379 transp = 1;
380 disp = 2; // force clear if using transparent color
381 }
382 if (dispose >= 0) {
383 disp = dispose & 7; // user override
384 }
385 disp <<= 2;
386
387 // packed fields
388 out.write(0 | // 1:3 reserved
389 disp | // 4:6 disposal
390 0 | // 7 user input - 0 = none
391 transp); // 8 transparency flag
392
393 writeShort(delay); // delay x 1/100 sec
394 out.write(transIndex); // transparent color index
395 out.write(0); // block terminator
396 }
397
398 /**
399 * Writes Image Descriptor
400 */
401 protected void writeImageDesc() throws IOException {
402 out.write(0x2c); // image separator
403 writeShort(0); // image position x,y = 0,0
404 writeShort(0);
405 writeShort(width); // image size
406 writeShort(height);
407 // packed fields
408 if (firstFrame) {
409 // no LCT - GCT is used for first (or only) frame
410 out.write(0);
411 } else {
412 // specify normal LCT
413 out.write(0x80 | // 1 local color table 1=yes
414 0 | // 2 interlace - 0=no
415 0 | // 3 sorted - 0=no
416 0 | // 4-5 reserved
417 palSize); // 6-8 size of color table
418 }
419 }
420
421 /**
422 * Writes Logical Screen Descriptor
423 */
424 protected void writeLSD() throws IOException {
425 // logical screen size
426 writeShort(width);
427 writeShort(height);
428 // packed fields
429 out.write((0x80 | // 1 : global color table flag = 1 (gct used)
430 0x70 | // 2-4 : color resolution = 7
431 0x00 | // 5 : gct sort flag = 0
432 palSize)); // 6-8 : gct size
433
434 out.write(0); // background color index
435 out.write(0); // pixel aspect ratio - assume 1:1
436 }
437
438 /**
439 * Writes Netscape application extension to define
440 * repeat count.
441 */
442 protected void writeNetscapeExt() throws IOException {
443 out.write(0x21); // extension introducer
444 out.write(0xff); // app extension label
445 out.write(11); // block size
446 writeString("NETSCAPE" + "2.0"); // app id + auth code
447 out.write(3); // sub-block size
448 out.write(1); // loop sub-block id
449 writeShort(repeat); // loop count (extra iterations, 0=repeat forever)
450 out.write(0); // block terminator
451 }
452
453 /**
454 * Writes color table
455 */
456 protected void writePalette() throws IOException {
457 out.write(colorTab, 0, colorTab.length);
458 int n = (3 * 256) - colorTab.length;
459 for (int i = 0; i < n; i++) {
460 out.write(0);
461 }
462 }
463
464 /**
465 * Encodes and writes pixel data
466 */
467 protected void writePixels() throws IOException {
468 LZWEncoder encoder =
469 new LZWEncoder(width, height, indexedPixels, colorDepth);
470 encoder.encode(out);
471 }
472
473 /**
474 * Write 16-bit value to output stream, LSB first
475 */
476 protected void writeShort(int value) throws IOException {
477 out.write(value & 0xff);
478 out.write((value >> 8) & 0xff);
479 }
480
481 /**
482 * Writes string to output stream
483 */
484 protected void writeString(String s) throws IOException {
485 for (int i = 0; i < s.length(); i++) {
486 out.write((byte) s.charAt(i));
487 }
488 }
489 }