View Javadoc

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 }