· 9 years ago · Feb 01, 2017, 04:58 AM
1/**
2The DrawingPanel class provides a simple interface for drawing persistent
3images using a Graphics object. An internal BufferedImage object is used
4to keep track of what has been drawn. A client of the class simply
5constructs a DrawingPanel of a particular size and then draws on it with
6the Graphics object, setting the background color if they so choose.
7<p>
8
9To ensure that the image is always displayed, a timer calls repaint at
10regular intervals.
11<p>
12
13This version of DrawingPanel also saves animated GIFs, though this is kind
14of hit-and-miss because animated GIFs are pretty sucky (256 color limit, large
15file size, etc).
16<p>
17
18Recent features:
19- save zoomed images (2011/10/25)
20- window no longer moves when zoom changes (2011/10/25)
21- grid lines (2011/10/11)
22
23@author Marty Stepp
24@version October 21, 2011
25*/
26
27import java.awt.AlphaComposite;
28import java.awt.BorderLayout;
29import java.awt.Color;
30import java.awt.Composite;
31import java.awt.Container;
32import java.awt.Dimension;
33import java.awt.EventQueue;
34import java.awt.FlowLayout;
35import java.awt.Font;
36import java.awt.Frame;
37import java.awt.Graphics;
38import java.awt.Graphics2D;
39import java.awt.GridLayout;
40import java.awt.Image;
41import java.awt.Point;
42import java.awt.RenderingHints;
43import java.awt.Toolkit;
44import java.awt.Window;
45import java.awt.event.ActionEvent;
46import java.awt.event.ActionListener;
47import java.awt.event.KeyEvent;
48import java.awt.event.KeyListener;
49import java.awt.event.MouseEvent;
50import java.awt.event.MouseListener;
51import java.awt.event.MouseMotionListener;
52import java.awt.event.WindowEvent;
53import java.awt.event.WindowListener;
54import java.awt.image.BufferedImage;
55import java.awt.image.PixelGrabber;
56import java.io.File;
57import java.io.FileOutputStream;
58import java.io.IOException;
59import java.io.OutputStream;
60import java.io.PrintStream;
61import java.lang.Exception;
62import java.lang.Integer;
63import java.lang.InterruptedException;
64import java.lang.Math;
65import java.lang.Object;
66import java.lang.OutOfMemoryError;
67import java.lang.SecurityException;
68import java.lang.String;
69import java.lang.System;
70import java.lang.Thread;
71import java.net.URL;
72import java.net.NoRouteToHostException;
73import java.net.SocketException;
74import java.net.UnknownHostException;
75import java.util.ArrayList;
76import java.util.List;
77import java.util.Scanner;
78import java.util.Vector;
79import javax.imageio.ImageIO;
80import javax.swing.BorderFactory;
81import javax.swing.Box;
82import javax.swing.JButton;
83import javax.swing.JCheckBox;
84import javax.swing.JCheckBoxMenuItem;
85import javax.swing.JColorChooser;
86import javax.swing.JDialog;
87import javax.swing.JFileChooser;
88import javax.swing.JFrame;
89import javax.swing.JLabel;
90import javax.swing.JMenu;
91import javax.swing.JMenuBar;
92import javax.swing.JMenuItem;
93import javax.swing.JOptionPane;
94import javax.swing.JPanel;
95import javax.swing.JScrollPane;
96import javax.swing.JSlider;
97import javax.swing.KeyStroke;
98import javax.swing.SwingConstants;
99import javax.swing.Timer;
100import javax.swing.UIManager;
101import javax.swing.event.ChangeEvent;
102import javax.swing.event.ChangeListener;
103import javax.swing.event.MouseInputListener;
104import javax.swing.filechooser.FileFilter;
105
106public final class DrawingPanel extends FileFilter
107 implements ActionListener, MouseMotionListener, Runnable, WindowListener {
108 // inner class to represent one frame of an animated GIF
109 private static class ImageFrame {
110 public Image image;
111 public int delay;
112
113 public ImageFrame(Image image, int delay) {
114 this.image = image;
115 this.delay = delay / 10; // strangely, gif stores delay as sec/100
116 }
117 }
118
119 // class constants
120 public static final String ANIMATED_PROPERTY = "drawingpanel.animated";
121 public static final String AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY = "drawingpanel.animateonsleep";
122 public static final String DIFF_PROPERTY = "drawingpanel.diff";
123 public static final String HEADLESS_PROPERTY = "drawingpanel.headless";
124 public static final String MULTIPLE_PROPERTY = "drawingpanel.multiple";
125 public static final String SAVE_PROPERTY = "drawingpanel.save";
126 public static final String ANIMATION_FILE_NAME = "_drawingpanel_animation_save.txt";
127 private static final String TITLE = "Drawing Panel";
128 private static final String COURSE_WEB_SITE = "http://www.cs.washington.edu/education/courses/cse142/12sp/drawingpanel.txt";
129 private static final Color GRID_LINE_COLOR = new Color(64, 64, 64, 128);
130 private static final int GRID_SIZE = 10; // 10px between grid lines
131 private static final int DELAY = 100; // delay between repaints in millis
132 private static final int MAX_FRAMES = 100; // max animation frames
133 private static final int MAX_SIZE = 10000; // max width/height
134 private static final boolean DEBUG = false;
135 private static final boolean SAVE_SCALED_IMAGES = true; // if true, when panel is zoomed, saves images at that zoom factor
136 private static int instances = 0;
137 private static Thread shutdownThread = null;
138
139 private static void checkAnimationSettings() {
140 try {
141 File settingsFile = new File(ANIMATION_FILE_NAME);
142 if (settingsFile.exists()) {
143 Scanner input = new Scanner(settingsFile);
144 String animationSaveFileName = input.nextLine();
145 input.close();
146 // *** TODO: delete the file
147 System.out.println("***");
148 System.out.println("*** DrawingPanel saving animated GIF: " +
149 new File(animationSaveFileName).getName());
150 System.out.println("***");
151 settingsFile.delete();
152
153 System.setProperty(ANIMATED_PROPERTY, "1");
154 System.setProperty(SAVE_PROPERTY, animationSaveFileName);
155 }
156 } catch (Exception e) {
157 if (DEBUG) {
158 System.out.println("error checking animation settings: " + e);
159 }
160 }
161 }
162
163 private static boolean hasProperty(String name) {
164 try {
165 return System.getProperty(name) != null;
166 } catch (SecurityException e) {
167 if (DEBUG) System.out.println("Security exception when trying to read " + name);
168 return false;
169 }
170 }
171
172 private static boolean propertyIsTrue(String name) {
173 try {
174 String prop = System.getProperty(name);
175 return prop != null && (prop.equalsIgnoreCase("true") || prop.equalsIgnoreCase("yes") || prop.equalsIgnoreCase("1"));
176 } catch (SecurityException e) {
177 if (DEBUG) System.out.println("Security exception when trying to read " + name);
178 return false;
179 }
180 }
181
182 /*
183 private static boolean propertyIsFalse(String name) {
184 try {
185 String prop = System.getProperty(name);
186 return prop != null && (prop.equalsIgnoreCase("false") || prop.equalsIgnoreCase("no") || prop.equalsIgnoreCase("0"));
187 } catch (SecurityException e) {
188 if (DEBUG) System.out.println("Security exception when trying to read " + name);
189 return false;
190 }
191 }
192 */
193
194 // Returns whether the 'main' thread is still running.
195 private static boolean mainIsActive() {
196 ThreadGroup group = Thread.currentThread().getThreadGroup();
197 int activeCount = group.activeCount();
198
199 // look for the main thread in the current thread group
200 Thread[] threads = new Thread[activeCount];
201 group.enumerate(threads);
202 for (int i = 0; i < threads.length; i++) {
203 Thread thread = threads[i];
204 String name = ("" + thread.getName()).toLowerCase();
205 if (name.indexOf("main") >= 0 ||
206 name.indexOf("testrunner-assignmentrunner") >= 0) {
207 // found main thread!
208 // (TestRunnerApplet's main runner also counts as "main" thread)
209 return thread.isAlive();
210 }
211 }
212
213 // didn't find a running main thread; guess that main is done running
214 return false;
215 }
216
217 private static boolean usingDrJava() {
218 try {
219 return System.getProperty("drjava.debug.port") != null ||
220 System.getProperty("java.class.path").toLowerCase().indexOf("drjava") >= 0;
221 } catch (SecurityException e) {
222 // running as an applet, or something
223 return false;
224 }
225 }
226
227 private class ImagePanel extends JPanel {
228 private static final long serialVersionUID = 0;
229 private Image image;
230
231 public ImagePanel(Image image) {
232 setImage(image);
233 setBackground(Color.WHITE);
234 setPreferredSize(new Dimension(image.getWidth(this), image.getHeight(this)));
235 setAlignmentX(0.0f);
236 }
237
238 public void paintComponent(Graphics g) {
239 super.paintComponent(g);
240 Graphics2D g2 = (Graphics2D) g;
241 if (currentZoom != 1) {
242 g2.scale(currentZoom, currentZoom);
243 }
244 g2.drawImage(image, 0, 0, this);
245
246 // possibly draw grid lines for debugging
247 if (gridLines) {
248 g2.setPaint(GRID_LINE_COLOR);
249 for (int row = 1; row <= getHeight() / GRID_SIZE; row++) {
250 g2.drawLine(0, row * GRID_SIZE, getWidth(), row * GRID_SIZE);
251 }
252 for (int col = 1; col <= getWidth() / GRID_SIZE; col++) {
253 g2.drawLine(col * GRID_SIZE, 0, col * GRID_SIZE, getHeight());
254 }
255 }
256 }
257
258 public void setImage(Image image) {
259 this.image = image;
260 repaint();
261 }
262 }
263
264 // fields
265 private int width, height; // dimensions of window frame
266 private JFrame frame; // overall window frame
267 private JPanel panel; // overall drawing surface
268 private ImagePanel imagePanel; // real drawing surface
269 private BufferedImage image; // remembers drawing commands
270 private Graphics2D g2; // graphics context for painting
271 private JLabel statusBar; // status bar showing mouse position
272 private JFileChooser chooser; // file chooser to save files
273 private long createTime; // time at which DrawingPanel was constructed
274 private Timer timer; // animation timer
275 private ArrayList<ImageFrame> frames; // stores frames of animation to save
276 private Gif89Encoder encoder;
277 // private FileOutputStream stream;
278 private Color backgroundColor = Color.WHITE;
279 private String callingClassName; // name of class that constructed this panel
280 private boolean animated = false; // changes to true if sleep() is called
281 private boolean PRETTY = true; // true to anti-alias
282 private boolean gridLines = false;
283 private int instanceNumber;
284 private int currentZoom = 1;
285 private int initialPixel; // initial value in each pixel, for clear()
286
287 // construct a drawing panel of given width and height enclosed in a window
288 public DrawingPanel(int width, int height) {
289 if (width < 0 || width > MAX_SIZE || height < 0 || height > MAX_SIZE) {
290 throw new IllegalArgumentException("Illegal width/height: " + width + " x " + height);
291 }
292
293 checkAnimationSettings();
294
295 synchronized (getClass()) {
296 instances++;
297 instanceNumber = instances; // each DrawingPanel stores its own int number
298
299 if (shutdownThread == null && !usingDrJava()) {
300 shutdownThread = new Thread(new Runnable() {
301 // Runnable implementation; used for shutdown thread.
302 public void run() {
303 try {
304 while (true) {
305 // maybe shut down the program, if no more DrawingPanels are onscreen
306 // and main has finished executing
307 if ((instances == 0 || shouldSave()) && !mainIsActive()) {
308 try {
309 System.exit(0);
310 } catch (SecurityException sex) {}
311 }
312
313 Thread.sleep(250);
314 }
315 } catch (Exception e) {}
316 }
317 });
318 shutdownThread.setPriority(Thread.MIN_PRIORITY);
319 shutdownThread.start();
320 }
321 }
322 this.width = width;
323 this.height = height;
324
325 if (DEBUG) System.out.println("w=" + width + ",h=" + height + ",anim=" + isAnimated() + ",graph=" + isGraphical() + ",save=" + shouldSave());
326
327 if (isAnimated() && shouldSave()) {
328 // image must be no more than 256 colors
329 image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
330 // image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
331 PRETTY = false; // turn off anti-aliasing to save palette colors
332
333 // initially fill the entire frame with the background color,
334 // because it won't show through via transparency like with a full ARGB image
335 Graphics g = image.getGraphics();
336 g.setColor(backgroundColor);
337 g.fillRect(0, 0, width + 1, height + 1);
338 } else {
339 image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
340 }
341 initialPixel = image.getRGB(0, 0);
342
343 g2 = (Graphics2D) image.getGraphics();
344 g2.setColor(Color.BLACK);
345 if (PRETTY) {
346 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
347 }
348
349 if (isAnimated()) {
350 initializeAnimation();
351 }
352
353 if (isGraphical()) {
354 try {
355 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
356 } catch (Exception e) {}
357
358 statusBar = new JLabel(" ");
359 statusBar.setBorder(BorderFactory.createLineBorder(Color.BLACK));
360
361 panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
362 panel.setBackground(backgroundColor);
363 panel.setPreferredSize(new Dimension(width, height));
364 imagePanel = new ImagePanel(image);
365 imagePanel.setBackground(backgroundColor);
366 panel.add(imagePanel);
367
368 // listen to mouse movement
369 panel.addMouseMotionListener(this);
370
371 // main window frame
372 frame = new JFrame(TITLE);
373 // frame.setResizable(false);
374 frame.addWindowListener(this);
375 // JPanel center = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
376 JScrollPane center = new JScrollPane(panel);
377 // center.add(panel);
378 frame.getContentPane().add(center);
379 frame.getContentPane().add(statusBar, "South");
380 frame.setBackground(Color.DARK_GRAY);
381
382 // menu bar
383 setupMenuBar();
384
385 frame.pack();
386 center(frame);
387 frame.setVisible(true);
388 if (!shouldSave()) {
389 toFront(frame);
390 }
391
392 // repaint timer so that the screen will update
393 createTime = System.currentTimeMillis();
394 timer = new Timer(DELAY, this);
395 timer.start();
396 } else if (shouldSave()) {
397 // headless mode; just set a hook on shutdown to save the image
398 callingClassName = getCallingClassName();
399 try {
400 Runtime.getRuntime().addShutdownHook(new Thread(this));
401 } catch (Exception e) {
402 if (DEBUG) System.out.println("unable to add shutdown hook: " + e);
403 }
404 }
405 }
406
407 // method of FileFilter interface
408 public boolean accept(File file) {
409 return file.isDirectory() ||
410 (file.getName().toLowerCase().endsWith(".png") ||
411 file.getName().toLowerCase().endsWith(".gif"));
412 }
413
414 // used for an internal timer that keeps repainting
415 public void actionPerformed(ActionEvent e) {
416 if (e.getSource() instanceof Timer) {
417 // redraw the screen at regular intervals to catch all paint operations
418 panel.repaint();
419 if (shouldDiff() &&
420 System.currentTimeMillis() > createTime + 4 * DELAY) {
421 String expected = System.getProperty(DIFF_PROPERTY);
422 try {
423 String actual = saveToTempFile();
424 DiffImage diff = new DiffImage(expected, actual);
425 diff.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
426 } catch (IOException ioe) {
427 System.err.println("Error diffing image: " + ioe);
428 }
429 timer.stop();
430 } else if (shouldSave() && readyToClose()) {
431 // auto-save-and-close if desired
432 try {
433 if (isAnimated()) {
434 saveAnimated(System.getProperty(SAVE_PROPERTY));
435 } else {
436 save(System.getProperty(SAVE_PROPERTY));
437 }
438 } catch (IOException ioe) {
439 System.err.println("Error saving image: " + ioe);
440 }
441 exit();
442 }
443 } else if (e.getActionCommand().equals("Exit")) {
444 exit();
445 } else if (e.getActionCommand().equals("Compare to File...")) {
446 compareToFile();
447 } else if (e.getActionCommand().equals("Compare to Web File...")) {
448 new Thread(new Runnable() {
449 public void run() {
450 compareToURL();
451 }
452 }).start();
453 } else if (e.getActionCommand().equals("Save As...")) {
454 saveAs();
455 } else if (e.getActionCommand().equals("Save Animated GIF...")) {
456 saveAsAnimated();
457 } else if (e.getActionCommand().equals("Zoom In")) {
458 zoom(currentZoom + 1);
459 } else if (e.getActionCommand().equals("Zoom Out")) {
460 zoom(currentZoom - 1);
461 } else if (e.getActionCommand().equals("Zoom Normal (100%)")) {
462 zoom(1);
463 } else if (e.getActionCommand().equals("Grid Lines")) {
464 setGridLines(((JCheckBoxMenuItem) e.getSource()).isSelected());
465 } else if (e.getActionCommand().equals("About...")) {
466 JOptionPane.showMessageDialog(frame,
467 "DrawingPanel\n" +
468 "Graphical library class to support Building Java Programs textbook\n" +
469 "written by Marty Stepp and Stuart Reges\n" +
470 "University of Washington\n\n" +
471 "please visit our web site at:\n" +
472 "http://www.buildingjavaprograms.com/",
473
474 "About DrawingPanel",
475 JOptionPane.INFORMATION_MESSAGE);
476 }
477 }
478
479 public void addKeyListener(KeyListener listener) {
480 frame.addKeyListener(listener);
481 }
482
483 public void addMouseListener(MouseListener listener) {
484 panel.addMouseListener(listener);
485 }
486
487 public void addMouseListener(MouseMotionListener listener) {
488 panel.addMouseMotionListener(listener);
489 }
490
491 public void addMouseMotionListener(MouseMotionListener listener) {
492 panel.addMouseMotionListener(listener);
493 }
494
495 public void addMouseListener(MouseInputListener listener) {
496 panel.addMouseListener(listener);
497 panel.addMouseMotionListener(listener);
498 }
499
500 // erases all drawn shapes/lines/colors from the panel
501 public void clear() {
502 int[] pixels = new int[width * height];
503 for (int i = 0; i < pixels.length; i++) {
504 pixels[i] = initialPixel;
505 }
506 image.setRGB(0, 0, width, height, pixels, 0, 1);
507 }
508
509 // erases all drawn shapes/lines/colors from the panel
510 public void clearWithoutRepaint() {
511 Graphics g = image.getGraphics();
512 g.setColor(backgroundColor);
513 g.fillRect(0, 0, getWidth(), getHeight());
514 }
515
516 // method of FileFilter interface
517 public String getDescription() {
518 return "Image files (*.png; *.gif)";
519 }
520
521 // obtain the Graphics object to draw on the panel
522 public Graphics2D getGraphics() {
523 return g2;
524 }
525
526 // returns the drawing panel's width in pixels
527 public int getHeight() {
528 return height;
529 }
530
531 // returns the drawing panel's pixel size (width, height) as a Dimension object
532 public Dimension getSize() {
533 return new Dimension(width, height);
534 }
535
536 // returns the drawing panel's width in pixels
537 public int getWidth() {
538 return width;
539 }
540
541 // returns panel's current zoom factor
542 public int getZoom() {
543 return currentZoom;
544 }
545
546 // listens to mouse dragging
547 public void mouseDragged(MouseEvent e) {}
548
549 // listens to mouse movement
550 public void mouseMoved(MouseEvent e) {
551 int x = e.getX() / currentZoom;
552 int y = e.getY() / currentZoom;
553 setStatusBarText("(" + x + ", " + y + ")");
554 }
555
556 // run on shutdown to save the image
557 public void run() {
558 if (DEBUG) System.out.println("Running shutdown hook");
559 try {
560 String filename = System.getProperty(SAVE_PROPERTY);
561 if (filename == null) {
562 filename = callingClassName + ".png";
563 }
564
565 if (isAnimated()) {
566 saveAnimated(filename);
567 } else {
568 save(filename);
569 }
570 } catch (SecurityException e) {
571 } catch (IOException e) {
572 System.err.println("Error saving image: " + e);
573 }
574 }
575
576 // take the current contents of the panel and write them to a file
577 public void save(String filename) throws IOException {
578 BufferedImage image2 = getImage();
579
580 // if zoomed, scale image before saving it
581 if (SAVE_SCALED_IMAGES && currentZoom != 1) {
582 BufferedImage zoomedImage = new BufferedImage(width * currentZoom, height * currentZoom, image.getType());
583 Graphics2D g = (Graphics2D) zoomedImage.getGraphics();
584 g.setColor(Color.BLACK);
585 if (PRETTY) {
586 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
587 }
588 g.scale(currentZoom, currentZoom);
589 g.drawImage(image2, 0, 0, imagePanel);
590 image2 = zoomedImage;
591 }
592
593 // if saving multiple panels, append number
594 // (e.g. output_*.png becomes output_1.png, output_2.png, etc.)
595 if (isMultiple()) {
596 filename = filename.replaceAll("\\*", String.valueOf(instanceNumber));
597 }
598
599 int lastDot = filename.lastIndexOf(".");
600 String extension = filename.substring(lastDot + 1);
601
602 // write file
603 // TODO: doesn't save background color I don't think
604 ImageIO.write(image2, extension, new File(filename));
605 }
606
607 // take the current contents of the panel and write them to a file
608 public void saveAnimated(String filename) throws IOException {
609 // add one more final frame
610 if (DEBUG) System.out.println("saveAnimated(" + filename + ")");
611 frames.add(new ImageFrame(getImage(), 5000));
612 // encoder.continueEncoding(stream, getImage(), 5000);
613
614 // Gif89Encoder gifenc = new Gif89Encoder();
615
616 // add each frame of animation to the encoder
617 try {
618 for (int i = 0; i < frames.size(); i++) {
619 ImageFrame imageFrame = frames.get(i);
620 encoder.addFrame(imageFrame.image);
621 encoder.getFrameAt(i).setDelay(imageFrame.delay);
622 imageFrame.image.flush();
623 frames.set(i, null);
624 }
625 } catch (OutOfMemoryError e) {
626 System.out.println("Out of memory when saving");
627 }
628
629 // gifenc.setComments(annotation);
630 // gifenc.setUniformDelay((int) Math.round(100 / frames_per_second));
631 // gifenc.setUniformDelay(DELAY);
632 // encoder.setBackground(backgroundColor);
633 encoder.setLoopCount(0);
634 encoder.encode(new FileOutputStream(filename));
635 }
636
637 // set the background color of the drawing panel
638 public void setBackground(Color c) {
639 Color oldBackgroundColor = backgroundColor;
640 backgroundColor = c;
641 if (isGraphical()) {
642 panel.setBackground(c);
643 imagePanel.setBackground(c);
644 }
645
646 // with animated images, need to palette-swap the old bg color for the new
647 // because there's no notion of transparency in a palettized 8-bit image
648 if (isAnimated()) {
649 replaceColor(image, oldBackgroundColor, c);
650 }
651 }
652
653 // Enables or disables the drawing of grid lines on top of the image to help
654 // with debugging sizes and coordinates.
655 public void setGridLines(boolean gridLines) {
656 this.gridLines = gridLines;
657 imagePanel.repaint();
658 }
659
660 // sets the drawing panel's height in pixels to the given value
661 // After calling this method, the client must call getGraphics() again
662 // to get the new graphics context of the newly enlarged image buffer.
663 public void setHeight(int height) {
664 setSize(getWidth(), height);
665 }
666
667 // sets the drawing panel's pixel size (width, height) to the given values
668 // After calling this method, the client must call getGraphics() again
669 // to get the new graphics context of the newly enlarged image buffer.
670 public void setSize(int width, int height) {
671 // replace the image buffer for drawing
672 BufferedImage newImage = new BufferedImage(width, height, image.getType());
673 imagePanel.setImage(newImage);
674 newImage.getGraphics().drawImage(image, 0, 0, imagePanel);
675
676 this.width = width;
677 this.height = height;
678 image = newImage;
679 g2 = (Graphics2D) newImage.getGraphics();
680 g2.setColor(Color.BLACK);
681 if (PRETTY) {
682 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
683 }
684 zoom(currentZoom);
685 if (isGraphical()) {
686 frame.pack();
687 }
688 }
689
690 // show or hide the drawing panel on the screen
691 public void setVisible(boolean visible) {
692 if (isGraphical()) {
693 frame.setVisible(visible);
694 }
695 }
696
697 // sets the drawing panel's width in pixels to the given value
698 // After calling this method, the client must call getGraphics() again
699 // to get the new graphics context of the newly enlarged image buffer.
700 public void setWidth(int width) {
701 setSize(width, getHeight());
702 }
703
704 // makes the program pause for the given amount of time,
705 // allowing for animation
706 public void sleep(int millis) {
707 if (isGraphical() && frame.isVisible()) {
708 // if not even displaying, we don't actually need to sleep
709 if (millis > 0) {
710 try {
711 Thread.sleep(millis);
712 panel.repaint();
713 toFront(frame);
714 } catch (Exception e) {}
715 }
716 }
717
718 // manually enable animation if necessary
719 if (!isAnimated() && !isMultiple() && autoEnableAnimationOnSleep()) {
720 animated = true;
721 initializeAnimation();
722 }
723
724 // capture a frame of animation
725 if (isAnimated() && shouldSave() && !isMultiple()) {
726 try {
727 if (frames.size() < MAX_FRAMES) {
728 frames.add(new ImageFrame(getImage(), millis));
729 }
730
731 // reset creation timer so that we won't save/close just yet
732 createTime = System.currentTimeMillis();
733 } catch (OutOfMemoryError e) {
734 System.out.println("Out of memory after capturing " + frames.size() + " frames");
735 }
736 }
737 }
738
739 // moves window on top of other windows
740 public void toFront() {
741 toFront(frame);
742 }
743
744 // called when DrawingPanel closes, to potentially exit the program
745 public void windowClosing(WindowEvent event) {
746 frame.setVisible(false);
747 synchronized (getClass()) {
748 instances--;
749 }
750 frame.dispose();
751 }
752
753 // methods required by WindowListener interface
754 public void windowActivated(WindowEvent event) {}
755 public void windowClosed(WindowEvent event) {}
756 public void windowDeactivated(WindowEvent event) {}
757 public void windowDeiconified(WindowEvent event) {}
758 public void windowIconified(WindowEvent event) {}
759 public void windowOpened(WindowEvent event) {}
760
761 // zooms the drawing panel in/out to the given factor
762 // factor should be >= 1
763 public void zoom(int zoomFactor) {
764 currentZoom = Math.max(1, zoomFactor);
765 if (isGraphical()) {
766 Dimension size = new Dimension(width * currentZoom, height * currentZoom);
767 imagePanel.setPreferredSize(size);
768 panel.setPreferredSize(size);
769 imagePanel.validate();
770 imagePanel.revalidate();
771 panel.validate();
772 panel.revalidate();
773 // imagePanel.setSize(size);
774 frame.getContentPane().validate();
775 imagePanel.repaint();
776 setStatusBarText(" ");
777
778 // resize frame if any more space for it exists or it's the wrong size
779 Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
780 if (size.width <= screen.width || size.height <= screen.height) {
781 frame.pack();
782 }
783
784 // if (size.width <= screen.width && size.height <= screen.height) {
785 // frame.pack();
786 // center(frame);
787 // }
788 }
789 }
790
791
792 // moves given jframe to center of screen
793 private void center(Window frame) {
794 Toolkit tk = Toolkit.getDefaultToolkit();
795 Dimension screen = tk.getScreenSize();
796
797 int x = Math.max(0, (screen.width - frame.getWidth()) / 2);
798 int y = Math.max(0, (screen.height - frame.getHeight()) / 2);
799 frame.setLocation(x, y);
800 }
801
802 // constructs and initializes JFileChooser object if necessary
803 private void checkChooser() {
804 if (chooser == null) {
805 // TODO: fix security on applet mode
806 chooser = new JFileChooser(System.getProperty("user.dir"));
807 chooser.setMultiSelectionEnabled(false);
808 chooser.setFileFilter(this);
809 }
810 }
811
812 // compares current DrawingPanel image to an image file on disk
813 private void compareToFile() {
814 // save current image to a temp file
815 try {
816 String tempFile = saveToTempFile();
817
818 // use file chooser dialog to find image to compare against
819 checkChooser();
820 if (chooser.showOpenDialog(frame) != JFileChooser.APPROVE_OPTION) {
821 return;
822 }
823
824 // user chose a file; let's diff it
825 new DiffImage(chooser.getSelectedFile().toString(), tempFile);
826 } catch (IOException ioe) {
827 JOptionPane.showMessageDialog(frame,
828 "Unable to compare images: \n" + ioe);
829 }
830 }
831
832 // compares current DrawingPanel image to an image file on the web
833 private void compareToURL() {
834 // save current image to a temp file
835 try {
836 String tempFile = saveToTempFile();
837
838 // get list of images to compare against from web site
839 if (COURSE_WEB_SITE == null || COURSE_WEB_SITE.length() == 0) {
840 return;
841 }
842 URL url = new URL(COURSE_WEB_SITE);
843 Scanner input = new Scanner(url.openStream());
844 List<String> lines = new ArrayList<String>();
845 List<String> filenames = new ArrayList<String>();
846 while (input.hasNextLine()) {
847 String line = input.nextLine().trim();
848 if (line.length() == 0) { continue; }
849
850 if (line.startsWith("#")) {
851 // a comment
852 if (line.endsWith(":")) {
853 // category label
854 lines.add(line);
855 line = line.replaceAll("#\\s*", "");
856 filenames.add(line);
857 }
858 } else {
859 lines.add(line);
860
861 // get filename
862 int lastSlash = line.lastIndexOf('/');
863 if (lastSlash >= 0) {
864 line = line.substring(lastSlash + 1);
865 }
866
867 // remove extension
868 int dot = line.lastIndexOf('.');
869 if (dot >= 0) {
870 line = line.substring(0, dot);
871 }
872
873 filenames.add(line);
874 }
875 }
876
877 if (filenames.isEmpty()) {
878 JOptionPane.showMessageDialog(frame,
879 "No valid web files found to compare against.",
880 "Error: no web files found",
881 JOptionPane.ERROR_MESSAGE);
882 return;
883 } else {
884 String fileURL = null;
885 if (filenames.size() == 1) {
886 // only one choice; take it
887 fileURL = lines.get(0);
888 } else {
889 // user chooses file to compare against
890 int choice = showOptionDialog(frame, "File to compare against?",
891 "Choose File", filenames.toArray(new String[0]));
892 if (choice < 0) {
893 return;
894 }
895
896 // user chose a file; let's diff it
897 fileURL = lines.get(choice);
898 }
899 if (DEBUG) System.out.println(fileURL);
900 new DiffImage(fileURL, tempFile);
901 }
902 } catch (NoRouteToHostException nrthe) {
903 JOptionPane.showMessageDialog(frame, "You do not appear to have a working internet connection.\nPlease check your internet settings and try again.\n\n" + nrthe);
904 } catch (UnknownHostException uhe) {
905 JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + uhe);
906 } catch (SocketException se) {
907 JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + se);
908 } catch (IOException ioe) {
909 JOptionPane.showMessageDialog(frame, "Unable to compare images: \n" + ioe);
910 }
911 }
912
913 // closes the frame and exits the program
914 private void exit() {
915 if (isGraphical()) {
916 frame.setVisible(false);
917 frame.dispose();
918 }
919 try {
920 System.exit(0);
921 } catch (SecurityException e) {
922 // if we're running in an applet or something, can't do System.exit
923 }
924 }
925
926 // returns a best guess about the name of the class that constructed this panel
927 private String getCallingClassName() {
928 StackTraceElement[] stack = new RuntimeException().getStackTrace();
929 String className = this.getClass().getName();
930 for (StackTraceElement element : stack) {
931 String cl = element.getClassName();
932 if (!className.equals(cl)) {
933 className = cl;
934 break;
935 }
936 }
937
938 return className;
939 }
940
941 private BufferedImage getImage() {
942 // create second image so we get the background color
943 BufferedImage image2;
944 if (isAnimated()) {
945 image2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
946 } else {
947 image2 = new BufferedImage(width, height, image.getType());
948 }
949 Graphics g = image2.getGraphics();
950 if (DEBUG) System.out.println("getImage setting background to " + backgroundColor);
951 g.setColor(backgroundColor);
952 g.fillRect(0, 0, width, height);
953 g.drawImage(image, 0, 0, panel);
954 return image2;
955 }
956
957 private void initializeAnimation() {
958 frames = new ArrayList<ImageFrame>();
959 encoder = new Gif89Encoder();
960 /*
961 try {
962 if (hasProperty(SAVE_PROPERTY)) {
963 stream = new FileOutputStream(System.getProperty(SAVE_PROPERTY));
964 }
965 // encoder.startEncoding(stream);
966 } catch (IOException e) {
967 System.out.println(e);
968 }
969 */
970 }
971
972 private boolean autoEnableAnimationOnSleep() {
973 return propertyIsTrue(AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY);
974 }
975
976 private boolean isAnimated() {
977 return animated || propertyIsTrue(ANIMATED_PROPERTY);
978 }
979
980 private boolean isGraphical() {
981 return !hasProperty(SAVE_PROPERTY) && !hasProperty(HEADLESS_PROPERTY);
982 }
983
984 private boolean isMultiple() {
985 return propertyIsTrue(MULTIPLE_PROPERTY);
986 }
987
988 private boolean readyToClose() {
989/*
990 if (isAnimated()) {
991 // wait a little longer, in case animation is sleeping
992 return System.currentTimeMillis() > createTime + 5 * DELAY;
993 } else {
994 return System.currentTimeMillis() > createTime + 4 * DELAY;
995 }
996*/
997 return (instances == 0 || shouldSave()) && !mainIsActive();
998 }
999
1000 private void replaceColor(BufferedImage image, Color oldColor, Color newColor) {
1001 int oldRGB = oldColor.getRGB();
1002 int newRGB = newColor.getRGB();
1003 for (int y = 0; y < image.getHeight(); y++) {
1004 for (int x = 0; x < image.getWidth(); x++) {
1005 if (image.getRGB(x, y) == oldRGB) {
1006 image.setRGB(x, y, newRGB);
1007 }
1008 }
1009 }
1010 }
1011
1012 // called when user presses "Save As" menu item
1013 private void saveAs() {
1014 String filename = saveAsHelper("png");
1015 if (filename != null) {
1016 try {
1017 save(filename); // save the file
1018 } catch (IOException ex) {
1019 JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex);
1020 }
1021 }
1022 }
1023
1024 private void saveAsAnimated() {
1025 String filename = saveAsHelper("gif");
1026 if (filename != null) {
1027 try {
1028 // record that the file should be saved next time
1029 PrintStream out = new PrintStream(new File(ANIMATION_FILE_NAME));
1030 out.println(filename);
1031 out.close();
1032
1033 JOptionPane.showMessageDialog(frame,
1034 "Due to constraints about how DrawingPanel works, you'll need to\n" +
1035 "re-run your program. When you run it the next time, DrawingPanel will \n" +
1036 "automatically save your animated image as: " + new File(filename).getName()
1037 );
1038 } catch (IOException ex) {
1039 JOptionPane.showMessageDialog(frame, "Unable to store animation settings:\n" + ex);
1040 }
1041 }
1042 }
1043
1044 private String saveAsHelper(String extension) {
1045 // use file chooser dialog to get filename to save into
1046 checkChooser();
1047 if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
1048 return null;
1049 }
1050
1051 File selectedFile = chooser.getSelectedFile();
1052 String filename = selectedFile.toString();
1053 if (!filename.toLowerCase().endsWith(extension)) {
1054 // Windows is dumb about extensions with file choosers
1055 filename += "." + extension;
1056 }
1057
1058 // confirm overwrite of file
1059 if (new File(filename).exists() && JOptionPane.showConfirmDialog(
1060 frame, "File exists. Overwrite?", "Overwrite?",
1061 JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
1062 return null;
1063 }
1064
1065 return filename;
1066 }
1067
1068 // saves DrawingPanel image to a temporary file and returns file's name
1069 private String saveToTempFile() throws IOException {
1070 File currentImageFile = File.createTempFile("current_image", ".png");
1071 save(currentImageFile.toString());
1072 return currentImageFile.toString();
1073 }
1074
1075 // sets the text that will appear in the bottom status bar
1076 private void setStatusBarText(String text) {
1077 if (currentZoom != 1) {
1078 text += " (current zoom: " + currentZoom + "x" + ")";
1079 }
1080 statusBar.setText(text);
1081 }
1082
1083 // initializes DrawingPanel's menu bar items
1084 private void setupMenuBar() {
1085 // abort compare if we're running as an applet or in a secure environment
1086 boolean secure = (System.getSecurityManager() != null);
1087
1088 JMenuItem saveAs = new JMenuItem("Save As...", 'A');
1089 saveAs.addActionListener(this);
1090 saveAs.setAccelerator(KeyStroke.getKeyStroke("ctrl S"));
1091 saveAs.setEnabled(!secure);
1092
1093 JMenuItem saveAnimated = new JMenuItem("Save Animated GIF...", 'G');
1094 saveAnimated.addActionListener(this);
1095 saveAnimated.setAccelerator(KeyStroke.getKeyStroke("ctrl A"));
1096 saveAnimated.setEnabled(!secure);
1097
1098 JMenuItem compare = new JMenuItem("Compare to File...", 'C');
1099 compare.addActionListener(this);
1100 // compare.setAccelerator(KeyStroke.getKeyStroke("ctrl C"));
1101 compare.setEnabled(!secure);
1102
1103 JMenuItem compareURL = new JMenuItem("Compare to Web File...", 'U');
1104 compareURL.addActionListener(this);
1105 compareURL.setAccelerator(KeyStroke.getKeyStroke("ctrl U"));
1106 compareURL.setEnabled(!secure);
1107
1108 JMenuItem zoomIn = new JMenuItem("Zoom In", 'I');
1109 zoomIn.addActionListener(this);
1110 zoomIn.setAccelerator(KeyStroke.getKeyStroke("ctrl EQUALS"));
1111
1112 JMenuItem zoomOut = new JMenuItem("Zoom Out", 'O');
1113 zoomOut.addActionListener(this);
1114 zoomOut.setAccelerator(KeyStroke.getKeyStroke("ctrl MINUS"));
1115
1116 JMenuItem zoomNormal = new JMenuItem("Zoom Normal (100%)", 'N');
1117 zoomNormal.addActionListener(this);
1118 zoomNormal.setAccelerator(KeyStroke.getKeyStroke("ctrl 0"));
1119
1120 JMenuItem gridLinesItem = new JCheckBoxMenuItem("Grid Lines");
1121 gridLinesItem.setMnemonic('G');
1122 gridLinesItem.addActionListener(this);
1123 gridLinesItem.setAccelerator(KeyStroke.getKeyStroke("ctrl G"));
1124
1125 JMenuItem exit = new JMenuItem("Exit", 'x');
1126 exit.addActionListener(this);
1127
1128 JMenuItem about = new JMenuItem("About...", 'A');
1129 about.addActionListener(this);
1130
1131 JMenu file = new JMenu("File");
1132 file.setMnemonic('F');
1133 file.add(compareURL);
1134 file.add(compare);
1135 file.addSeparator();
1136 file.add(saveAs);
1137 file.add(saveAnimated);
1138 file.addSeparator();
1139 file.add(exit);
1140
1141 JMenu view = new JMenu("View");
1142 view.setMnemonic('V');
1143 view.add(zoomIn);
1144 view.add(zoomOut);
1145 view.add(zoomNormal);
1146 view.addSeparator();
1147 view.add(gridLinesItem);
1148
1149 JMenu help = new JMenu("Help");
1150 help.setMnemonic('H');
1151 help.add(about);
1152
1153 JMenuBar bar = new JMenuBar();
1154 bar.add(file);
1155 bar.add(view);
1156 bar.add(help);
1157 frame.setJMenuBar(bar);
1158 }
1159
1160 private boolean shouldDiff() {
1161 return hasProperty(DIFF_PROPERTY);
1162 }
1163
1164 private boolean shouldSave() {
1165 return hasProperty(SAVE_PROPERTY);
1166 }
1167
1168 // show dialog box with given choices; return index chosen (-1 == cancel)
1169 private int showOptionDialog(Frame parent, String title,
1170 String message, final String[] names) {
1171 final JDialog dialog = new JDialog(parent, title, true);
1172 JPanel center = new JPanel(new GridLayout(0, 1));
1173
1174 // just a hack to make the return value a mutable reference to an int
1175 final int[] hack = {-1};
1176
1177 for (int i = 0; i < names.length; i++) {
1178 if (names[i].endsWith(":")) {
1179 center.add(new JLabel("<html><b>" + names[i] + "</b></html>"));
1180 } else {
1181 final JButton button = new JButton(names[i]);
1182 button.setActionCommand(String.valueOf(i));
1183 button.addActionListener(new ActionListener() {
1184 public void actionPerformed(ActionEvent e) {
1185 hack[0] = Integer.parseInt(button.getActionCommand());
1186 dialog.setVisible(false);
1187 }
1188 });
1189 center.add(button);
1190 }
1191 }
1192
1193 JPanel south = new JPanel();
1194 JButton cancel = new JButton("Cancel");
1195 cancel.setMnemonic('C');
1196 cancel.requestFocus();
1197 cancel.addActionListener(new ActionListener() {
1198 public void actionPerformed(ActionEvent e) {
1199 dialog.setVisible(false);
1200 }
1201 });
1202 south.add(cancel);
1203
1204 dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
1205 dialog.getContentPane().setLayout(new BorderLayout(10, 5));
1206 // ((JComponent) dialog.getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
1207
1208 if (message != null) {
1209 JLabel messageLabel = new JLabel(message);
1210 dialog.add(messageLabel, BorderLayout.NORTH);
1211 }
1212 dialog.add(center);
1213 dialog.add(south, BorderLayout.SOUTH);
1214 dialog.pack();
1215 dialog.setResizable(false);
1216 center(dialog);
1217 cancel.requestFocus();
1218 dialog.setVisible(true);
1219 cancel.requestFocus();
1220
1221 return hack[0];
1222 }
1223
1224 // brings the given window to the front of the z-ordering
1225 private void toFront(final Window window) {
1226 EventQueue.invokeLater(new Runnable() {
1227 public void run() {
1228 if (window != null) {
1229 window.toFront();
1230 window.repaint();
1231 }
1232 }
1233 });
1234 }
1235
1236
1237
1238 // Reports the differences between two images.
1239 private class DiffImage extends JPanel implements ActionListener,
1240 ChangeListener {
1241 private static final long serialVersionUID = 0;
1242
1243 private BufferedImage image1;
1244 private BufferedImage image2;
1245 private String image1name;
1246 private int numDiffPixels;
1247 private int opacity = 50;
1248 private String label1Text = "Expected";
1249 private String label2Text = "Actual";
1250 private boolean highlightDiffs = false;
1251
1252 private Color highlightColor = new Color(224, 0, 224);
1253 private JLabel image1Label;
1254 private JLabel image2Label;
1255 private JLabel diffPixelsLabel;
1256 private JSlider slider;
1257 private JCheckBox box;
1258 private JMenuItem saveAsItem;
1259 private JMenuItem setImage1Item;
1260 private JMenuItem setImage2Item;
1261 private JFrame frame;
1262 private JButton colorButton;
1263
1264 public DiffImage(String file1, String file2) throws IOException {
1265 setImage1(file1);
1266 setImage2(file2);
1267 display();
1268 }
1269
1270 public void actionPerformed(ActionEvent e) {
1271 Object source = e.getSource();
1272 if (source == box) {
1273 highlightDiffs = box.isSelected();
1274 repaint();
1275 } else if (source == colorButton) {
1276 Color color = JColorChooser.showDialog(frame,
1277 "Choose highlight color", highlightColor);
1278 if (color != null) {
1279 highlightColor = color;
1280 colorButton.setBackground(color);
1281 colorButton.setForeground(color);
1282 repaint();
1283 }
1284 } else if (source == saveAsItem) {
1285 saveAs();
1286 } else if (source == setImage1Item) {
1287 setImage1();
1288 } else if (source == setImage2Item) {
1289 setImage2();
1290 }
1291 }
1292
1293 // Counts number of pixels that differ between the two images.
1294 public void countDiffPixels() {
1295 if (image1 == null || image2 == null) {
1296 return;
1297 }
1298
1299 int w1 = image1.getWidth();
1300 int h1 = image1.getHeight();
1301 int w2 = image2.getWidth();
1302 int h2 = image2.getHeight();
1303 int wmax = Math.max(w1, w2);
1304 int hmax = Math.max(h1, h2);
1305
1306 // check each pair of pixels
1307 numDiffPixels = 0;
1308 for (int y = 0; y < hmax; y++) {
1309 for (int x = 0; x < wmax; x++) {
1310 int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0;
1311 int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0;
1312 if (pixel1 != pixel2) {
1313 numDiffPixels++;
1314 }
1315 }
1316 }
1317 }
1318
1319 // initializes diffimage panel
1320 public void display() {
1321 countDiffPixels();
1322
1323 setupComponents();
1324 setupEvents();
1325 setupLayout();
1326
1327 frame.pack();
1328 center(frame);
1329
1330 frame.setVisible(true);
1331 toFront(frame);
1332 }
1333
1334 // draws the given image onto the given graphics context
1335 public void drawImageFull(Graphics2D g2, BufferedImage image) {
1336 int iw = image.getWidth();
1337 int ih = image.getHeight();
1338 int w = getWidth();
1339 int h = getHeight();
1340 int dw = w - iw;
1341 int dh = h - ih;
1342
1343 if (dw > 0) {
1344 g2.fillRect(iw, 0, dw, ih);
1345 }
1346 if (dh > 0) {
1347 g2.fillRect(0, ih, iw, dh);
1348 }
1349 if (dw > 0 && dh > 0) {
1350 g2.fillRect(iw, ih, dw, dh);
1351 }
1352 g2.drawImage(image, 0, 0, this);
1353 }
1354
1355 // paints the DiffImage panel
1356 public void paintComponent(Graphics g) {
1357 super.paintComponent(g);
1358 Graphics2D g2 = (Graphics2D) g;
1359
1360 // draw the expected output (image 1)
1361 if (image1 != null) {
1362 drawImageFull(g2, image1);
1363 }
1364
1365 // draw the actual output (image 2)
1366 if (image2 != null) {
1367 Composite oldComposite = g2.getComposite();
1368 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, ((float) opacity) / 100));
1369 drawImageFull(g2, image2);
1370 g2.setComposite(oldComposite);
1371 }
1372 g2.setColor(Color.BLACK);
1373
1374 // draw the highlighted diffs (if so desired)
1375 if (highlightDiffs && image1 != null && image2 != null) {
1376 int w1 = image1.getWidth();
1377 int h1 = image1.getHeight();
1378 int w2 = image2.getWidth();
1379 int h2 = image2.getHeight();
1380
1381 int wmax = Math.max(w1, w2);
1382 int hmax = Math.max(h1, h2);
1383
1384 // check each pair of pixels
1385 g2.setColor(highlightColor);
1386 for (int y = 0; y < hmax; y++) {
1387 for (int x = 0; x < wmax; x++) {
1388 int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0;
1389 int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0;
1390 if (pixel1 != pixel2) {
1391 g2.fillRect(x, y, 1, 1);
1392 }
1393 }
1394 }
1395 }
1396 }
1397
1398 public void save(File file) throws IOException {
1399 // String extension = filename.substring(filename.lastIndexOf(".") + 1);
1400 // ImageIO.write(diffImage, extension, new File(filename));
1401 String filename = file.getName();
1402 String extension = filename.substring(filename.lastIndexOf(".") + 1);
1403 BufferedImage img = new BufferedImage(getPreferredSize().width, getPreferredSize().height, BufferedImage.TYPE_INT_ARGB);
1404 img.getGraphics().setColor(getBackground());
1405 img.getGraphics().fillRect(0, 0, img.getWidth(), img.getHeight());
1406 paintComponent(img.getGraphics());
1407 ImageIO.write(img, extension, file);
1408 }
1409
1410 public void save(String filename) throws IOException {
1411 save(new File(filename));
1412 }
1413
1414 // Called when "Save As" menu item is clicked
1415 public void saveAs() {
1416 checkChooser();
1417 if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
1418 return;
1419 }
1420
1421 File selectedFile = chooser.getSelectedFile();
1422 try {
1423 save(selectedFile.toString());
1424 } catch (IOException ex) {
1425 JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex);
1426 }
1427 }
1428
1429 // called when "Set Image 1" menu item is clicked
1430 public void setImage1() {
1431 checkChooser();
1432 if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
1433 return;
1434 }
1435
1436 File selectedFile = chooser.getSelectedFile();
1437 try {
1438 setImage1(selectedFile.toString());
1439 countDiffPixels();
1440 diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)");
1441 image1Label.setText(selectedFile.getName());
1442 frame.pack();
1443 } catch (IOException ex) {
1444 JOptionPane.showMessageDialog(frame, "Unable to set image 1:\n" + ex);
1445 }
1446 }
1447
1448 // sets image 1 to be the given image
1449 public void setImage1(BufferedImage image) {
1450 if (image == null) {
1451 throw new NullPointerException();
1452 }
1453
1454 image1 = image;
1455 setPreferredSize(new Dimension(
1456 Math.max(getPreferredSize().width, image.getWidth()),
1457 Math.max(getPreferredSize().height, image.getHeight()))
1458 );
1459 if (frame != null) {
1460 frame.pack();
1461 }
1462 repaint();
1463 }
1464
1465 // loads image 1 from the given filename or URL
1466 public void setImage1(String filename) throws IOException {
1467 image1name = new File(filename).getName();
1468 if (filename.startsWith("http")) {
1469 setImage1(ImageIO.read(new URL(filename)));
1470 } else {
1471 setImage1(ImageIO.read(new File(filename)));
1472 }
1473 }
1474
1475 // called when "Set Image 2" menu item is clicked
1476 public void setImage2() {
1477 checkChooser();
1478 if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
1479 return;
1480 }
1481
1482 File selectedFile = chooser.getSelectedFile();
1483 try {
1484 setImage2(selectedFile.toString());
1485 countDiffPixels();
1486 diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)");
1487 image2Label.setText(selectedFile.getName());
1488 frame.pack();
1489 } catch (IOException ex) {
1490 JOptionPane.showMessageDialog(frame, "Unable to set image 2:\n" + ex);
1491 }
1492 }
1493
1494 // sets image 2 to be the given image
1495 public void setImage2(BufferedImage image) {
1496 if (image == null) {
1497 throw new NullPointerException();
1498 }
1499
1500 image2 = image;
1501 setPreferredSize(new Dimension(
1502 Math.max(getPreferredSize().width, image.getWidth()),
1503 Math.max(getPreferredSize().height, image.getHeight()))
1504 );
1505 if (frame != null) {
1506 frame.pack();
1507 }
1508 repaint();
1509 }
1510
1511 // loads image 2 from the given filename
1512 public void setImage2(String filename) throws IOException {
1513 if (filename.startsWith("http")) {
1514 setImage2(ImageIO.read(new URL(filename)));
1515 } else {
1516 setImage2(ImageIO.read(new File(filename)));
1517 }
1518
1519 }
1520
1521 private void setupComponents() {
1522 String title = "DiffImage";
1523 if (image1name != null) {
1524 title = "Compare to " + image1name;
1525 }
1526 frame = new JFrame(title);
1527 frame.setResizable(false);
1528 // frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
1529
1530 slider = new JSlider();
1531 slider.setPaintLabels(false);
1532 slider.setPaintTicks(true);
1533 slider.setSnapToTicks(true);
1534 slider.setMajorTickSpacing(25);
1535 slider.setMinorTickSpacing(5);
1536
1537 box = new JCheckBox("Highlight diffs in color: ", highlightDiffs);
1538
1539 colorButton = new JButton();
1540 colorButton.setBackground(highlightColor);
1541 colorButton.setForeground(highlightColor);
1542 colorButton.setPreferredSize(new Dimension(24, 24));
1543
1544 diffPixelsLabel = new JLabel("(" + numDiffPixels + " pixels differ)");
1545 diffPixelsLabel.setFont(diffPixelsLabel.getFont().deriveFont(Font.BOLD));
1546 image1Label = new JLabel(label1Text);
1547 image2Label = new JLabel(label2Text);
1548
1549 setupMenuBar();
1550 }
1551
1552 // initializes layout of components
1553 private void setupLayout() {
1554 JPanel southPanel1 = new JPanel();
1555 southPanel1.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
1556 southPanel1.add(image1Label);
1557 southPanel1.add(slider);
1558 southPanel1.add(image2Label);
1559 southPanel1.add(Box.createHorizontalStrut(20));
1560
1561 JPanel southPanel2 = new JPanel();
1562 southPanel2.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
1563 southPanel2.add(diffPixelsLabel);
1564 southPanel2.add(Box.createHorizontalStrut(20));
1565 southPanel2.add(box);
1566 southPanel2.add(colorButton);
1567
1568 Container southPanel = javax.swing.Box.createVerticalBox();
1569 southPanel.add(southPanel1);
1570 southPanel.add(southPanel2);
1571
1572 frame.add(this, BorderLayout.CENTER);
1573 frame.add(southPanel, BorderLayout.SOUTH);
1574 }
1575
1576 // initializes main menu bar
1577 private void setupMenuBar() {
1578 saveAsItem = new JMenuItem("Save As...", 'A');
1579 saveAsItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S"));
1580 setImage1Item = new JMenuItem("Set Image 1...", '1');
1581 setImage1Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 1"));
1582 setImage2Item = new JMenuItem("Set Image 2...", '2');
1583 setImage2Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 2"));
1584
1585 JMenu file = new JMenu("File");
1586 file.setMnemonic('F');
1587 file.add(setImage1Item);
1588 file.add(setImage2Item);
1589 file.addSeparator();
1590 file.add(saveAsItem);
1591
1592 JMenuBar bar = new JMenuBar();
1593 bar.add(file);
1594
1595 // disabling menu bar to simplify code
1596 // frame.setJMenuBar(bar);
1597 }
1598
1599 // method of ChangeListener interface
1600 public void stateChanged(ChangeEvent e) {
1601 opacity = slider.getValue();
1602 repaint();
1603 }
1604
1605 // adds event listeners to various components
1606 private void setupEvents() {
1607 slider.addChangeListener(this);
1608 box.addActionListener(this);
1609 colorButton.addActionListener(this);
1610 saveAsItem.addActionListener(this);
1611 this.setImage1Item.addActionListener(this);
1612 this.setImage2Item.addActionListener(this);
1613 }
1614 }
1615
1616
1617
1618 //******************************************************************************
1619 // DirectGif89Frame.java
1620 //******************************************************************************
1621
1622 //==============================================================================
1623 /** Instances of this Gif89Frame subclass are constructed from RGB image info,
1624 * either in the form of an Image object or a pixel array.
1625 * <p>
1626 * There is an important restriction to note. It is only permissible to add
1627 * DirectGif89Frame objects to a Gif89Encoder constructed without an explicit
1628 * color map. The GIF color table will be automatically generated from pixel
1629 * information.
1630 *
1631 * @version 0.90 beta (15-Jul-2000)
1632 * @author J. M. G. Elliott (tep@jmge.net)
1633 * @see Gif89Encoder
1634 * @see Gif89Frame
1635 * @see IndexGif89Frame
1636 */
1637 class DirectGif89Frame extends Gif89Frame {
1638
1639 private int[] argbPixels;
1640
1641 //----------------------------------------------------------------------------
1642 /** Construct an DirectGif89Frame from a Java image.
1643 *
1644 * @param img
1645 * A java.awt.Image object that supports pixel-grabbing.
1646 * @exception IOException
1647 * If the image is unencodable due to failure of pixel-grabbing.
1648 */
1649 public DirectGif89Frame(Image img) throws IOException
1650 {
1651 PixelGrabber pg = new PixelGrabber(img, 0, 0, -1, -1, true);
1652
1653 String errmsg = null;
1654 try {
1655 if (!pg.grabPixels())
1656 errmsg = "can't grab pixels from image";
1657 } catch (InterruptedException e) {
1658 errmsg = "interrupted grabbing pixels from image";
1659 }
1660
1661 if (errmsg != null)
1662 throw new IOException(errmsg + " (" + getClass().getName() + ")");
1663
1664 theWidth = pg.getWidth();
1665 theHeight = pg.getHeight();
1666 argbPixels = (int[]) pg.getPixels();
1667 ciPixels = new byte[argbPixels.length];
1668
1669 // flush to conserve resources
1670 img.flush();
1671 }
1672
1673 //----------------------------------------------------------------------------
1674 /** Construct an DirectGif89Frame from ARGB pixel data.
1675 *
1676 * @param width
1677 * Width of the bitmap.
1678 * @param height
1679 * Height of the bitmap.
1680 * @param argb_pixels
1681 * Array containing at least width*height pixels in the format returned by
1682 * java.awt.Color.getRGB().
1683 */
1684 public DirectGif89Frame(int width, int height, int argb_pixels[])
1685 {
1686 theWidth = width;
1687 theHeight = height;
1688 argbPixels = new int[theWidth * theHeight];
1689 System.arraycopy(argb_pixels, 0, argbPixels, 0, argbPixels.length);
1690 ciPixels = new byte[argbPixels.length];
1691 }
1692
1693 //----------------------------------------------------------------------------
1694 Object getPixelSource() { return argbPixels; }
1695 }
1696
1697
1698
1699 //******************************************************************************
1700 // Gif89Encoder.java
1701 //******************************************************************************
1702
1703 //==============================================================================
1704 /** This is the central class of a JDK 1.1 compatible GIF encoder that, AFAIK,
1705 * supports more features of the extended GIF spec than any other Java open
1706 * source encoder. Some sections of the source are lifted or adapted from Jef
1707 * Poskanzer's <cite>Acme GifEncoder</cite> (so please see the
1708 * <a href="../readme.txt">readme</a> containing his notice), but much of it,
1709 * including nearly all of the present class, is original code. My main
1710 * motivation for writing a new encoder was to support animated GIFs, but the
1711 * package also adds support for embedded textual comments.
1712 * <p>
1713 * There are still some limitations. For instance, animations are limited to
1714 * a single global color table. But that is usually what you want anyway, so
1715 * as to avoid irregularities on some displays. (So this is not really a
1716 * limitation, but a "disciplinary feature" :) Another rather more serious
1717 * restriction is that the total number of RGB colors in a given input-batch
1718 * mustn't exceed 256. Obviously, there is an opening here for someone who
1719 * would like to add a color-reducing preprocessor.
1720 * <p>
1721 * The encoder, though very usable in its present form, is at bottom only a
1722 * partial implementation skewed toward my own particular needs. Hence a
1723 * couple of caveats are in order. (1) During development it was in the back
1724 * of my mind that an encoder object should be reusable - i.e., you should be
1725 * able to make multiple calls to encode() on the same object, with or without
1726 * intervening frame additions or changes to options. But I haven't reviewed
1727 * the code with such usage in mind, much less tested it, so it's likely I
1728 * overlooked something. (2) The encoder classes aren't thread safe, so use
1729 * caution in a context where access is shared by multiple threads. (Better
1730 * yet, finish the library and re-release it :)
1731 * <p>
1732 * There follow a couple of simple examples illustrating the most common way to
1733 * use the encoder, i.e., to encode AWT Image objects created elsewhere in the
1734 * program. Use of some of the most popular format options is also shown,
1735 * though you will want to peruse the API for additional features.
1736 *
1737 * <p>
1738 * <strong>Animated GIF Example</strong>
1739 * <pre>
1740 * import net.jmge.gif.Gif89Encoder;
1741 * // ...
1742 * void writeAnimatedGIF(Image[] still_images,
1743 * String annotation,
1744 * boolean looped,
1745 * double frames_per_second,
1746 * OutputStream out) throws IOException
1747 * {
1748 * Gif89Encoder gifenc = new Gif89Encoder();
1749 * for (int i = 0; i < still_images.length; ++i)
1750 * gifenc.addFrame(still_images[i]);
1751 * gifenc.setComments(annotation);
1752 * gifenc.setLoopCount(looped ? 0 : 1);
1753 * gifenc.setUniformDelay((int) Math.round(100 / frames_per_second));
1754 * gifenc.encode(out);
1755 * }
1756 * </pre>
1757 *
1758 * <strong>Static GIF Example</strong>
1759 * <pre>
1760 * import net.jmge.gif.Gif89Encoder;
1761 * // ...
1762 * void writeNormalGIF(Image img,
1763 * String annotation,
1764 * int transparent_index, // pass -1 for none
1765 * boolean interlaced,
1766 * OutputStream out) throws IOException
1767 * {
1768 * Gif89Encoder gifenc = new Gif89Encoder(img);
1769 * gifenc.setComments(annotation);
1770 * gifenc.setTransparentIndex(transparent_index);
1771 * gifenc.getFrameAt(0).setInterlaced(interlaced);
1772 * gifenc.encode(out);
1773 * }
1774 * </pre>
1775 *
1776 * @version 0.90 beta (15-Jul-2000)
1777 * @author J. M. G. Elliott (tep@jmge.net)
1778 * @see Gif89Frame
1779 * @see DirectGif89Frame
1780 * @see IndexGif89Frame
1781 */
1782 class Gif89Encoder {
1783 private static final boolean DEBUG = false;
1784 private Dimension dispDim = new Dimension(0, 0);
1785 private GifColorTable colorTable;
1786 private int bgIndex = 0;
1787 private int loopCount = 1;
1788 private String theComments;
1789 private Vector<Gif89Frame> vFrames = new Vector<Gif89Frame>();
1790
1791 //----------------------------------------------------------------------------
1792 /** Use this default constructor if you'll be adding multiple frames
1793 * constructed from RGB data (i.e., AWT Image objects or ARGB-pixel arrays).
1794 */
1795 public Gif89Encoder()
1796 {
1797 // empty color table puts us into "palette autodetect" mode
1798 colorTable = new GifColorTable();
1799 }
1800
1801 //----------------------------------------------------------------------------
1802 /** Like the default except that it also adds a single frame, for conveniently
1803 * encoding a static GIF from an image.
1804 *
1805 * @param static_image
1806 * Any Image object that supports pixel-grabbing.
1807 * @exception IOException
1808 * See the addFrame() methods.
1809 */
1810 public Gif89Encoder(Image static_image) throws IOException
1811 {
1812 this();
1813 addFrame(static_image);
1814 }
1815
1816 //----------------------------------------------------------------------------
1817 /** This constructor installs a user color table, overriding the detection of
1818 * of a palette from ARBG pixels.
1819 *
1820 * Use of this constructor imposes a couple of restrictions:
1821 * (1) Frame objects can't be of type DirectGif89Frame
1822 * (2) Transparency, if desired, must be set explicitly.
1823 *
1824 * @param colors
1825 * Array of color values; no more than 256 colors will be read, since that's
1826 * the limit for a GIF.
1827 */
1828 public Gif89Encoder(Color[] colors)
1829 {
1830 colorTable = new GifColorTable(colors);
1831 }
1832
1833 //----------------------------------------------------------------------------
1834 /** Convenience constructor for encoding a static GIF from index-model data.
1835 * Adds a single frame as specified.
1836 *
1837 * @param colors
1838 * Array of color values; no more than 256 colors will be read, since
1839 * that's the limit for a GIF.
1840 * @param width
1841 * Width of the GIF bitmap.
1842 * @param height
1843 * Height of same.
1844 * @param ci_pixels
1845 * Array of color-index pixels no less than width * height in length.
1846 * @exception IOException
1847 * See the addFrame() methods.
1848 */
1849 public Gif89Encoder(Color[] colors, int width, int height, byte ci_pixels[])
1850 throws IOException
1851 {
1852 this(colors);
1853 addFrame(width, height, ci_pixels);
1854 }
1855
1856 //----------------------------------------------------------------------------
1857 /** Get the number of frames that have been added so far.
1858 *
1859 * @return
1860 * Number of frame items.
1861 */
1862 public int getFrameCount() { return vFrames.size(); }
1863
1864 //----------------------------------------------------------------------------
1865 /** Get a reference back to a Gif89Frame object by position.
1866 *
1867 * @param index
1868 * Zero-based index of the frame in the sequence.
1869 * @return
1870 * Gif89Frame object at the specified position (or null if no such frame).
1871 */
1872 public Gif89Frame getFrameAt(int index)
1873 {
1874 return isOk(index) ? vFrames.elementAt(index) : null;
1875 }
1876
1877 //----------------------------------------------------------------------------
1878 /** Add a Gif89Frame frame to the end of the internal sequence. Note that
1879 * there are restrictions on the Gif89Frame type: if the encoder object was
1880 * constructed with an explicit color table, an attempt to add a
1881 * DirectGif89Frame will throw an exception.
1882 *
1883 * @param gf
1884 * An externally constructed Gif89Frame.
1885 * @exception IOException
1886 * If Gif89Frame can't be accommodated. This could happen if either (1) the
1887 * aggregate cross-frame RGB color count exceeds 256, or (2) the Gif89Frame
1888 * subclass is incompatible with the present encoder object.
1889 */
1890 public void addFrame(Gif89Frame gf) throws IOException
1891 {
1892 accommodateFrame(gf);
1893 vFrames.addElement(gf);
1894 }
1895
1896 //----------------------------------------------------------------------------
1897 /** Convenience version of addFrame() that takes a Java Image, internally
1898 * constructing the requisite DirectGif89Frame.
1899 *
1900 * @param image
1901 * Any Image object that supports pixel-grabbing.
1902 * @exception IOException
1903 * If either (1) pixel-grabbing fails, (2) the aggregate cross-frame RGB
1904 * color count exceeds 256, or (3) this encoder object was constructed with
1905 * an explicit color table.
1906 */
1907 public void addFrame(Image image) throws IOException
1908 {
1909 DirectGif89Frame frame = new DirectGif89Frame(image);
1910 addFrame(frame);
1911 }
1912
1913 //----------------------------------------------------------------------------
1914 /** The index-model convenience version of addFrame().
1915 *
1916 * @param width
1917 * Width of the GIF bitmap.
1918 * @param height
1919 * Height of same.
1920 * @param ci_pixels
1921 * Array of color-index pixels no less than width * height in length.
1922 * @exception IOException
1923 * Actually, in the present implementation, there aren't any unchecked
1924 * exceptions that can be thrown when adding an IndexGif89Frame
1925 * <i>per se</i>. But I might add some pedantic check later, to justify the
1926 * generality :)
1927 */
1928 public void addFrame(int width, int height, byte ci_pixels[])
1929 throws IOException
1930 {
1931 addFrame(new IndexGif89Frame(width, height, ci_pixels));
1932 }
1933
1934 //----------------------------------------------------------------------------
1935 /** Like addFrame() except that the frame is inserted at a specific point in
1936 * the sequence rather than appended.
1937 *
1938 * @param index
1939 * Zero-based index at which to insert frame.
1940 * @param gf
1941 * An externally constructed Gif89Frame.
1942 * @exception IOException
1943 * If Gif89Frame can't be accommodated. This could happen if either (1)
1944 * the aggregate cross-frame RGB color count exceeds 256, or (2) the
1945 * Gif89Frame subclass is incompatible with the present encoder object.
1946 */
1947 public void insertFrame(int index, Gif89Frame gf) throws IOException
1948 {
1949 accommodateFrame(gf);
1950 vFrames.insertElementAt(gf, index);
1951 }
1952
1953 //----------------------------------------------------------------------------
1954 /** Set the color table index for the transparent color, if any.
1955 *
1956 * @param index
1957 * Index of the color that should be rendered as transparent, if any.
1958 * A value of -1 turns off transparency. (Default: -1)
1959 */
1960 public void setTransparentIndex(int index)
1961 {
1962 colorTable.setTransparent(index);
1963 }
1964
1965 //----------------------------------------------------------------------------
1966 /** Sets attributes of the multi-image display area, if applicable.
1967 *
1968 * @param dim
1969 * Width/height of display. (Default: largest detected frame size)
1970 * @param background
1971 * Color table index of background color. (Default: 0)
1972 * @see Gif89Frame#setPosition
1973 */
1974 public void setLogicalDisplay(Dimension dim, int background)
1975 {
1976 dispDim = new Dimension(dim);
1977 bgIndex = background;
1978 }
1979
1980 //----------------------------------------------------------------------------
1981 /** Set animation looping parameter, if applicable.
1982 *
1983 * @param count
1984 * Number of times to play sequence. Special value of 0 specifies
1985 * indefinite looping. (Default: 1)
1986 */
1987 public void setLoopCount(int count)
1988 {
1989 loopCount = count;
1990 }
1991
1992 //----------------------------------------------------------------------------
1993 /** Specify some textual comments to be embedded in GIF.
1994 *
1995 * @param comments
1996 * String containing ASCII comments.
1997 */
1998 public void setComments(String comments)
1999 {
2000 theComments = comments;
2001 }
2002
2003 //----------------------------------------------------------------------------
2004 /** A convenience method for setting the "animation speed". It simply sets
2005 * the delay parameter for each frame in the sequence to the supplied value.
2006 * Since this is actually frame-level rather than animation-level data, take
2007 * care to add your frames before calling this method.
2008 *
2009 * @param interval
2010 * Interframe interval in centiseconds.
2011 */
2012 public void setUniformDelay(int interval)
2013 {
2014 for (int i = 0; i < vFrames.size(); ++i)
2015 vFrames.elementAt(i).setDelay(interval);
2016 }
2017
2018 //----------------------------------------------------------------------------
2019 /** After adding your frame(s) and setting your options, simply call this
2020 * method to write the GIF to the passed stream. Multiple calls are
2021 * permissible if for some reason that is useful to your application. (The
2022 * method simply encodes the current state of the object with no thought
2023 * to previous calls.)
2024 *
2025 * @param out
2026 * The stream you want the GIF written to.
2027 * @exception IOException
2028 * If a write error is encountered.
2029 */
2030 public void encode(OutputStream out) throws IOException
2031 {
2032 int nframes = getFrameCount();
2033 boolean is_sequence = nframes > 1;
2034
2035 // N.B. must be called before writing screen descriptor
2036 colorTable.closePixelProcessing();
2037
2038 // write GIF HEADER
2039 putAscii("GIF89a", out);
2040
2041 // write global blocks
2042 writeLogicalScreenDescriptor(out);
2043 colorTable.encode(out);
2044 if (is_sequence && loopCount != 1)
2045 writeNetscapeExtension(out);
2046 if (theComments != null && theComments.length() > 0)
2047 writeCommentExtension(out);
2048
2049 // write out the control and rendering data for each frame
2050 for (int i = 0; i < nframes; ++i) {
2051 DirectGif89Frame frame = (DirectGif89Frame) vFrames.elementAt(i);
2052 frame.encode(out, is_sequence, colorTable.getDepth(), colorTable.getTransparent());
2053 vFrames.set(i, null); // for GC's sake
2054 System.gc();
2055 }
2056
2057 // write GIF TRAILER
2058 out.write((int) ';');
2059
2060 out.flush();
2061 }
2062
2063 public boolean hasStarted = false;
2064
2065 //----------------------------------------------------------------------------
2066 /** After adding your frame(s) and setting your options, simply call this
2067 * method to write the GIF to the passed stream. Multiple calls are
2068 * permissible if for some reason that is useful to your application. (The
2069 * method simply encodes the current state of the object with no thought
2070 * to previous calls.)
2071 *
2072 * @param out
2073 * The stream you want the GIF written to.
2074 * @exception IOException
2075 * If a write error is encountered.
2076 */
2077 public void startEncoding(OutputStream out, Image image, int delay) throws IOException
2078 {
2079 hasStarted = true;
2080 boolean is_sequence = true;
2081 Gif89Frame gf = new DirectGif89Frame(image);
2082 accommodateFrame(gf);
2083
2084 // N.B. must be called before writing screen descriptor
2085 colorTable.closePixelProcessing();
2086
2087 // write GIF HEADER
2088 putAscii("GIF89a", out);
2089
2090 // write global blocks
2091 writeLogicalScreenDescriptor(out);
2092 colorTable.encode(out);
2093 if (is_sequence && loopCount != 1)
2094 writeNetscapeExtension(out);
2095 if (theComments != null && theComments.length() > 0)
2096 writeCommentExtension(out);
2097 }
2098
2099 public void continueEncoding(OutputStream out, Image image, int delay) throws IOException {
2100 // write out the control and rendering data for each frame
2101 Gif89Frame gf = new DirectGif89Frame(image);
2102 accommodateFrame(gf);
2103 gf.encode(out, true, colorTable.getDepth(), colorTable.getTransparent());
2104 out.flush();
2105 image.flush();
2106 }
2107
2108 public void endEncoding(OutputStream out) throws IOException {
2109 // write GIF TRAILER
2110 out.write((int) ';');
2111
2112 out.flush();
2113 }
2114
2115 public void setBackground(Color color) {
2116 bgIndex = colorTable.indexOf(color);
2117 if (bgIndex < 0) {
2118 try {
2119 BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED);
2120 Graphics g = img.getGraphics();
2121 g.setColor(color);
2122 g.fillRect(0, 0, 2, 2);
2123 DirectGif89Frame frame = new DirectGif89Frame(img);
2124 accommodateFrame(frame);
2125 bgIndex = colorTable.indexOf(color);
2126 } catch (IOException e) {
2127 if (DEBUG) System.out.println("Error while setting background color: " + e);
2128 }
2129 }
2130 if (DEBUG) System.out.println("Setting bg index to " + bgIndex);
2131 }
2132
2133 //----------------------------------------------------------------------------
2134 private void accommodateFrame(Gif89Frame gf) throws IOException
2135 {
2136 dispDim.width = Math.max(dispDim.width, gf.getWidth());
2137 dispDim.height = Math.max(dispDim.height, gf.getHeight());
2138 colorTable.processPixels(gf);
2139 }
2140
2141 //----------------------------------------------------------------------------
2142 private void writeLogicalScreenDescriptor(OutputStream os) throws IOException
2143 {
2144 putShort(dispDim.width, os);
2145 putShort(dispDim.height, os);
2146
2147 // write 4 fields, packed into a byte (bitfieldsize:value)
2148 // global color map present? (1:1)
2149 // bits per primary color less 1 (3:7)
2150 // sorted color table? (1:0)
2151 // bits per pixel less 1 (3:varies)
2152 os.write(0xf0 | colorTable.getDepth() - 1);
2153
2154 // write background color index
2155 os.write(bgIndex);
2156
2157 // Jef Poskanzer's notes on the next field, for our possible edification:
2158 // Pixel aspect ratio - 1:1.
2159 //Putbyte( (byte) 49, outs );
2160 // Java's GIF reader currently has a bug, if the aspect ratio byte is
2161 // not zero it throws an ImageFormatException. It doesn't know that
2162 // 49 means a 1:1 aspect ratio. Well, whatever, zero works with all
2163 // the other decoders I've tried so it probably doesn't hurt.
2164
2165 // OK, if it's good enough for Jef, it's definitely good enough for us:
2166 os.write(0);
2167 }
2168
2169 //----------------------------------------------------------------------------
2170 private void writeNetscapeExtension(OutputStream os) throws IOException
2171 {
2172 // n.b. most software seems to interpret the count as a repeat count
2173 // (i.e., interations beyond 1) rather than as an iteration count
2174 // (thus, to avoid repeating we have to omit the whole extension)
2175
2176 os.write((int) '!'); // GIF Extension Introducer
2177 os.write(0xff); // Application Extension Label
2178
2179 os.write(11); // application ID block size
2180 putAscii("NETSCAPE2.0", os); // application ID data
2181
2182 os.write(3); // data sub-block size
2183 os.write(1); // a looping flag? dunno
2184
2185 // we finally write the relevent data
2186 putShort(loopCount > 1 ? loopCount - 1 : 0, os);
2187
2188 os.write(0); // block terminator
2189 }
2190
2191 //----------------------------------------------------------------------------
2192 private void writeCommentExtension(OutputStream os) throws IOException
2193 {
2194 os.write((int) '!'); // GIF Extension Introducer
2195 os.write(0xfe); // Comment Extension Label
2196
2197 int remainder = theComments.length() % 255;
2198 int nsubblocks_full = theComments.length() / 255;
2199 int nsubblocks = nsubblocks_full + (remainder > 0 ? 1 : 0);
2200 int ibyte = 0;
2201 for (int isb = 0; isb < nsubblocks; ++isb)
2202 {
2203 int size = isb < nsubblocks_full ? 255 : remainder;
2204
2205 os.write(size);
2206 putAscii(theComments.substring(ibyte, ibyte + size), os);
2207 ibyte += size;
2208 }
2209
2210 os.write(0); // block terminator
2211 }
2212
2213 //----------------------------------------------------------------------------
2214 private boolean isOk(int frame_index)
2215 {
2216 return frame_index >= 0 && frame_index < vFrames.size();
2217 }
2218 }
2219
2220 //==============================================================================
2221 class GifColorTable {
2222
2223 // the palette of ARGB colors, packed as returned by Color.getRGB()
2224 private int[] theColors = new int[256];
2225
2226 // other basic attributes
2227 private int colorDepth;
2228 private int transparentIndex = -1;
2229
2230 // these fields track color-index info across frames
2231 private int ciCount = 0; // count of distinct color indices
2232 private ReverseColorMap ciLookup; // cumulative rgb-to-ci lookup table
2233
2234 //----------------------------------------------------------------------------
2235 GifColorTable()
2236 {
2237 ciLookup = new ReverseColorMap(); // puts us into "auto-detect mode"
2238 }
2239
2240 //----------------------------------------------------------------------------
2241 GifColorTable(Color[] colors)
2242 {
2243 int n2copy = Math.min(theColors.length, colors.length);
2244 for (int i = 0; i < n2copy; ++i)
2245 theColors[i] = colors[i].getRGB();
2246 }
2247
2248 int indexOf(Color color) {
2249 int rgb = color.getRGB();
2250 for (int i = 0; i < theColors.length; i++) {
2251 if (rgb == theColors[i]) {
2252 return i;
2253 }
2254 }
2255 return -1;
2256 }
2257
2258 //----------------------------------------------------------------------------
2259 int getDepth() { return colorDepth; }
2260
2261 //----------------------------------------------------------------------------
2262 int getTransparent() { return transparentIndex; }
2263
2264 //----------------------------------------------------------------------------
2265 // default: -1 (no transparency)
2266 void setTransparent(int color_index)
2267 {
2268 transparentIndex = color_index;
2269 }
2270
2271 //----------------------------------------------------------------------------
2272 void processPixels(Gif89Frame gf) throws IOException
2273 {
2274 if (gf instanceof DirectGif89Frame)
2275 filterPixels((DirectGif89Frame) gf);
2276 else
2277 trackPixelUsage((IndexGif89Frame) gf);
2278 }
2279
2280 //----------------------------------------------------------------------------
2281 void closePixelProcessing() // must be called before encode()
2282 {
2283 colorDepth = computeColorDepth(ciCount);
2284 }
2285
2286 //----------------------------------------------------------------------------
2287 void encode(OutputStream os) throws IOException
2288 {
2289 // size of palette written is the smallest power of 2 that can accomdate
2290 // the number of RGB colors detected (or largest color index, in case of
2291 // index pixels)
2292 int palette_size = 1 << colorDepth;
2293 for (int i = 0; i < palette_size; ++i)
2294 {
2295 os.write(theColors[i] >> 16 & 0xff);
2296 os.write(theColors[i] >> 8 & 0xff);
2297 os.write(theColors[i] & 0xff);
2298 }
2299 }
2300
2301 //----------------------------------------------------------------------------
2302 // This method accomplishes three things:
2303 // (1) converts the passed rgb pixels to indexes into our rgb lookup table
2304 // (2) fills the rgb table as new colors are encountered
2305 // (3) looks for transparent pixels so as to set the transparent index
2306 // The information is cumulative across multiple calls.
2307 //
2308 // (Note: some of the logic is borrowed from Jef Poskanzer's code.)
2309 //----------------------------------------------------------------------------
2310 private void filterPixels(DirectGif89Frame dgf) throws IOException
2311 {
2312 if (ciLookup == null)
2313 throw new IOException("RGB frames require palette autodetection");
2314
2315 int[] argb_pixels = (int[]) dgf.getPixelSource();
2316 byte[] ci_pixels = dgf.getPixelSink();
2317 int npixels = argb_pixels.length;
2318 for (int i = 0; i < npixels; ++i)
2319 {
2320 int argb = argb_pixels[i];
2321
2322 // handle transparency
2323 if ((argb >>> 24) < 0x80) // transparent pixel?
2324 if (transparentIndex == -1) // first transparent color encountered?
2325 transparentIndex = ciCount; // record its index
2326 else if (argb != theColors[transparentIndex]) // different pixel value?
2327 {
2328 // collapse all transparent pixels into one color index
2329 ci_pixels[i] = (byte) transparentIndex;
2330 continue; // CONTINUE - index already in table
2331 }
2332
2333 // try to look up the index in our "reverse" color table
2334 int color_index = ciLookup.getPaletteIndex(argb & 0xffffff);
2335
2336 if (color_index == -1) // if it isn't in there yet
2337 {
2338 if (ciCount == 256)
2339 throw new IOException("can't encode as GIF (> 256 colors)");
2340
2341 // store color in our accumulating palette
2342 theColors[ciCount] = argb;
2343
2344 // store index in reverse color table
2345 ciLookup.put(argb & 0xffffff, ciCount);
2346
2347 // send color index to our output array
2348 ci_pixels[i] = (byte) ciCount;
2349
2350 // increment count of distinct color indices
2351 ++ciCount;
2352 }
2353 else // we've already snagged color into our palette
2354 ci_pixels[i] = (byte) color_index; // just send filtered pixel
2355 }
2356 }
2357
2358 //----------------------------------------------------------------------------
2359 private void trackPixelUsage(IndexGif89Frame igf) throws IOException
2360 {
2361 byte[] ci_pixels = (byte[]) igf.getPixelSource();
2362 int npixels = ci_pixels.length;
2363 for (int i = 0; i < npixels; ++i)
2364 if (ci_pixels[i] >= ciCount)
2365 ciCount = ci_pixels[i] + 1;
2366 }
2367
2368 //----------------------------------------------------------------------------
2369 private int computeColorDepth(int colorcount)
2370 {
2371 // color depth = log-base-2 of maximum number of simultaneous colors, i.e.
2372 // bits per color-index pixel
2373 if (colorcount <= 2)
2374 return 1;
2375 if (colorcount <= 4)
2376 return 2;
2377 if (colorcount <= 16)
2378 return 4;
2379 return 8;
2380 }
2381 }
2382
2383 //==============================================================================
2384 // We're doing a very simple linear hashing thing here, which seems sufficient
2385 // for our needs. I make no claims for this approach other than that it seems
2386 // an improvement over doing a brute linear search for each pixel on the one
2387 // hand, and creating a Java object for each pixel (if we were to use a Java
2388 // Hashtable) on the other. Doubtless my little hash could be improved by
2389 // tuning the capacity (at the very least). Suggestions are welcome.
2390 //==============================================================================
2391 class ReverseColorMap {
2392
2393 private class ColorRecord {
2394 int rgb;
2395 int ipalette;
2396 ColorRecord(int rgb, int ipalette)
2397 {
2398 this.rgb = rgb;
2399 this.ipalette = ipalette;
2400 }
2401 }
2402
2403 // I wouldn't really know what a good hashing capacity is, having missed out
2404 // on data structures and algorithms class :) Alls I know is, we've got a lot
2405 // more space than we have time. So let's try a sparse table with a maximum
2406 // load of about 1/8 capacity.
2407 private static final int HCAPACITY = 2053; // a nice prime number
2408
2409 // our hash table proper
2410 private ColorRecord[] hTable = new ColorRecord[HCAPACITY];
2411
2412 //----------------------------------------------------------------------------
2413 // Assert: rgb is not negative (which is the same as saying, be sure the
2414 // alpha transparency byte - i.e., the high byte - has been masked out).
2415 //----------------------------------------------------------------------------
2416 int getPaletteIndex(int rgb)
2417 {
2418 ColorRecord rec;
2419
2420 for ( int itable = rgb % hTable.length;
2421 (rec = hTable[itable]) != null && rec.rgb != rgb;
2422 itable = ++itable % hTable.length
2423 )
2424 ;
2425
2426 if (rec != null)
2427 return rec.ipalette;
2428
2429 return -1;
2430 }
2431
2432 //----------------------------------------------------------------------------
2433 // Assert: (1) same as above; (2) rgb key not already present
2434 //----------------------------------------------------------------------------
2435 void put(int rgb, int ipalette)
2436 {
2437 int itable;
2438
2439 for ( itable = rgb % hTable.length;
2440 hTable[itable] != null;
2441 itable = ++itable % hTable.length
2442 )
2443 ;
2444
2445 hTable[itable] = new ColorRecord(rgb, ipalette);
2446 }
2447 }
2448
2449
2450
2451 //******************************************************************************
2452 // Gif89Frame.java
2453 //******************************************************************************
2454
2455 //==============================================================================
2456 /** First off, just to dispel any doubt, this class and its subclasses have
2457 * nothing to do with GUI "frames" such as java.awt.Frame. We merely use the
2458 * term in its very common sense of a still picture in an animation sequence.
2459 * It's hoped that the restricted context will prevent any confusion.
2460 * <p>
2461 * An instance of this class is used in conjunction with a Gif89Encoder object
2462 * to represent and encode a single static image and its associated "control"
2463 * data. A Gif89Frame doesn't know or care whether it is encoding one of the
2464 * many animation frames in a GIF movie, or the single bitmap in a "normal"
2465 * GIF. (FYI, this design mirrors the encoded GIF structure.)
2466 * <p>
2467 * Since Gif89Frame is an abstract class we don't instantiate it directly, but
2468 * instead create instances of its concrete subclasses, IndexGif89Frame and
2469 * DirectGif89Frame. From the API standpoint, these subclasses differ only
2470 * in the sort of data their instances are constructed from. Most folks will
2471 * probably work with DirectGif89Frame, since it can be constructed from a
2472 * java.awt.Image object, but the lower-level IndexGif89Frame class offers
2473 * advantages in specialized circumstances. (Of course, in routine situations
2474 * you might not explicitly instantiate any frames at all, instead letting
2475 * Gif89Encoder's convenience methods do the honors.)
2476 * <p>
2477 * As far as the public API is concerned, objects in the Gif89Frame hierarchy
2478 * interact with a Gif89Encoder only via the latter's methods for adding and
2479 * querying frames. (As a side note, you should know that while Gif89Encoder
2480 * objects are permanently modified by the addition of Gif89Frames, the reverse
2481 * is NOT true. That is, even though the ultimate encoding of a Gif89Frame may
2482 * be affected by the context its parent encoder object provides, it retains
2483 * its original condition and can be reused in a different context.)
2484 * <p>
2485 * The core pixel-encoding code in this class was essentially lifted from
2486 * Jef Poskanzer's well-known <cite>Acme GifEncoder</cite>, so please see the
2487 * <a href="../readme.txt">readme</a> containing his notice.
2488 *
2489 * @version 0.90 beta (15-Jul-2000)
2490 * @author J. M. G. Elliott (tep@jmge.net)
2491 * @see Gif89Encoder
2492 * @see DirectGif89Frame
2493 * @see IndexGif89Frame
2494 */
2495 abstract class Gif89Frame {
2496
2497 //// Public "Disposal Mode" constants ////
2498
2499 /** The animated GIF renderer shall decide how to dispose of this Gif89Frame's
2500 * display area.
2501 * @see Gif89Frame#setDisposalMode
2502 */
2503 public static final int DM_UNDEFINED = 0;
2504
2505 /** The animated GIF renderer shall take no display-disposal action.
2506 * @see Gif89Frame#setDisposalMode
2507 */
2508 public static final int DM_LEAVE = 1;
2509
2510 /** The animated GIF renderer shall replace this Gif89Frame's area with the
2511 * background color.
2512 * @see Gif89Frame#setDisposalMode
2513 */
2514 public static final int DM_BGCOLOR = 2;
2515
2516 /** The animated GIF renderer shall replace this Gif89Frame's area with the
2517 * previous frame's bitmap.
2518 * @see Gif89Frame#setDisposalMode
2519 */
2520 public static final int DM_REVERT = 3;
2521
2522 //// Bitmap variables set in package subclass constructors ////
2523 int theWidth = -1;
2524 int theHeight = -1;
2525 byte[] ciPixels;
2526
2527 //// GIF graphic frame control options ////
2528 private Point thePosition = new Point(0, 0);
2529 private boolean isInterlaced;
2530 private int csecsDelay;
2531 private int disposalCode = DM_LEAVE;
2532
2533 //----------------------------------------------------------------------------
2534 /** Set the position of this frame within a larger animation display space.
2535 *
2536 * @param p
2537 * Coordinates of the frame's upper left corner in the display space.
2538 * (Default: The logical display's origin [0, 0])
2539 * @see Gif89Encoder#setLogicalDisplay
2540 */
2541 public void setPosition(Point p)
2542 {
2543 thePosition = new Point(p);
2544 }
2545
2546 //----------------------------------------------------------------------------
2547 /** Set or clear the interlace flag.
2548 *
2549 * @param b
2550 * true if you want interlacing. (Default: false)
2551 */
2552 public void setInterlaced(boolean b)
2553 {
2554 isInterlaced = b;
2555 }
2556
2557 //----------------------------------------------------------------------------
2558 /** Set the between-frame interval.
2559 *
2560 * @param interval
2561 * Centiseconds to wait before displaying the subsequent frame.
2562 * (Default: 0)
2563 */
2564 public void setDelay(int interval)
2565 {
2566 csecsDelay = interval;
2567 }
2568
2569 //----------------------------------------------------------------------------
2570 /** Setting this option determines (in a cooperative GIF-viewer) what will be
2571 * done with this frame's display area before the subsequent frame is
2572 * displayed. For instance, a setting of DM_BGCOLOR can be used for erasure
2573 * when redrawing with displacement.
2574 *
2575 * @param code
2576 * One of the four int constants of the Gif89Frame.DM_* series.
2577 * (Default: DM_LEAVE)
2578 */
2579 public void setDisposalMode(int code)
2580 {
2581 disposalCode = code;
2582 }
2583
2584 //----------------------------------------------------------------------------
2585 Gif89Frame() {} // package-visible default constructor
2586
2587 //----------------------------------------------------------------------------
2588 abstract Object getPixelSource();
2589
2590 //----------------------------------------------------------------------------
2591 int getWidth() { return theWidth; }
2592
2593 //----------------------------------------------------------------------------
2594 int getHeight() { return theHeight; }
2595
2596 //----------------------------------------------------------------------------
2597 byte[] getPixelSink() { return ciPixels; }
2598
2599 //----------------------------------------------------------------------------
2600 void encode(OutputStream os, boolean epluribus, int color_depth,
2601 int transparent_index) throws IOException
2602 {
2603 writeGraphicControlExtension(os, epluribus, transparent_index);
2604 writeImageDescriptor(os);
2605 new GifPixelsEncoder(
2606 theWidth, theHeight, ciPixels, isInterlaced, color_depth
2607 ).encode(os);
2608 }
2609
2610 //----------------------------------------------------------------------------
2611 private void writeGraphicControlExtension(OutputStream os, boolean epluribus,
2612 int itransparent) throws IOException
2613 {
2614 int transflag = itransparent == -1 ? 0 : 1;
2615 if (transflag == 1 || epluribus) // using transparency or animating ?
2616 {
2617 os.write((int) '!'); // GIF Extension Introducer
2618 os.write(0xf9); // Graphic Control Label
2619 os.write(4); // subsequent data block size
2620 os.write((disposalCode << 2) | transflag); // packed fields (1 byte)
2621 putShort(csecsDelay, os); // delay field (2 bytes)
2622 os.write(itransparent); // transparent index field
2623 os.write(0); // block terminator
2624 }
2625 }
2626
2627 //----------------------------------------------------------------------------
2628 private void writeImageDescriptor(OutputStream os) throws IOException
2629 {
2630 os.write((int) ','); // Image Separator
2631 putShort(thePosition.x, os);
2632 putShort(thePosition.y, os);
2633 putShort(theWidth, os);
2634 putShort(theHeight, os);
2635 os.write(isInterlaced ? 0x40 : 0); // packed fields (1 byte)
2636 }
2637 }
2638
2639 //==============================================================================
2640 class GifPixelsEncoder {
2641
2642 private static final int EOF = -1;
2643
2644 private int imgW, imgH;
2645 private byte[] pixAry;
2646 private boolean wantInterlaced;
2647 private int initCodeSize;
2648
2649 // raster data navigators
2650 private int countDown;
2651 private int xCur, yCur;
2652 private int curPass;
2653
2654 //----------------------------------------------------------------------------
2655 GifPixelsEncoder(int width, int height, byte[] pixels, boolean interlaced,
2656 int color_depth)
2657 {
2658 imgW = width;
2659 imgH = height;
2660 pixAry = pixels;
2661 wantInterlaced = interlaced;
2662 initCodeSize = Math.max(2, color_depth);
2663 }
2664
2665 //----------------------------------------------------------------------------
2666 void encode(OutputStream os) throws IOException
2667 {
2668 os.write(initCodeSize); // write "initial code size" byte
2669
2670 countDown = imgW * imgH; // reset navigation variables
2671 xCur = yCur = curPass = 0;
2672
2673 compress(initCodeSize + 1, os); // compress and write the pixel data
2674
2675 os.write(0); // write block terminator
2676 }
2677
2678 //****************************************************************************
2679 // (J.E.) The logic of the next two methods is largely intact from
2680 // Jef Poskanzer. Some stylistic changes were made for consistency sake,
2681 // plus the second method accesses the pixel value from a prefiltered linear
2682 // array. That's about it.
2683 //****************************************************************************
2684
2685 //----------------------------------------------------------------------------
2686 // Bump the 'xCur' and 'yCur' to point to the next pixel.
2687 //----------------------------------------------------------------------------
2688 private void bumpPosition()
2689 {
2690 // Bump the current X position
2691 ++xCur;
2692
2693 // If we are at the end of a scan line, set xCur back to the beginning
2694 // If we are interlaced, bump the yCur to the appropriate spot,
2695 // otherwise, just increment it.
2696 if (xCur == imgW)
2697 {
2698 xCur = 0;
2699
2700 if (!wantInterlaced)
2701 ++yCur;
2702 else
2703 switch (curPass)
2704 {
2705 case 0:
2706 yCur += 8;
2707 if (yCur >= imgH)
2708 {
2709 ++curPass;
2710 yCur = 4;
2711 }
2712 break;
2713 case 1:
2714 yCur += 8;
2715 if (yCur >= imgH)
2716 {
2717 ++curPass;
2718 yCur = 2;
2719 }
2720 break;
2721 case 2:
2722 yCur += 4;
2723 if (yCur >= imgH)
2724 {
2725 ++curPass;
2726 yCur = 1;
2727 }
2728 break;
2729 case 3:
2730 yCur += 2;
2731 break;
2732 }
2733 }
2734 }
2735
2736 //----------------------------------------------------------------------------
2737 // Return the next pixel from the image
2738 //----------------------------------------------------------------------------
2739 private int nextPixel()
2740 {
2741 if (countDown == 0)
2742 return EOF;
2743
2744 --countDown;
2745
2746 byte pix = pixAry[yCur * imgW + xCur];
2747
2748 bumpPosition();
2749
2750 return pix & 0xff;
2751 }
2752
2753 //****************************************************************************
2754 // (J.E.) I didn't touch Jef Poskanzer's code from this point on. (Well, OK,
2755 // I changed the name of the sole outside method it accesses.) I figure
2756 // if I have no idea how something works, I shouldn't play with it :)
2757 //
2758 // Despite its unencapsulated structure, this section is actually highly
2759 // self-contained. The calling code merely calls compress(), and the present
2760 // code calls nextPixel() in the caller. That's the sum total of their
2761 // communication. I could have dumped it in a separate class with a callback
2762 // via an interface, but it didn't seem worth messing with.
2763 //****************************************************************************
2764
2765 // GIFCOMPR.C - GIF Image compression routines
2766 //
2767 // Lempel-Ziv compression based on 'compress'. GIF modifications by
2768 // David Rowley (mgardi@watdcsu.waterloo.edu)
2769
2770 // General DEFINEs
2771
2772 static final int BITS = 12;
2773
2774 static final int HSIZE = 5003; // 80% occupancy
2775
2776 // GIF Image compression - modified 'compress'
2777 //
2778 // Based on: compress.c - File compression ala IEEE Computer, June 1984.
2779 //
2780 // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
2781 // Jim McKie (decvax!mcvax!jim)
2782 // Steve Davies (decvax!vax135!petsd!peora!srd)
2783 // Ken Turkowski (decvax!decwrl!turtlevax!ken)
2784 // James A. Woods (decvax!ihnp4!ames!jaw)
2785 // Joe Orost (decvax!vax135!petsd!joe)
2786
2787 int n_bits; // number of bits/code
2788 int maxbits = BITS; // user settable max # bits/code
2789 int maxcode; // maximum code, given n_bits
2790 int maxmaxcode = 1 << BITS; // should NEVER generate this code
2791
2792 final int MAXCODE( int n_bits )
2793 {
2794 return ( 1 << n_bits ) - 1;
2795 }
2796
2797 int[] htab = new int[HSIZE];
2798 int[] codetab = new int[HSIZE];
2799
2800 int hsize = HSIZE; // for dynamic table sizing
2801
2802 int free_ent = 0; // first unused entry
2803
2804 // block compression parameters -- after all codes are used up,
2805 // and compression rate changes, start over.
2806 boolean clear_flg = false;
2807
2808 // Algorithm: use open addressing double hashing (no chaining) on the
2809 // prefix code / next character combination. We do a variant of Knuth's
2810 // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
2811 // secondary probe. Here, the modular division first probe is gives way
2812 // to a faster exclusive-or manipulation. Also do block compression with
2813 // an adaptive reset, whereby the code table is cleared when the compression
2814 // ratio decreases, but after the table fills. The variable-length output
2815 // codes are re-sized at this point, and a special CLEAR code is generated
2816 // for the decompressor. Late addition: construct the table according to
2817 // file size for noticeable speed improvement on small files. Please direct
2818 // questions about this implementation to ames!jaw.
2819
2820 int g_init_bits;
2821
2822 int ClearCode;
2823 int EOFCode;
2824
2825 void compress( int init_bits, OutputStream outs ) throws IOException
2826 {
2827 int fcode;
2828 int i /* = 0 */;
2829 int c;
2830 int ent;
2831 int disp;
2832 int hsize_reg;
2833 int hshift;
2834
2835 // Set up the globals: g_init_bits - initial number of bits
2836 g_init_bits = init_bits;
2837
2838 // Set up the necessary values
2839 clear_flg = false;
2840 n_bits = g_init_bits;
2841 maxcode = MAXCODE( n_bits );
2842
2843 ClearCode = 1 << ( init_bits - 1 );
2844 EOFCode = ClearCode + 1;
2845 free_ent = ClearCode + 2;
2846
2847 char_init();
2848
2849 ent = nextPixel();
2850
2851 hshift = 0;
2852 for ( fcode = hsize; fcode < 65536; fcode *= 2 )
2853 ++hshift;
2854 hshift = 8 - hshift; // set hash code range bound
2855
2856 hsize_reg = hsize;
2857 cl_hash( hsize_reg ); // clear hash table
2858
2859 output( ClearCode, outs );
2860
2861 outer_loop:
2862 while ( (c = nextPixel()) != EOF )
2863 {
2864 fcode = ( c << maxbits ) + ent;
2865 i = ( c << hshift ) ^ ent; // xor hashing
2866
2867 if ( htab[i] == fcode )
2868 {
2869 ent = codetab[i];
2870 continue;
2871 }
2872 else if ( htab[i] >= 0 ) // non-empty slot
2873 {
2874 disp = hsize_reg - i; // secondary hash (after G. Knott)
2875 if ( i == 0 )
2876 disp = 1;
2877 do
2878 {
2879 if ( (i -= disp) < 0 )
2880 i += hsize_reg;
2881
2882 if ( htab[i] == fcode )
2883 {
2884 ent = codetab[i];
2885 continue outer_loop;
2886 }
2887 }
2888 while ( htab[i] >= 0 );
2889 }
2890 output( ent, outs );
2891 ent = c;
2892 if ( free_ent < maxmaxcode )
2893 {
2894 codetab[i] = free_ent++; // code -> hashtable
2895 htab[i] = fcode;
2896 }
2897 else
2898 cl_block( outs );
2899 }
2900 // Put out the final code.
2901 output( ent, outs );
2902 output( EOFCode, outs );
2903 }
2904
2905 // output
2906 //
2907 // Output the given code.
2908 // Inputs:
2909 // code: A n_bits-bit integer. If == -1, then EOF. This assumes
2910 // that n_bits =< wordsize - 1.
2911 // Outputs:
2912 // Outputs code to the file.
2913 // Assumptions:
2914 // Chars are 8 bits long.
2915 // Algorithm:
2916 // Maintain a BITS character long buffer (so that 8 codes will
2917 // fit in it exactly). Use the VAX insv instruction to insert each
2918 // code in turn. When the buffer fills up empty it and start over.
2919
2920 int cur_accum = 0;
2921 int cur_bits = 0;
2922
2923 int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F,
2924 0x001F, 0x003F, 0x007F, 0x00FF,
2925 0x01FF, 0x03FF, 0x07FF, 0x0FFF,
2926 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF };
2927
2928 void output( int code, OutputStream outs ) throws IOException
2929 {
2930 cur_accum &= masks[cur_bits];
2931
2932 if ( cur_bits > 0 )
2933 cur_accum |= ( code << cur_bits );
2934 else
2935 cur_accum = code;
2936
2937 cur_bits += n_bits;
2938
2939 while ( cur_bits >= 8 )
2940 {
2941 char_out( (byte) ( cur_accum & 0xff ), outs );
2942 cur_accum >>= 8;
2943 cur_bits -= 8;
2944 }
2945
2946 // If the next entry is going to be too big for the code size,
2947 // then increase it, if possible.
2948 if ( free_ent > maxcode || clear_flg )
2949 {
2950 if ( clear_flg )
2951 {
2952 maxcode = MAXCODE(n_bits = g_init_bits);
2953 clear_flg = false;
2954 }
2955 else
2956 {
2957 ++n_bits;
2958 if ( n_bits == maxbits )
2959 maxcode = maxmaxcode;
2960 else
2961 maxcode = MAXCODE(n_bits);
2962 }
2963 }
2964
2965 if ( code == EOFCode )
2966 {
2967 // At EOF, write the rest of the buffer.
2968 while ( cur_bits > 0 )
2969 {
2970 char_out( (byte) ( cur_accum & 0xff ), outs );
2971 cur_accum >>= 8;
2972 cur_bits -= 8;
2973 }
2974
2975 flush_char( outs );
2976 }
2977 }
2978
2979 // Clear out the hash table
2980
2981 // table clear for block compress
2982 void cl_block( OutputStream outs ) throws IOException
2983 {
2984 cl_hash( hsize );
2985 free_ent = ClearCode + 2;
2986 clear_flg = true;
2987
2988 output( ClearCode, outs );
2989 }
2990
2991 // reset code table
2992 void cl_hash( int hsize )
2993 {
2994 for ( int i = 0; i < hsize; ++i )
2995 htab[i] = -1;
2996 }
2997
2998 // GIF Specific routines
2999
3000 // Number of characters so far in this 'packet'
3001 int a_count;
3002
3003 // Set up the 'byte output' routine
3004 void char_init()
3005 {
3006 a_count = 0;
3007 }
3008
3009 // Define the storage for the packet accumulator
3010 byte[] accum = new byte[256];
3011
3012 // Add a character to the end of the current packet, and if it is 254
3013 // characters, flush the packet to disk.
3014 void char_out( byte c, OutputStream outs ) throws IOException
3015 {
3016 accum[a_count++] = c;
3017 if ( a_count >= 254 )
3018 flush_char( outs );
3019 }
3020
3021 // Flush the packet to disk, and reset the accumulator
3022 void flush_char( OutputStream outs ) throws IOException
3023 {
3024 if ( a_count > 0 )
3025 {
3026 outs.write( a_count );
3027 outs.write( accum, 0, a_count );
3028 a_count = 0;
3029 }
3030 }
3031 }
3032
3033
3034
3035 //******************************************************************************
3036 // IndexGif89Frame.java
3037 //******************************************************************************
3038
3039 //==============================================================================
3040 /** Instances of this Gif89Frame subclass are constructed from bitmaps in the
3041 * form of color-index pixels, which accords with a GIF's native palettized
3042 * color model. The class is useful when complete control over a GIF's color
3043 * palette is desired. It is also much more efficient when one is using an
3044 * algorithmic frame generator that isn't interested in RGB values (such
3045 * as a cellular automaton).
3046 * <p>
3047 * Objects of this class are normally added to a Gif89Encoder object that has
3048 * been provided with an explicit color table at construction. While you may
3049 * also add them to "auto-map" encoders without an exception being thrown,
3050 * there obviously must be at least one DirectGif89Frame object in the sequence
3051 * so that a color table may be detected.
3052 *
3053 * @version 0.90 beta (15-Jul-2000)
3054 * @author J. M. G. Elliott (tep@jmge.net)
3055 * @see Gif89Encoder
3056 * @see Gif89Frame
3057 * @see DirectGif89Frame
3058 */
3059 class IndexGif89Frame extends Gif89Frame {
3060
3061 //----------------------------------------------------------------------------
3062 /** Construct a IndexGif89Frame from color-index pixel data.
3063 *
3064 * @param width
3065 * Width of the bitmap.
3066 * @param height
3067 * Height of the bitmap.
3068 * @param ci_pixels
3069 * Array containing at least width*height color-index pixels.
3070 */
3071 public IndexGif89Frame(int width, int height, byte ci_pixels[])
3072 {
3073 theWidth = width;
3074 theHeight = height;
3075 ciPixels = new byte[theWidth * theHeight];
3076 System.arraycopy(ci_pixels, 0, ciPixels, 0, ciPixels.length);
3077 }
3078
3079 //----------------------------------------------------------------------------
3080 Object getPixelSource() { return ciPixels; }
3081 }
3082
3083
3084
3085 //----------------------------------------------------------------------------
3086 /** Write just the low bytes of a String. (This sucks, but the concept of an
3087 * encoding seems inapplicable to a binary file ID string. I would think
3088 * flexibility is just what we don't want - but then again, maybe I'm slow.)
3089 */
3090 public static void putAscii(String s, OutputStream os) throws IOException
3091 {
3092 byte[] bytes = new byte[s.length()];
3093 for (int i = 0; i < bytes.length; ++i) {
3094 bytes[i] = (byte) s.charAt(i); // discard the high byte
3095 }
3096 os.write(bytes);
3097 }
3098
3099 //----------------------------------------------------------------------------
3100 /** Write a 16-bit integer in little endian byte order.
3101 */
3102 public static void putShort(int i16, OutputStream os) throws IOException
3103 {
3104 os.write(i16 & 0xff);
3105 os.write(i16 >> 8 & 0xff);
3106 }
3107}