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