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}