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