/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