/Users/lyon/j4p/src/ip/gif/neuquantAnimation/AnimatedGifEncoder.java

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