View Javadoc

1   /* ============================================================
2    * JRobin : Pure java implementation of RRDTool's functionality
3    * ============================================================
4    *
5    * Project Info:  http://www.jrobin.org
6    * Project Lead:  Sasa Markovic (saxon@jrobin.org)
7    *
8    * Developers:    Sasa Markovic (saxon@jrobin.org)
9    *
10   *
11   * (C) Copyright 2003-2005, by Sasa Markovic.
12   *
13   * This library is free software; you can redistribute it and/or modify it under the terms
14   * of the GNU Lesser General Public License as published by the Free Software Foundation;
15   * either version 2.1 of the License, or (at your option) any later version.
16   *
17   * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
18   * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19   * See the GNU Lesser General Public License for more details.
20   *
21   * You should have received a copy of the GNU Lesser General Public License along with this
22   * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
23   * Boston, MA 02111-1307, USA.
24   */
25  
26  ///////////////////////////////////////////////////////////////////
27  // GifEncoder from J.M.G. Elliott
28  // http://jmge.net/java/gifenc/
29  ///////////////////////////////////////////////////////////////////
30  
31  package org.jrobin.graph;
32  
33  import java.awt.*;
34  import java.awt.image.PixelGrabber;
35  import java.io.IOException;
36  import java.io.OutputStream;
37  import java.util.Vector;
38  
39  class GifEncoder {
40  	private Dimension dispDim = new Dimension(0, 0);
41  	private GifColorTable colorTable;
42  	private int bgIndex = 0;
43  	private int loopCount = 1;
44  	private String theComments;
45  	private Vector<Gif89Frame> vFrames = new Vector<Gif89Frame>();
46  
47  	GifEncoder() {
48  		colorTable = new GifColorTable();
49  	}
50  
51  	GifEncoder(Image static_image) throws IOException {
52  		this();
53  		addFrame(static_image);
54  	}
55  
56  	GifEncoder(Color[] colors) {
57  		colorTable = new GifColorTable(colors);
58  	}
59  
60  	GifEncoder(Color[] colors, int width, int height, byte ci_pixels[])
61  			throws IOException {
62  		this(colors);
63  		addFrame(width, height, ci_pixels);
64  	}
65  
66  	int getFrameCount() {
67  		return vFrames.size();
68  	}
69  
70  	Gif89Frame getFrameAt(int index) {
71  		return isOk(index) ? vFrames.elementAt(index) : null;
72  	}
73  
74  	void addFrame(Gif89Frame gf) throws IOException {
75  		accommodateFrame(gf);
76  		vFrames.addElement(gf);
77  	}
78  
79  	void addFrame(Image image) throws IOException {
80  		addFrame(new DirectGif89Frame(image));
81  	}
82  
83  	void addFrame(int width, int height, byte ci_pixels[])
84  			throws IOException {
85  		addFrame(new IndexGif89Frame(width, height, ci_pixels));
86  	}
87  
88  	void insertFrame(int index, Gif89Frame gf) throws IOException {
89  		accommodateFrame(gf);
90  		vFrames.insertElementAt(gf, index);
91  	}
92  
93  	void setTransparentIndex(int index) {
94  		colorTable.setTransparent(index);
95  	}
96  
97  	void setLogicalDisplay(Dimension dim, int background) {
98  		dispDim = new Dimension(dim);
99  		bgIndex = background;
100 	}
101 
102 	void setLoopCount(int count) {
103 		loopCount = count;
104 	}
105 
106 	void setComments(String comments) {
107 		theComments = comments;
108 	}
109 
110 	void setUniformDelay(int interval) {
111 		for (int i = 0; i < vFrames.size(); ++i) {
112 			vFrames.elementAt(i).setDelay(interval);
113 		}
114 	}
115 
116 	void encode(OutputStream out) throws IOException {
117 		int nframes = getFrameCount();
118 		boolean is_sequence = nframes > 1;
119 		colorTable.closePixelProcessing();
120 		Put.ascii("GIF89a", out);
121 		writeLogicalScreenDescriptor(out);
122 		colorTable.encode(out);
123 		if (is_sequence && loopCount != 1) {
124 			writeNetscapeExtension(out);
125 		}
126 		if (theComments != null && theComments.length() > 0) {
127 			writeCommentExtension(out);
128 		}
129 		for (int i = 0; i < nframes; ++i) {
130 			vFrames.elementAt(i).encode(
131 					out, is_sequence, colorTable.getDepth(), colorTable.getTransparent()
132 			);
133 		}
134 		out.write((int) ';');
135 		out.flush();
136 	}
137 
138 	private void accommodateFrame(Gif89Frame gf) throws IOException {
139 		dispDim.width = Math.max(dispDim.width, gf.getWidth());
140 		dispDim.height = Math.max(dispDim.height, gf.getHeight());
141 		colorTable.processPixels(gf);
142 	}
143 
144 	private void writeLogicalScreenDescriptor(OutputStream os) throws IOException {
145 		Put.leShort(dispDim.width, os);
146 		Put.leShort(dispDim.height, os);
147 		os.write(0xf0 | colorTable.getDepth() - 1);
148 		os.write(bgIndex);
149 		os.write(0);
150 	}
151 
152 
153 	private void writeNetscapeExtension(OutputStream os) throws IOException {
154 		os.write((int) '!');
155 		os.write(0xff);
156 		os.write(11);
157 		Put.ascii("NETSCAPE2.0", os);
158 		os.write(3);
159 		os.write(1);
160 		Put.leShort(loopCount > 1 ? loopCount - 1 : 0, os);
161 		os.write(0);
162 	}
163 
164 
165 	private void writeCommentExtension(OutputStream os) throws IOException {
166 		os.write((int) '!');
167 		os.write(0xfe);
168 		int remainder = theComments.length() % 255;
169 		int nsubblocks_full = theComments.length() / 255;
170 		int nsubblocks = nsubblocks_full + (remainder > 0 ? 1 : 0);
171 		int ibyte = 0;
172 		for (int isb = 0; isb < nsubblocks; ++isb) {
173 			int size = isb < nsubblocks_full ? 255 : remainder;
174 			os.write(size);
175 			Put.ascii(theComments.substring(ibyte, ibyte + size), os);
176 			ibyte += size;
177 		}
178 		os.write(0);
179 	}
180 
181 
182 	private boolean isOk(int frame_index) {
183 		return frame_index >= 0 && frame_index < vFrames.size();
184 	}
185 }
186 
187 class DirectGif89Frame extends Gif89Frame {
188 	private int[] argbPixels;
189 
190 	DirectGif89Frame(Image img) throws IOException {
191 		PixelGrabber pg = new PixelGrabber(img, 0, 0, -1, -1, true);
192 		String errmsg = null;
193 		try {
194 			if (!pg.grabPixels()) {
195 				errmsg = "can't grab pixels from image";
196 			}
197 		}
198 		catch (InterruptedException e) {
199 			errmsg = "interrupted grabbing pixels from image";
200 		}
201 		if (errmsg != null) {
202 			throw new IOException(errmsg + " (" + getClass().getName() + ")");
203 		}
204 		theWidth = pg.getWidth();
205 		theHeight = pg.getHeight();
206 		argbPixels = (int[]) pg.getPixels();
207 		ciPixels = new byte[argbPixels.length];
208 	}
209 
210 	DirectGif89Frame(int width, int height, int argb_pixels[]) {
211 		theWidth = width;
212 		theHeight = height;
213 		argbPixels = new int[theWidth * theHeight];
214 		System.arraycopy(argb_pixels, 0, argbPixels, 0, argbPixels.length);
215 		ciPixels = new byte[argbPixels.length];
216 	}
217 
218 	Object getPixelSource() {
219 		return argbPixels;
220 	}
221 }
222 
223 
224 class GifColorTable {
225 	private int[] theColors = new int[256];
226 	private int colorDepth;
227 	private int transparentIndex = -1;
228 	private int ciCount = 0;
229 	private ReverseColorMap ciLookup;
230 
231 	GifColorTable() {
232 		ciLookup = new ReverseColorMap();
233 	}
234 
235 	GifColorTable(Color[] colors) {
236 		int n2copy = Math.min(theColors.length, colors.length);
237 		for (int i = 0; i < n2copy; ++i) {
238 			theColors[i] = colors[i].getRGB();
239 		}
240 	}
241 
242 	int getDepth() {
243 		return colorDepth;
244 	}
245 
246 	int getTransparent() {
247 		return transparentIndex;
248 	}
249 
250 	void setTransparent(int color_index) {
251 		transparentIndex = color_index;
252 	}
253 
254 	void processPixels(Gif89Frame gf) throws IOException {
255 		if (gf instanceof DirectGif89Frame) {
256 			filterPixels((DirectGif89Frame) gf);
257 		}
258 		else {
259 			trackPixelUsage((IndexGif89Frame) gf);
260 		}
261 	}
262 
263 	void closePixelProcessing() {
264 		colorDepth = computeColorDepth(ciCount);
265 	}
266 
267 	void encode(OutputStream os) throws IOException {
268 		int palette_size = 1 << colorDepth;
269 		for (int i = 0; i < palette_size; ++i) {
270 			os.write(theColors[i] >> 16 & 0xff);
271 			os.write(theColors[i] >> 8 & 0xff);
272 			os.write(theColors[i] & 0xff);
273 		}
274 	}
275 
276 	private void filterPixels(DirectGif89Frame dgf) throws IOException {
277 		if (ciLookup == null) {
278 			throw new IOException("RGB frames require palette autodetection");
279 		}
280 		int[] argb_pixels = (int[]) dgf.getPixelSource();
281 		byte[] ci_pixels = dgf.getPixelSink();
282 		int npixels = argb_pixels.length;
283 		for (int i = 0; i < npixels; ++i) {
284 			int argb = argb_pixels[i];
285 			if ((argb >>> 24) < 0x80) {
286 				if (transparentIndex == -1) {
287 					transparentIndex = ciCount;
288 				}
289 				else if (argb != theColors[transparentIndex]) {
290 					ci_pixels[i] = (byte) transparentIndex;
291 					continue;
292 				}
293 			}
294 			int color_index = ciLookup.getPaletteIndex(argb & 0xffffff);
295 			if (color_index == -1) {
296 				if (ciCount == 256) {
297 					throw new IOException("can't encode as GIF (> 256 colors)");
298 				}
299 				theColors[ciCount] = argb;
300 				ciLookup.put(argb & 0xffffff, ciCount);
301 				ci_pixels[i] = (byte) ciCount;
302 				++ciCount;
303 			}
304 			else {
305 				ci_pixels[i] = (byte) color_index;
306 			}
307 		}
308 	}
309 
310 	private void trackPixelUsage(IndexGif89Frame igf) {
311 		byte[] ci_pixels = (byte[]) igf.getPixelSource();
312 		int npixels = ci_pixels.length;
313 		for (int i = 0; i < npixels; ++i) {
314 			if (ci_pixels[i] >= ciCount) {
315 				ciCount = ci_pixels[i] + 1;
316 			}
317 		}
318 	}
319 
320 	private int computeColorDepth(int colorcount) {
321 		if (colorcount <= 2) {
322 			return 1;
323 		}
324 		if (colorcount <= 4) {
325 			return 2;
326 		}
327 		if (colorcount <= 16) {
328 			return 4;
329 		}
330 		return 8;
331 	}
332 }
333 
334 class ReverseColorMap {
335 	private static class ColorRecord {
336 		int rgb;
337 		int ipalette;
338 
339 		ColorRecord(int rgb, int ipalette) {
340 			this.rgb = rgb;
341 			this.ipalette = ipalette;
342 		}
343 	}
344 
345 	private static final int HCAPACITY = 2053;
346 	private ColorRecord[] hTable = new ColorRecord[HCAPACITY];
347 
348 	int getPaletteIndex(int rgb) {
349 		ColorRecord rec;
350 		for (int itable = rgb % hTable.length;
351 			 (rec = hTable[itable]) != null && rec.rgb != rgb;
352 			 itable = ++itable % hTable.length
353 				) {
354 			;
355 		}
356 		if (rec != null) {
357 			return rec.ipalette;
358 		}
359 		return -1;
360 	}
361 
362 
363 	void put(int rgb, int ipalette) {
364 		int itable;
365 		for (itable = rgb % hTable.length;
366 			 hTable[itable] != null;
367 			 itable = ++itable % hTable.length
368 				) {
369 			;
370 		}
371 		hTable[itable] = new ColorRecord(rgb, ipalette);
372 	}
373 }
374 
375 abstract class Gif89Frame {
376 	static final int DM_UNDEFINED = 0;
377 	static final int DM_LEAVE = 1;
378 	static final int DM_BGCOLOR = 2;
379 	static final int DM_REVERT = 3;
380 	int theWidth = -1;
381 	int theHeight = -1;
382 	byte[] ciPixels;
383 
384 	private Point thePosition = new Point(0, 0);
385 	private boolean isInterlaced;
386 	private int csecsDelay;
387 	private int disposalCode = DM_LEAVE;
388 
389 	void setPosition(Point p) {
390 		thePosition = new Point(p);
391 	}
392 
393 	void setInterlaced(boolean b) {
394 		isInterlaced = b;
395 	}
396 
397 	void setDelay(int interval) {
398 		csecsDelay = interval;
399 	}
400 
401 	void setDisposalMode(int code) {
402 		disposalCode = code;
403 	}
404 
405 	Gif89Frame() {
406 	}
407 
408 	abstract Object getPixelSource();
409 
410 	int getWidth() {
411 		return theWidth;
412 	}
413 
414 	int getHeight() {
415 		return theHeight;
416 	}
417 
418 	byte[] getPixelSink() {
419 		return ciPixels;
420 	}
421 
422 	void encode(OutputStream os, boolean epluribus, int color_depth,
423 				int transparent_index) throws IOException {
424 		writeGraphicControlExtension(os, epluribus, transparent_index);
425 		writeImageDescriptor(os);
426 		new GifPixelsEncoder(
427 				theWidth, theHeight, ciPixels, isInterlaced, color_depth
428 		).encode(os);
429 	}
430 
431 	private void writeGraphicControlExtension(OutputStream os, boolean epluribus,
432 											  int itransparent) throws IOException {
433 		int transflag = itransparent == -1 ? 0 : 1;
434 		if (transflag == 1 || epluribus) {
435 			os.write((int) '!');
436 			os.write(0xf9);
437 			os.write(4);
438 			os.write((disposalCode << 2) | transflag);
439 			Put.leShort(csecsDelay, os);
440 			os.write(itransparent);
441 			os.write(0);
442 		}
443 	}
444 
445 	private void writeImageDescriptor(OutputStream os) throws IOException {
446 		os.write((int) ',');
447 		Put.leShort(thePosition.x, os);
448 		Put.leShort(thePosition.y, os);
449 		Put.leShort(theWidth, os);
450 		Put.leShort(theHeight, os);
451 		os.write(isInterlaced ? 0x40 : 0);
452 	}
453 }
454 
455 class GifPixelsEncoder {
456 	private static final int EOF = -1;
457 	private int imgW, imgH;
458 	private byte[] pixAry;
459 	private boolean wantInterlaced;
460 	private int initCodeSize;
461 	private int countDown;
462 	private int xCur, yCur;
463 	private int curPass;
464 
465 	GifPixelsEncoder(int width, int height, byte[] pixels, boolean interlaced,
466 					 int color_depth) {
467 		imgW = width;
468 		imgH = height;
469 		pixAry = pixels;
470 		wantInterlaced = interlaced;
471 		initCodeSize = Math.max(2, color_depth);
472 	}
473 
474 	void encode(OutputStream os) throws IOException {
475 		os.write(initCodeSize);
476 
477 		countDown = imgW * imgH;
478 		xCur = yCur = curPass = 0;
479 
480 		compress(initCodeSize + 1, os);
481 
482 		os.write(0);
483 	}
484 
485 	private void bumpPosition() {
486 		++xCur;
487 		if (xCur == imgW) {
488 			xCur = 0;
489 			if (!wantInterlaced) {
490 				++yCur;
491 			}
492 			else {
493 				switch (curPass) {
494 					case 0:
495 						yCur += 8;
496 						if (yCur >= imgH) {
497 							++curPass;
498 							yCur = 4;
499 						}
500 						break;
501 					case 1:
502 						yCur += 8;
503 						if (yCur >= imgH) {
504 							++curPass;
505 							yCur = 2;
506 						}
507 						break;
508 					case 2:
509 						yCur += 4;
510 						if (yCur >= imgH) {
511 							++curPass;
512 							yCur = 1;
513 						}
514 						break;
515 					case 3:
516 						yCur += 2;
517 						break;
518 				}
519 			}
520 		}
521 	}
522 
523 	private int nextPixel() {
524 		if (countDown == 0) {
525 			return EOF;
526 		}
527 		--countDown;
528 		byte pix = pixAry[yCur * imgW + xCur];
529 		bumpPosition();
530 		return pix & 0xff;
531 	}
532 
533 	static final int BITS = 12;
534 	static final int HSIZE = 5003;
535 	int n_bits;
536 	int maxbits = BITS;
537 	int maxcode;
538 	int maxmaxcode = 1 << BITS;
539 
540 	final int MAXCODE(int n_bits) {
541 		return (1 << n_bits) - 1;
542 	}
543 
544 	int[] htab = new int[HSIZE];
545 	int[] codetab = new int[HSIZE];
546 	int hsize = HSIZE;
547 	int free_ent = 0;
548 	boolean clear_flg = false;
549 	int g_init_bits;
550 	int ClearCode;
551 	int EOFCode;
552 
553 	void compress(int init_bits, OutputStream outs) throws IOException {
554 		int fcode;
555 		int i /* = 0 */;
556 		int c;
557 		int ent;
558 		int disp;
559 		int hsize_reg;
560 		int hshift;
561 		g_init_bits = init_bits;
562 		clear_flg = false;
563 		n_bits = g_init_bits;
564 		maxcode = MAXCODE(n_bits);
565 		ClearCode = 1 << (init_bits - 1);
566 		EOFCode = ClearCode + 1;
567 		free_ent = ClearCode + 2;
568 
569 		char_init();
570 		ent = nextPixel();
571 		hshift = 0;
572 		for (fcode = hsize; fcode < 65536; fcode *= 2) {
573 			++hshift;
574 		}
575 		hshift = 8 - hshift;
576 		hsize_reg = hsize;
577 		cl_hash(hsize_reg);
578 		output(ClearCode, outs);
579 		outer_loop:
580 		while ((c = nextPixel()) != EOF) {
581 			fcode = (c << maxbits) + ent;
582 			i = (c << hshift) ^ ent;
583 			if (htab[i] == fcode) {
584 				ent = codetab[i];
585 				continue;
586 			}
587 			else if (htab[i] >= 0) {
588 				disp = hsize_reg - i;
589 				if (i == 0) {
590 					disp = 1;
591 				}
592 				do {
593 					if ((i -= disp) < 0) {
594 						i += hsize_reg;
595 					}
596 
597 					if (htab[i] == fcode) {
598 						ent = codetab[i];
599 						continue outer_loop;
600 					}
601 				} while (htab[i] >= 0);
602 			}
603 			output(ent, outs);
604 			ent = c;
605 			if (free_ent < maxmaxcode) {
606 				codetab[i] = free_ent++;
607 				htab[i] = fcode;
608 			}
609 			else {
610 				cl_block(outs);
611 			}
612 		}
613 		output(ent, outs);
614 		output(EOFCode, outs);
615 	}
616 
617 	int cur_accum = 0;
618 	int cur_bits = 0;
619 	int masks[] = {0x0000, 0x0001, 0x0003, 0x0007, 0x000F,
620 			0x001F, 0x003F, 0x007F, 0x00FF,
621 			0x01FF, 0x03FF, 0x07FF, 0x0FFF,
622 			0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF};
623 
624 	void output(int code, OutputStream outs) throws IOException {
625 		cur_accum &= masks[cur_bits];
626 		if (cur_bits > 0) {
627 			cur_accum |= (code << cur_bits);
628 		}
629 		else {
630 			cur_accum = code;
631 		}
632 
633 		cur_bits += n_bits;
634 
635 		while (cur_bits >= 8) {
636 			char_out((byte) (cur_accum & 0xff), outs);
637 			cur_accum >>= 8;
638 			cur_bits -= 8;
639 		}
640 		if (free_ent > maxcode || clear_flg) {
641 			if (clear_flg) {
642 				maxcode = MAXCODE(n_bits = g_init_bits);
643 				clear_flg = false;
644 			}
645 			else {
646 				++n_bits;
647 				if (n_bits == maxbits) {
648 					maxcode = maxmaxcode;
649 				}
650 				else {
651 					maxcode = MAXCODE(n_bits);
652 				}
653 			}
654 		}
655 		if (code == EOFCode) {
656 
657 			while (cur_bits > 0) {
658 				char_out((byte) (cur_accum & 0xff), outs);
659 				cur_accum >>= 8;
660 				cur_bits -= 8;
661 			}
662 			flush_char(outs);
663 		}
664 	}
665 
666 
667 	void cl_block(OutputStream outs) throws IOException {
668 		cl_hash(hsize);
669 		free_ent = ClearCode + 2;
670 		clear_flg = true;
671 
672 		output(ClearCode, outs);
673 	}
674 
675 
676 	void cl_hash(int hsize) {
677 		for (int i = 0; i < hsize; ++i) {
678 			htab[i] = -1;
679 		}
680 	}
681 
682 	int a_count;
683 
684 	void char_init() {
685 		a_count = 0;
686 	}
687 
688 	byte[] accum = new byte[256];
689 
690 	void char_out(byte c, OutputStream outs) throws IOException {
691 		accum[a_count++] = c;
692 		if (a_count >= 254) {
693 			flush_char(outs);
694 		}
695 	}
696 
697 	void flush_char(OutputStream outs) throws IOException {
698 		if (a_count > 0) {
699 			outs.write(a_count);
700 			outs.write(accum, 0, a_count);
701 			a_count = 0;
702 		}
703 	}
704 }
705 
706 class IndexGif89Frame extends Gif89Frame {
707 
708 	IndexGif89Frame(int width, int height, byte ci_pixels[]) {
709 		theWidth = width;
710 		theHeight = height;
711 		ciPixels = new byte[theWidth * theHeight];
712 		System.arraycopy(ci_pixels, 0, ciPixels, 0, ciPixels.length);
713 	}
714 
715 	Object getPixelSource() {
716 		return ciPixels;
717 	}
718 }
719 
720 
721 final class Put {
722 	static void ascii(String s, OutputStream os) throws IOException {
723 		byte[] bytes = new byte[s.length()];
724 		for (int i = 0; i < bytes.length; ++i) {
725 			bytes[i] = (byte) s.charAt(i);
726 		}
727 		os.write(bytes);
728 	}
729 
730 	static void leShort(int i16, OutputStream os) throws IOException {
731 		os.write(i16 & 0xff);
732 		os.write(i16 >> 8 & 0xff);
733 	}
734 }