001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas/ 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see https://www.gnu.org/licenses/. 027 */ 028package edu.wisc.ssec.mcidasv.util; 029 030import java.awt.Color; 031import java.awt.Graphics2D; 032import java.awt.image.BufferedImage; 033import java.awt.image.DataBufferByte; 034import java.io.BufferedOutputStream; 035import java.io.FileOutputStream; 036import java.io.IOException; 037import java.io.OutputStream; 038 039/** 040 * Class AnimatedGifEncoder - Encodes a GIF file consisting of one or 041 * more frames. 042 * <pre> 043 * Example: 044 * AnimatedGifEncoder e = new AnimatedGifEncoder(); 045 * e.start(outputFileName); 046 * e.setDelay(1000); // 1 frame per sec 047 * e.addFrame(image1); 048 * e.addFrame(image2); 049 * e.finish(); 050 * </pre> 051 * No copyright asserted on the source code of this class. May be used 052 * for any purpose, however, refer to the Unisys LZW patent for restrictions 053 * on use of the associated LZWEncoder class. Please forward any corrections 054 * to questions at fmsware.com. 055 * 056 * @author Kevin Weiner, FM Software 057 * @version 1.03 November 2003 058 * 059 */ 060 061public class AnimatedGifEncoder { 062 063 protected int width; // image size 064 protected int height; 065 protected Color transparent = null; // transparent color if given 066 protected boolean transparentExactMatch = false; // transparent color will be found by looking for the closest color 067 // or for the exact color if transparentExactMatch == true 068 protected Color background = null; // background color if given 069 protected int transIndex; // transparent index in color table 070 protected int repeat = -1; // no repeat 071 protected int delay = 0; // frame delay (hundredths) 072 protected boolean started = false; // ready to output frames 073 protected OutputStream out; 074 protected BufferedImage image; // current frame 075 protected byte[] pixels; // BGR byte array from frame 076 protected byte[] indexedPixels; // converted frame indexed to palette 077 protected int colorDepth; // number of bit planes 078 protected byte[] colorTab; // RGB palette 079 protected boolean[] usedEntry = new boolean[256]; // active palette entries 080 protected int palSize = 7; // color table size (bits-1) 081 protected int dispose = -1; // disposal code (-1 = use default) 082 protected boolean closeStream = false; // close stream when finished 083 protected boolean firstFrame = true; 084 protected boolean sizeSet = false; // if false, get size from first frame 085 protected int sample = 10; // default sample interval for quantizer 086 087 /** 088 * Sets the delay time between each frame, or changes it 089 * for subsequent frames (applies to last frame added). 090 * 091 * @param ms int delay time in milliseconds 092 */ 093 public void setDelay(int ms) { 094 delay = Math.round(ms / 10.0f); 095 } 096 097 /** 098 * Sets the GIF frame disposal code for the last added frame 099 * and any subsequent frames. Default is 0 if no transparent 100 * color has been set, otherwise 2. 101 * @param code int disposal code. 102 */ 103 public void setDispose(int code) { 104 if (code >= 0) { 105 dispose = code; 106 } 107 } 108 109 /** 110 * Sets the number of times the set of GIF frames 111 * should be played. Default is 1; 0 means play 112 * indefinitely. Must be invoked before the first 113 * image is added. 114 * 115 * @param iter int number of iterations. 116 */ 117 public void setRepeat(int iter) { 118 if (iter >= 0) { 119 repeat = iter; 120 } 121 } 122 123 /** 124 * Sets the transparent color for the last added frame 125 * and any subsequent frames. 126 * Since all colors are subject to modification 127 * in the quantization process, the color in the final 128 * palette for each frame closest to the given color 129 * becomes the transparent color for that frame. 130 * May be set to null to indicate no transparent color. 131 * 132 * @param c Color to be treated as transparent on display. 133 */ 134 public void setTransparent(Color c) { 135 setTransparent (c, false); 136 } 137 138 /** 139 * Sets the transparent color for the last added frame 140 * and any subsequent frames. 141 * Since all colors are subject to modification 142 * in the quantization process, the color in the final 143 * palette for each frame closest to the given color 144 * becomes the transparent color for that frame. 145 * If exactMatch is set to true, transparent color index 146 * is search with exact match, and not looking for the 147 * closest one. 148 * May be set to null to indicate no transparent color. 149 * 150 * @param c Color to be treated as transparent on display. 151 */ 152 public void setTransparent(Color c, boolean exactMatch) { 153 transparent = c; 154 transparentExactMatch = exactMatch; 155 } 156 157 /** 158 * Sets the background color for the last added frame 159 * and any subsequent frames. 160 * Since all colors are subject to modification 161 * in the quantization process, the color in the final 162 * palette for each frame closest to the given color 163 * becomes the background color for that frame. 164 * May be set to null to indicate no background color 165 * which will default to black. 166 * 167 * @param c Color to be treated as background on display. 168 */ 169 public void setBackground(Color c) { 170 background = c; 171 } 172 173 /** 174 * Adds next GIF frame. The frame is not written immediately, but is 175 * actually deferred until the next frame is received so that timing 176 * data can be inserted. Invoking <code>finish()</code> flushes all 177 * frames. If <code>setSize</code> was not invoked, the size of the 178 * first image is used for all subsequent frames. 179 * 180 * @param im BufferedImage containing frame to write. 181 * @return true if successful. 182 */ 183 public boolean addFrame(BufferedImage im) { 184 if ((im == null) || !started) { 185 return false; 186 } 187 boolean ok = true; 188 try { 189 if (!sizeSet) { 190 // use first frame's size 191 setSize(im.getWidth(), im.getHeight()); 192 } 193 image = im; 194 getImagePixels(); // convert to correct format if necessary 195 analyzePixels(); // build color table & map pixels 196 if (firstFrame) { 197 writeLSD(); // logical screen descriptior 198 writePalette(); // global color table 199 if (repeat >= 0) { 200 // use NS app extension to indicate reps 201 writeNetscapeExt(); 202 } 203 } 204 writeGraphicCtrlExt(); // write graphic control extension 205 writeImageDesc(); // image descriptor 206 if (!firstFrame) { 207 writePalette(); // local color table 208 } 209 writePixels(); // encode and write pixel data 210 firstFrame = false; 211 } catch (IOException e) { 212 ok = false; 213 } 214 215 return ok; 216 } 217 218 /** 219 * Flushes any pending data and closes output file. 220 * If writing to an OutputStream, the stream is not 221 * closed. 222 */ 223 public boolean finish() { 224 if (!started) return false; 225 boolean ok = true; 226 started = false; 227 try { 228 out.write(0x3b); // gif trailer 229 out.flush(); 230 if (closeStream) { 231 out.close(); 232 } 233 } catch (IOException e) { 234 ok = false; 235 } 236 237 // reset for subsequent use 238 transIndex = 0; 239 out = null; 240 image = null; 241 pixels = null; 242 indexedPixels = null; 243 colorTab = null; 244 closeStream = false; 245 firstFrame = true; 246 247 return ok; 248 } 249 250 /** 251 * Sets frame rate in frames per second. Equivalent to 252 * <code>setDelay(1000/fps)</code>. 253 * 254 * @param fps float frame rate (frames per second) 255 */ 256 public void setFrameRate(float fps) { 257 if (fps != 0f) { 258 delay = Math.round(100f / fps); 259 } 260 } 261 262 /** 263 * Sets quality of color quantization (conversion of images 264 * to the maximum 256 colors allowed by the GIF specification). 265 * Lower values (minimum = 1) produce better colors, but slow 266 * processing significantly. 10 is the default, and produces 267 * good color mapping at reasonable speeds. Values greater 268 * than 20 do not yield significant improvements in speed. 269 * 270 * @param quality int greater than 0. 271 */ 272 public void setQuality(int quality) { 273 if (quality < 1) quality = 1; 274 sample = quality; 275 } 276 277 /** 278 * Sets the GIF frame size. The default size is the 279 * size of the first frame added if this method is 280 * not invoked. 281 * 282 * @param w int frame width. 283 * @param h int frame width. 284 */ 285 public void setSize(int w, int h) { 286 if (started && !firstFrame) return; 287 width = w; 288 height = h; 289 if (width < 1) width = 320; 290 if (height < 1) height = 240; 291 sizeSet = true; 292 } 293 294 /** 295 * Initiates GIF file creation on the given stream. The stream 296 * is not closed automatically. 297 * 298 * @param os OutputStream on which GIF images are written. 299 * @return false if initial write failed. 300 */ 301 public boolean start(OutputStream os) { 302 if (os == null) return false; 303 boolean ok = true; 304 closeStream = false; 305 out = os; 306 try { 307 writeString("GIF89a"); // header 308 } catch (IOException e) { 309 ok = false; 310 } 311 return started = ok; 312 } 313 314 /** 315 * Initiates writing of a GIF file with the specified name. 316 * 317 * @param file String containing output file name. 318 * @return false if open or initial write failed. 319 */ 320 public boolean start(String file) { 321 boolean ok = true; 322 try { 323 out = new BufferedOutputStream(new FileOutputStream(file)); 324 ok = start(out); 325 closeStream = true; 326 } catch (IOException e) { 327 ok = false; 328 } 329 return started = ok; 330 } 331 332 public boolean isStarted() { 333 return started; 334 } 335 336 /** 337 * Analyzes image colors and creates color map. 338 */ 339 protected void analyzePixels() { 340 int len = pixels.length; 341 int nPix = len / 3; 342 indexedPixels = new byte[nPix]; 343 NeuQuant nq = new NeuQuant(pixels, len, sample); 344 // initialize quantizer 345 colorTab = nq.process(); // create reduced palette 346 // convert map from BGR to RGB 347 for (int i = 0; i < colorTab.length; i += 3) { 348 byte temp = colorTab[i]; 349 colorTab[i] = colorTab[i + 2]; 350 colorTab[i + 2] = temp; 351 usedEntry[i / 3] = false; 352 } 353 // map image pixels to new palette 354 int k = 0; 355 for (int i = 0; i < nPix; i++) { 356 int index = 357 nq.map(pixels[k++] & 0xff, 358 pixels[k++] & 0xff, 359 pixels[k++] & 0xff); 360 usedEntry[index] = true; 361 indexedPixels[i] = (byte) index; 362 } 363 pixels = null; 364 colorDepth = 8; 365 palSize = 7; 366 // get closest match to transparent color if specified 367 if (transparent != null) { 368 transIndex = transparentExactMatch ? findExact(transparent) : findClosest(transparent); 369 } 370 } 371 372 /** 373 * Returns index of palette color closest to c 374 * 375 */ 376 protected int findClosest(Color c) { 377 if (colorTab == null) return -1; 378 int r = c.getRed(); 379 int g = c.getGreen(); 380 int b = c.getBlue(); 381 int minpos = 0; 382 int dmin = 256 * 256 * 256; 383 int len = colorTab.length; 384 for (int i = 0; i < len;) { 385 int dr = r - (colorTab[i++] & 0xff); 386 int dg = g - (colorTab[i++] & 0xff); 387 int db = b - (colorTab[i] & 0xff); 388 int d = dr * dr + dg * dg + db * db; 389 int index = i / 3; 390 if (usedEntry[index] && (d < dmin)) { 391 dmin = d; 392 minpos = index; 393 } 394 i++; 395 } 396 return minpos; 397 } 398 399 /* 400 * Returns true if the exact matching color is existing, and used in the color palette, otherwise, return false. This method has to be called before 401 * finishing the image, because after finished the palette is destroyed and it will always return false. 402 */ 403 boolean isColorUsed(Color c) { 404 return findExact(c) != -1; 405 } 406 407 /** 408 * Returns index of palette exactly matching to color c or -1 if there is no exact matching. 409 * 410 */ 411 protected int findExact(Color c) { 412 if (colorTab == null) { 413 return -1; 414 } 415 416 int r = c.getRed(); 417 int g = c.getGreen(); 418 int b = c.getBlue(); 419 int len = colorTab.length / 3; 420 for (int index = 0; index < len; ++index) { 421 int i = index * 3; 422 // If the entry is used in colorTab, then check if it is the same exact color we're looking for 423 if (usedEntry[index] && r == (colorTab[i] & 0xff) && g == (colorTab[i+1] & 0xff) && b == (colorTab[i+2] & 0xff)) { 424 return index; 425 } 426 } 427 return -1; 428 } 429 430 /** 431 * Extracts image pixels into byte array "pixels" 432 */ 433 protected void getImagePixels() { 434 int w = image.getWidth(); 435 int h = image.getHeight(); 436 int type = image.getType(); 437 if ((w != width) 438 || (h != height) 439 || (type != BufferedImage.TYPE_3BYTE_BGR)) { 440 // create new image with right size/format 441 BufferedImage temp = 442 new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); 443 Graphics2D g = temp.createGraphics(); 444 g.setColor(background); 445 g.fillRect(0, 0, width, height); 446 g.drawImage(image, 0, 0, null); 447 image = temp; 448 } 449 pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); 450 } 451 452 /** 453 * Writes Graphic Control Extension 454 */ 455 protected void writeGraphicCtrlExt() throws IOException { 456 out.write(0x21); // extension introducer 457 out.write(0xf9); // GCE label 458 out.write(4); // data block size 459 int transp, disp; 460 if (transparent == null) { 461 transp = 0; 462 disp = 0; // dispose = no action 463 } else { 464 transp = 1; 465 disp = 2; // force clear if using transparent color 466 } 467 if (dispose >= 0) { 468 disp = dispose & 7; // user override 469 } 470 disp <<= 2; 471 472 // packed fields 473 out.write(0 | // 1:3 reserved 474 disp | // 4:6 disposal 475 0 | // 7 user input - 0 = none 476 transp); // 8 transparency flag 477 478 writeShort(delay); // delay x 1/100 sec 479 out.write(transIndex); // transparent color index 480 out.write(0); // block terminator 481 } 482 483 /** 484 * Writes Image Descriptor 485 */ 486 protected void writeImageDesc() throws IOException { 487 out.write(0x2c); // image separator 488 writeShort(0); // image position x,y = 0,0 489 writeShort(0); 490 writeShort(width); // image size 491 writeShort(height); 492 // packed fields 493 if (firstFrame) { 494 // no LCT - GCT is used for first (or only) frame 495 out.write(0); 496 } else { 497 // specify normal LCT 498 out.write(0x80 | // 1 local color table 1=yes 499 0 | // 2 interlace - 0=no 500 0 | // 3 sorted - 0=no 501 0 | // 4-5 reserved 502 palSize); // 6-8 size of color table 503 } 504 } 505 506 /** 507 * Writes Logical Screen Descriptor 508 */ 509 protected void writeLSD() throws IOException { 510 // logical screen size 511 writeShort(width); 512 writeShort(height); 513 // packed fields 514 out.write((0x80 | // 1 : global color table flag = 1 (gct used) 515 0x70 | // 2-4 : color resolution = 7 516 0x00 | // 5 : gct sort flag = 0 517 palSize)); // 6-8 : gct size 518 519 out.write(0); // background color index 520 out.write(0); // pixel aspect ratio - assume 1:1 521 } 522 523 /** 524 * Writes Netscape application extension to define 525 * repeat count. 526 */ 527 protected void writeNetscapeExt() throws IOException { 528 out.write(0x21); // extension introducer 529 out.write(0xff); // app extension label 530 out.write(11); // block size 531 writeString("NETSCAPE" + "2.0"); // app id + auth code 532 out.write(3); // sub-block size 533 out.write(1); // loop sub-block id 534 writeShort(repeat); // loop count (extra iterations, 0=repeat forever) 535 out.write(0); // block terminator 536 } 537 538 /** 539 * Writes color table 540 */ 541 protected void writePalette() throws IOException { 542 out.write(colorTab, 0, colorTab.length); 543 int n = (3 * 256) - colorTab.length; 544 for (int i = 0; i < n; i++) { 545 out.write(0); 546 } 547 } 548 549 /** 550 * Encodes and writes pixel data 551 */ 552 protected void writePixels() throws IOException { 553 LZWEncoder encoder = 554 new LZWEncoder(width, height, indexedPixels, colorDepth); 555 encoder.encode(out); 556 } 557 558 /** 559 * Write 16-bit value to output stream, LSB first 560 */ 561 protected void writeShort(int value) throws IOException { 562 out.write(value & 0xff); 563 out.write((value >> 8) & 0xff); 564 } 565 566 /** 567 * Writes string to output stream 568 */ 569 protected void writeString(String s) throws IOException { 570 for (int i = 0; i < s.length(); i++) { 571 out.write((byte) s.charAt(i)); 572 } 573 } 574}