/* * ===================================================================== * DrawingPanel.java * Simplified Java drawing window class * to accompany Building Java Programs textbook and associated materials * * authors: Marty Stepp, Stanford University * Stuart Reges, University of Washington * version: 4.01, 2016/03/08 (BJP 4th edition) * ===================================================================== * * COMPATIBILITY NOTE: This version of DrawingPanel requires Java 8 or higher. * To make this file work on Java 7 and lower, you must make two small * modifications to its source code. * Search for the two occurrences of the annotation @FunctionalInterface * and comment them out or remove those lines. * Then the file should compile and run properly on older versions of Java. * * ===================================================================== * * The DrawingPanel class provides a simple interface for drawing persistent * images using a Graphics object. An internal BufferedImage object is used * to keep track of what has been drawn. A client of the class simply * constructs a DrawingPanel of a particular size and then draws on it with * the Graphics object, setting the background color if they so choose. * See JavaDoc comments below for more information. */ import java.awt.FontMetrics; import java.awt.Rectangle; import java.awt.Shape; import java.awt.image.ImageObserver; import java.text.AttributedCharacterIterator; import java.util.Collections; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Composite; import java.awt.Container; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.Image; import java.awt.MediaTracker; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.image.BufferedImage; import java.awt.image.PixelGrabber; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.lang.Exception; import java.lang.Integer; import java.lang.InterruptedException; import java.lang.Math; import java.lang.Object; import java.lang.OutOfMemoryError; import java.lang.SecurityException; import java.lang.String; import java.lang.System; import java.lang.Thread; import java.net.URL; import java.net.NoRouteToHostException; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.TreeMap; import java.util.Vector; import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JCheckBoxMenuItem; import javax.swing.JColorChooser; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.KeyStroke; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MouseInputAdapter; import javax.swing.event.MouseInputListener; import javax.swing.filechooser.FileFilter; /** * * DrawingPanel is a simplified Java drawing window class to accompany * Building Java Programs textbook and associated materials. * *

* Authors: Marty Stepp (Stanford University) and Stuart Reges (University of Washington). * *

* Version: 4.01, 2016/03/08 (to accompany BJP 4th edition). * *

* You can always download the latest {@code DrawingPanel} from * * http://www.buildingjavaprograms.com/drawingpanel/DrawingPanel.java . * *

* For more information and related materials, please visit * * www.buildingjavaprograms.com . * *

* COMPATIBILITY NOTE: This version of DrawingPanel requires Java 8 or higher. * To make this file work on Java 7 and lower, you must make two small * modifications to its source code. * Search for the two occurrences of the annotation @FunctionalInterface * and comment them out or remove those lines. * Then the file should compile and run properly on older versions of Java. * *

Description:

* *

* The {@code DrawingPanel} class provides a simple interface for drawing persistent * images using a {@code Graphics} object. An internal {@code BufferedImage} object is used * to keep track of what has been drawn. A client of the class simply * constructs a {@code DrawingPanel} of a particular size and then draws on it with * the {@code Graphics} object, setting the background color if they so choose. *

* *

* The intention is that this custom library will mostly "stay out of the way" * so that the client mostly interacts with a standard Java {@code java.awt.Graphics} * object, and therefore most of the experience gained while using this library * will transfer to Java graphics programming in other contexts. * {@code DrawingPanel} is not intended to be a full rich graphical library for things * like object-oriented drawing of shapes, animations, creating games, etc. *

* *

Example basic usage:

* *

* Here is a canonical example of creating a {@code DrawingPanel} of a given size and * using it to draw a few shapes. *

* *
 * // basic usage example
 * DrawingPanel panel = new DrawingPanel(600, 400);
 * Graphics g = panel.getGraphics();
 * g.setColor(Color.RED);
 * g.fillRect(17, 45, 139, 241);
 * g.drawOval(234, 77, 100, 100);
 * ...
 * 
* *

* To ensure that the image is always displayed, a timer calls repaint at * regular intervals. *

* *

Pixel processing (new in BJP 4th edition):

* *

* This version of {@code DrawingPanel} allows you to loop over the pixels of an image. * You can process each pixel as a {@code Color} object (easier OO interface, but takes * more CPU and memory to run) or as a 32-bit RGB integer (clunkier to use, but * much more efficient in runtime and memory usage). * Look at the methods get/setPixel(s) to get a better idea. * *

 * // example of horizontally flipping an image
 * public static void flipHorizontal(DrawingPanel panel) {
 *     int width  = panel.getWidth();
 *     int height = panel.getHeight();
 *     int[] pixels = panel.getPixelsRGB();
 *     for (int row = 0; row < height; row++) {
 *         for (int col = 0; col < width / 2; col++) {
 *             // swap this pixel with the one opposite it
 *             int col2 = width - 1 - col;
 *             int temp = pixels[row][col];
 *             pixels[row][col] = pixels[row][col2];
 *             pixels[row][col2] = temp;
 *         }
 *     }
 *     panel.setPixels(pixels);
 * }
 * 
* *

Event listeners and lambdas (new in BJP 4th edition):

* *

* With Java 8, you can now attach event handlers to listen to keyboard and mouse * events that occur in a {@code DrawingPanel} using a lambda function. For example: * *

 * // example of attaching a mouse click handler using Java 8
 * panel.onClick( (x, y) -> System.out.println(x + " " + y) );
 * 
*

Debugging facilities (new in BJP 4th edition):

* *

* This version now includes an inner class named {@code DebuggingGraphics} * that keeps track of how many times various drawing methods are called. * It includes a {@code showCounts} method for the {@code DrawingPanel} itself * that allows a client to examine this. The panel will record basic drawing * methods performed by a version of the {@code Graphics} class obtained by * calling {@code getDebuggingGraphics} : * *

 * // example of debugging counts of graphics method calls
 * Graphics g = panel.getDebuggingGraphics();
 * 
* *

* Novices will be encouraged to simply print it at the end of {@code main}, as in: * *

 * System.out.println(panel.getCounts());
 * 
* *

History and recent changes:

* * 2016/03/07 * - Code cleanup and improvements to JavaDoc comments for BJP4 release. *

* * 2015/09/04 * - Now includes methods for get/setting individual pixels and all pixels on the * drawing panel. This helps facilitate 2D array-based pixel-processing * exercises and problems for Building Java Programs, 4th edition. * - Code cleanup and reorganization. * Now better alphabetization/formatting of members and encapsulation. * Commenting also augmented throughout code. *

* * 2015/04/09 * - Now includes a DebuggingGraphics inner class that keeps track of how many * times various drawing methods are called. * All additions are commented (search for "DebuggingGraphics") *

* * 2011/10/25 * - save zoomed images (2011/10/25) *

* * 2011/10/21 * - window no longer moves when zoom changes * - grid lines * * @author Marty Stepp, Stanford University, and Stuart Reges, University of Washington * @version 4.01, 2016/03/08 (BJP 4th edition) */ public final class DrawingPanel implements ImageObserver { // class constants private static final Color GRID_LINE_COLOR = new Color(64, 64, 64, 128); // color of grid lines on panel private static final Object LOCK = new Object(); // object used for concurrency locking private static final boolean SAVE_SCALED_IMAGES = true; // if true, when panel is zoomed, saves images at that zoom factor private static final int DELAY = 100; // delay between repaints in millis private static final int MAX_FRAMES = 100; // max animation frames private static final int MAX_SIZE = 10000; // max width/height private static final int GRID_LINES_PX_GAP_DEFAULT = 10; // default px between grid lines private static final String VERSION = "4.01 (2016/03/08)"; private static final String ABOUT_MESSAGE = "DrawingPanel\n" + "Graphical library class to support Building Java Programs textbook\n" + "written by Marty Stepp, Stanford University\n" + "and Stuart Reges, University of Washington\n\n" + "Version: " + VERSION + "\n\n" + "please visit our web site at:\n" + "http://www.buildingjavaprograms.com/"; private static final String ABOUT_MESSAGE_TITLE = "About DrawingPanel"; private static final String COURSE_WEB_SITE = "https://courses.cs.washington.edu/courses/cse142/CurrentQtr/drawingpanel.txt"; private static final String TITLE = "Drawing Panel"; /** An RGB integer representing alpha at 100% opacity (0xff000000). */ public static final int PIXEL_ALPHA = 0xff000000; // rgb integer for alpha 100% opacity /** An RGB integer representing 100% blue (0x000000ff). */ public static final int PIXEL_BLUE = 0x000000ff; // rgb integer for 100% blue /** An RGB integer representing 100% green (0x0000ff00). */ public static final int PIXEL_GREEN = 0x0000ff00; // rgb integer for 100% green /** An RGB integer representing 100% red (0x00ff0000). */ public static final int PIXEL_RED = 0x00ff0000; // rgb integer for 100% red /** * The default width of a DrawingPanel in pixels, if none is supplied at construction (500 pixels). */ public static final int DEFAULT_WIDTH = 500; /** * The default height of a DrawingPanel in pixels, if none is supplied at construction (400 pixels). */ public static final int DEFAULT_HEIGHT = 400; /** An internal constant for setting system properties; clients should not use this. */ public static final String ANIMATED_PROPERTY = "drawingpanel.animated"; /** An internal constant for setting system properties; clients should not use this. */ public static final String ANIMATION_FILE_NAME = "_drawingpanel_animation_save.txt"; /** An internal constant for setting system properties; clients should not use this. */ public static final String AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY = "drawingpanel.animateonsleep"; /** An internal constant for setting system properties; clients should not use this. */ public static final String DIFF_PROPERTY = "drawingpanel.diff"; /** An internal constant for setting system properties; clients should not use this. */ public static final String HEADLESS_PROPERTY = "drawingpanel.headless"; /** An internal constant for setting system properties; clients should not use this. */ public static final String MULTIPLE_PROPERTY = "drawingpanel.multiple"; /** An internal constant for setting system properties; clients should not use this. */ public static final String SAVE_PROPERTY = "drawingpanel.save"; // static variables private static boolean DEBUG = false; private static int instances = 0; private static Thread shutdownThread = null; // static class initializer - sets up thread to close program if // last DrawingPanel is closed static { try { String debugProp = String.valueOf(System.getProperty("drawingpanel.debug")).toLowerCase(); DEBUG = DEBUG || "true".equalsIgnoreCase(debugProp) || "on".equalsIgnoreCase(debugProp) || "yes".equalsIgnoreCase(debugProp) || "1".equals(debugProp); } catch (Throwable t) { // empty } } /* * Called when DrawingPanel class loads up. * Checks whether the user wants to save an animation to a file. */ private static void checkAnimationSettings() { try { File settingsFile = new File(ANIMATION_FILE_NAME); if (settingsFile.exists()) { Scanner input = new Scanner(settingsFile); String animationSaveFileName = input.nextLine(); input.close(); System.out.println("***"); System.out.println("*** DrawingPanel saving animated GIF: " + new File(animationSaveFileName).getName()); System.out.println("***"); settingsFile.delete(); System.setProperty(ANIMATED_PROPERTY, "1"); System.setProperty(SAVE_PROPERTY, animationSaveFileName); } } catch (Exception e) { if (DEBUG) { System.out.println("error checking animation settings: " + e); } } } /** * Returns the red component of the given RGB pixel from 0-255. * Often used in conjunction with the methods getPixelRGB, setPixelRGB, etc. * @param rgb RGB integer with alpha in bits 0-7, red in bits 8-15, green in * bits 16-23, and blue in bits 24-31 * @return red component from 0-255 */ public static int getRed(int rgb) { return (rgb & 0x00ff0000) >> 16; } /** * Returns the green component of the given RGB pixel from 0-255. * Often used in conjunction with the methods getPixelRGB, setPixelRGB, etc. * @param rgb RGB integer with alpha in bits 0-7, red in bits 8-15, green in * bits 16-23, and blue in bits 24-31 * @return green component from 0-255 */ public static int getGreen(int rgb) { return (rgb & 0x0000ff00) >> 8; } /** * Returns the blue component of the given RGB pixel from 0-255. * Often used in conjunction with the methods getPixelRGB, setPixelRGB, etc. * @param rgb RGB integer with alpha in bits 0-7, red in bits 8-15, green in * bits 16-23, and blue in bits 24-31 * @return blue component from 0-255 */ public static int getBlue(int rgb) { return (rgb & 0x000000ff); } /** * Returns the alpha (opacity) component of the given RGB pixel from 0-255. * Often used in conjunction with the methods getPixelRGB, setPixelRGB, etc. * @param rgb RGB integer with alpha in bits 0-7, red in bits 8-15, green in * bits 16-23, and blue in bits 24-31 * @return alpha component from 0-255 */ public static int getAlpha(int rgb) { return (rgb & 0xff000000) >> 24; } /* * Returns whether the given Java system property has been set. */ private static boolean hasProperty(String name) { try { return System.getProperty(name) != null; } catch (SecurityException e) { if (DEBUG) System.out.println("Security exception when trying to read " + name); return false; } } /** * Internal method; returns whether the 'main' thread is still running. * Used to determine whether to exit the program when the drawing panel * is closed by the user. * This is an internal method not meant to be called by clients. * @return true if main thread is still running */ public static boolean mainIsActive() { ThreadGroup group = Thread.currentThread().getThreadGroup(); int activeCount = group.activeCount(); // look for the main thread in the current thread group Thread[] threads = new Thread[activeCount]; group.enumerate(threads); for (int i = 0; i < threads.length; i++) { Thread thread = threads[i]; String name = String.valueOf(thread.getName()).toLowerCase(); if (DEBUG) System.out.println(" DrawingPanel.mainIsActive(): " + thread.getName() + ", priority=" + thread.getPriority() + ", alive=" + thread.isAlive() + ", stack=" + java.util.Arrays.toString(thread.getStackTrace())); if (name.indexOf("main") >= 0 || name.indexOf("testrunner-assignmentrunner") >= 0) { // found main thread! // (TestRunnerApplet's main runner also counts as "main" thread) return thread.isAlive(); } } // didn't find a running main thread; guess that main is done running return false; } /* * Returns whether the given Java system property has been set to a * "truthy" value such as "yes" or "true" or "1". */ private static boolean propertyIsTrue(String name) { try { String prop = System.getProperty(name); return prop != null && (prop.equalsIgnoreCase("true") || prop.equalsIgnoreCase("yes") || prop.equalsIgnoreCase("1")); } catch (SecurityException e) { if (DEBUG) System.out.println("Security exception when trying to read " + name); return false; } } /* * Helper that throws an IllegalArgumentException if the given integer * is not between the given min-max inclusive */ private static void ensureInRange(String name, int value, int min, int max) { if (value < min || value > max) { throw new IllegalArgumentException(name + " must be between " + min + " and " + max + ", but saw " + value); } } /* * Helper that throws a NullPointerException if the given value is null */ private static void ensureNotNull(String name, Object value) { if (value == null) { throw new NullPointerException("null value was passed for " + name); } } /** * Returns an RGB integer made from the given red, green, and blue components * from 0-255. The returned integer is suitable for use with various RGB * integer methods in this class such as setPixel. * @param r red component from 0-255 (bits 8-15) * @param g green component from 0-255 (bits 16-23) * @param b blue component from 0-255 (bits 24-31) * @return RGB integer with full 255 for alpha and r-g-b in bits 8-31 * @throws IllegalArgumentException if r, g, or b is not in 0-255 range */ public static int toRgbInteger(int r, int g, int b) { return toRgbInteger(/* alpha */ 255, r, g, b); } /** * Returns an RGB integer made from the given alpha, red, green, and blue components * from 0-255. The returned integer is suitable for use with various RGB * integer methods in this class such as setPixel. * @param alpha alpha (transparency) component from 0-255 (bits 0-7) * @param r red component from 0-255 (bits 8-15) * @param g green component from 0-255 (bits 16-23) * @param b blue component from 0-255 (bits 24-31) * @return RGB integer with the given four components * @throws IllegalArgumentException if alpha, r, g, or b is not in 0-255 range */ public static int toRgbInteger(int alpha, int r, int g, int b) { ensureInRange("alpha", alpha, 0, 255); ensureInRange("red", r, 0, 255); ensureInRange("green", g, 0, 255); ensureInRange("blue", b, 0, 255); return ((alpha & 0x000000ff) << 24) | ((r & 0x000000ff) << 16) | ((g & 0x000000ff) << 8) | ((b & 0x000000ff)); } /* * Returns whether the current program is running in the DrJava editor. * This was needed in the past because DrJava messed with some settings. */ private static boolean usingDrJava() { try { return System.getProperty("drjava.debug.port") != null || System.getProperty("java.class.path").toLowerCase().indexOf("drjava") >= 0; } catch (SecurityException e) { // running as an applet, or something return false; } } // fields private ActionListener actionListener; private List frames; // stores frames of animation to save private boolean animated = false; // changes to true if sleep() is called private boolean antialias = true; // true to smooth corners of shapes private boolean gridLines = false; // grid lines every 10px on screen private BufferedImage image; // remembers drawing commands private Color backgroundColor = Color.WHITE; private Gif89Encoder encoder; // for saving animations private Graphics g3; // new field to support DebuggingGraphics private Graphics2D g2; // graphics context for painting private ImagePanel imagePanel; // real drawing surface private int currentZoom = 1; // panel's zoom factor for drawing private int gridLinesPxGap = GRID_LINES_PX_GAP_DEFAULT; // px between grid lines private int initialPixel; // initial value in each pixel, for clear() private int instanceNumber; // every DPanel has a unique number private int width; // dimensions of window frame private int height; // dimensions of window frame private JFileChooser chooser; // file chooser to save files private JFrame frame; // overall window frame private JLabel statusBar; // status bar showing mouse position private JPanel panel; // overall drawing surface private long createTime; // time at which DrawingPanel was constructed private Map counts; // new field to support DebuggingGraphics private MouseInputListener mouseListener; private String callingClassName; // name of class that constructed this panel private Timer timer; // animation timer private WindowListener windowListener; /** * Constructs a drawing panel with a default width and height enclosed in a window. * Uses DEFAULT_WIDTH and DEFAULT_HEIGHT for the panel's size. */ public DrawingPanel() { this(DEFAULT_WIDTH, DEFAULT_HEIGHT); } /** * Constructs a drawing panel of given width and height enclosed in a window. * @param width panel's width in pixels * @param height panel's height in pixels */ public DrawingPanel(int width, int height) { ensureInRange("width", width, 0, MAX_SIZE); ensureInRange("height", height, 0, MAX_SIZE); checkAnimationSettings(); if (DEBUG) System.out.println("DrawingPanel(): going to grab lock"); synchronized (LOCK) { instances++; instanceNumber = instances; // each DrawingPanel stores its own int number if (shutdownThread == null && !usingDrJava()) { if (DEBUG) System.out.println("DrawingPanel(): starting idle thread"); shutdownThread = new Thread(new Runnable() { // Runnable implementation; used for shutdown thread. public void run() { boolean save = shouldSave(); try { while (true) { // maybe shut down the program, if no more DrawingPanels are onscreen // and main has finished executing save |= shouldSave(); if (DEBUG) System.out.println("DrawingPanel idle thread: instances=" + instances + ", save=" + save + ", main active=" + mainIsActive()); if ((instances == 0 || save) && !mainIsActive()) { try { System.exit(0); } catch (SecurityException sex) { if (DEBUG) System.out.println("DrawingPanel idle thread: unable to exit program: " + sex); } } Thread.sleep(250); } } catch (Exception e) { if (DEBUG) System.out.println("DrawingPanel idle thread: exception caught: " + e); } } }); // shutdownThread.setPriority(Thread.MIN_PRIORITY); shutdownThread.setName("DrawingPanel-shutdown"); shutdownThread.start(); } } this.width = width; this.height = height; if (DEBUG) System.out.println("DrawingPanel(w=" + width + ",h=" + height + ",anim=" + isAnimated() + ",graph=" + isGraphical() + ",save=" + shouldSave()); if (isAnimated() && shouldSave()) { // image must be no more than 256 colors image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); // image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); antialias = false; // turn off anti-aliasing to save palette colors // initially fill the entire frame with the background color, // because it won't show through via transparency like with a full ARGB image Graphics g = image.getGraphics(); g.setColor(backgroundColor); g.fillRect(0, 0, width + 1, height + 1); } else { image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } initialPixel = image.getRGB(0, 0); g2 = (Graphics2D) image.getGraphics(); // new field assignments for DebuggingGraphics g3 = new DebuggingGraphics(); counts = new TreeMap(); g2.setColor(Color.BLACK); if (antialias) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } if (isAnimated()) { initializeAnimation(); } if (isGraphical()) { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { // empty } statusBar = new JLabel(" "); statusBar.setBorder(BorderFactory.createLineBorder(Color.BLACK)); panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); panel.setBackground(backgroundColor); panel.setPreferredSize(new Dimension(width, height)); imagePanel = new ImagePanel(image); imagePanel.setBackground(backgroundColor); panel.add(imagePanel); // listen to mouse movement mouseListener = new DPMouseListener(); panel.addMouseMotionListener(mouseListener); // main window frame frame = new JFrame(TITLE); // frame.setResizable(false); windowListener = new DPWindowListener(); frame.addWindowListener(windowListener); // JPanel center = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); JScrollPane center = new JScrollPane(panel); // center.add(panel); frame.getContentPane().add(center); frame.getContentPane().add(statusBar, "South"); frame.setBackground(Color.DARK_GRAY); // menu bar actionListener = new DPActionListener(); setupMenuBar(); frame.pack(); center(frame); frame.setVisible(true); if (!shouldSave()) { toFront(frame); } // repaint timer so that the screen will update createTime = System.currentTimeMillis(); timer = new Timer(DELAY, actionListener); timer.start(); } else if (shouldSave()) { // headless mode; just set a hook on shutdown to save the image callingClassName = getCallingClassName(); try { Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { // run on shutdown to save the image public void run() { if (DEBUG) System.out.println("DrawingPanel.run(): Running shutdown hook"); if (DEBUG) System.out.println("DrawingPanel shutdown hook: instances=" + instances); try { String filename = System.getProperty(SAVE_PROPERTY); if (filename == null) { filename = callingClassName + ".png"; } if (isAnimated()) { saveAnimated(filename); } else { save(filename); } } catch (SecurityException e) { System.err.println("Security error while saving image: " + e); } catch (IOException e) { System.err.println("Error saving image: " + e); } } })); } catch (Exception e) { if (DEBUG) System.out.println("DrawingPanel(): unable to add shutdown hook: " + e); } } } /** * Adds the given event listener to respond to key events on this panel. * @param listener the key event listener to attach */ public void addKeyListener(KeyListener listener) { ensureNotNull("listener", listener); frame.addKeyListener(listener); panel.setFocusable(false); frame.requestFocusInWindow(); frame.requestFocus(); } /** * Adds the given event listener to respond to mouse events on this panel. * @param listener the mouse event listener to attach */ public void addMouseListener(MouseListener listener) { ensureNotNull("listener", listener); panel.addMouseListener(listener); if (listener instanceof MouseMotionListener) { panel.addMouseMotionListener((MouseMotionListener) listener); } } /** * Adds the given event listener to respond to mouse events on this panel. */ // public void addMouseListener(MouseMotionListener listener) { // panel.addMouseMotionListener(listener); // if (listener instanceof MouseListener) { // panel.addMouseListener((MouseListener) listener); // } // } // /** // * Adds the given event listener to respond to mouse events on this panel. // */ // public void addMouseListener(MouseInputListener listener) { // addMouseListener((MouseListener) listener); // } /* * Whether the panel should automatically switch to animated mode * if it calls the sleep method. */ private boolean autoEnableAnimationOnSleep() { return propertyIsTrue(AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY); } /* * Moves the given JFrame to the center of the screen. */ private void center(Window frame) { Toolkit tk = Toolkit.getDefaultToolkit(); Dimension screen = tk.getScreenSize(); int x = Math.max(0, (screen.width - frame.getWidth()) / 2); int y = Math.max(0, (screen.height - frame.getHeight()) / 2); frame.setLocation(x, y); } /* * Constructs and initializes our JFileChooser field if necessary. */ private void checkChooser() { if (chooser == null) { chooser = new JFileChooser(); try { chooser.setCurrentDirectory(new File(System.getProperty("user.dir"))); } catch (Exception e) { // empty } chooser.setMultiSelectionEnabled(false); chooser.setFileFilter(new DPFileFilter()); } } /** * Erases all drawn shapes/lines/colors from the panel. */ public void clear() { int[] pixels = new int[width * height]; for (int i = 0; i < pixels.length; i++) { pixels[i] = initialPixel; } image.setRGB(0, 0, width, height, pixels, 0, 1); } /* * Compares the current DrawingPanel image to an image file on disk. */ private void compareToFile() { // save current image to a temp file try { String tempFile = saveToTempFile(); // use file chooser dialog to find image to compare against checkChooser(); if (chooser.showOpenDialog(frame) != JFileChooser.APPROVE_OPTION) { return; } // user chose a file; let's diff it new DiffImage(chooser.getSelectedFile().toString(), tempFile); } catch (IOException ioe) { JOptionPane.showMessageDialog(frame, "Unable to compare images: \n" + ioe); } } /* * Compares the current DrawingPanel image to an image file on the web. */ private void compareToURL() { // save current image to a temp file try { String tempFile = saveToTempFile(); // get list of images to compare against from web site URL url = new URL(COURSE_WEB_SITE); Scanner input = new Scanner(url.openStream()); List lines = new ArrayList(); List filenames = new ArrayList(); while (input.hasNextLine()) { String line = input.nextLine().trim(); if (line.length() == 0) { continue; } if (line.startsWith("#")) { // a comment if (line.endsWith(":")) { // category label lines.add(line); line = line.replaceAll("#\\s*", ""); filenames.add(line); } } else { lines.add(line); // get filename int lastSlash = line.lastIndexOf('/'); if (lastSlash >= 0) { line = line.substring(lastSlash + 1); } // remove extension int dot = line.lastIndexOf('.'); if (dot >= 0) { line = line.substring(0, dot); } filenames.add(line); } } input.close(); if (filenames.isEmpty()) { JOptionPane.showMessageDialog(frame, "No valid web files found to compare against.", "Error: no web files found", JOptionPane.ERROR_MESSAGE); return; } else { String fileURL = null; if (filenames.size() == 1) { // only one choice; take it fileURL = lines.get(0); } else { // user chooses file to compare against int choice = showOptionDialog(frame, "File to compare against?", "Choose File", filenames.toArray(new String[0])); if (choice < 0) { return; } // user chose a file; let's diff it fileURL = lines.get(choice); } new DiffImage(fileURL, tempFile); } } catch (NoRouteToHostException nrthe) { JOptionPane.showMessageDialog(frame, "You do not appear to have a working internet connection.\nPlease check your internet settings and try again.\n\n" + nrthe); } catch (UnknownHostException uhe) { JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + uhe); } catch (SocketException se) { JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + se); } catch (IOException ioe) { JOptionPane.showMessageDialog(frame, "Unable to compare images: \n" + ioe); } } /* * Closes the DrawingPanel and exits the program. */ private void exit() { if (isGraphical()) { frame.setVisible(false); frame.dispose(); } try { System.exit(0); } catch (SecurityException e) { // if we're running in an applet or something, can't do System.exit } } /* * Returns a best guess about the name of the class that constructed this panel. */ private String getCallingClassName() { StackTraceElement[] stack = new RuntimeException().getStackTrace(); String className = this.getClass().getName(); for (StackTraceElement element : stack) { String cl = element.getClassName(); if (!className.equals(cl)) { className = cl; break; } } return className; } /** * Returns a map of counts of occurrences of calls of various drawing methods. * You can print this map to see how many times your graphics methods have * been called to aid in debugging. * @return map of {method name, count} pairs */ public Map getCounts() { return Collections.unmodifiableMap(counts); } /** * A variation of getGraphics that returns an object that records * a count for various drawing methods. * See also: getCounts * @return debug Graphics object */ public Graphics getDebuggingGraphics() { if (g3 == null) { g3 = new DebuggingGraphics(); } return g3; } /** * Obtain the Graphics object to draw on the panel. * @return panel's Graphics object */ public Graphics2D getGraphics() { return g2; } /* * Creates the buffered image for drawing on this panel. */ private BufferedImage getImage() { // create second image so we get the background color BufferedImage image2; if (isAnimated()) { image2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); } else { image2 = new BufferedImage(width, height, image.getType()); } Graphics g = image2.getGraphics(); // if (DEBUG) System.out.println("DrawingPanel getImage setting background to " + backgroundColor); g.setColor(backgroundColor); g.fillRect(0, 0, width, height); g.drawImage(image, 0, 0, panel); return image2; } /** * Returns the drawing panel's height in pixels. * @return drawing panel's height in pixels */ public int getHeight() { return height; } /** * Returns the color of the pixel at the given x/y coordinate as a Color object. * If nothing has been explicitly drawn on this particular pixel, the panel's * background color is returned. * @param x x-coordinate of pixel to retrieve * @param y y-coordinate of pixel to retrieve * @return pixel (x, y) color as a Color object * @throws IllegalArgumentException if (x, y) is out of range */ public Color getPixel(int x, int y) { int rgb = getPixelRGB(x, y); if (getAlpha(rgb) == 0) { return backgroundColor; } else { return new Color(rgb, /* hasAlpha */ true); } } /** * Returns the color of the pixel at the given x/y coordinate as an RGB integer. * The individual red, green, and blue components of the RGB integer can be * extracted from this by calling DrawingPanel.getRed, getGreen, and getBlue. * If nothing has been explicitly drawn on this particular pixel, the panel's * background color is returned. * See also: getPixel. * @param x x-coordinate of pixel to retrieve * @param y y-coordinate of pixel to retrieve * @return pixel (x, y) color as an RGB integer * @throws IllegalArgumentException if (x, y) is out of range */ public int getPixelRGB(int x, int y) { ensureInRange("x", x, 0, getWidth() - 1); ensureInRange("y", y, 0, getHeight() - 1); int rgb = image.getRGB(x, y); if (getAlpha(rgb) == 0) { return backgroundColor.getRGB(); } else { return rgb; } } /** * Returns the colors of all pixels in this DrawingPanel as a 2-D array * of Color objects. * The first index of the array is the y-coordinate, and the second index * is the x-coordinate. So, for example, index [r][c] represents the RGB * pixel data for the pixel at position (x=c, y=r). * @return 2D array of colors (row-major) */ public Color[][] getPixels() { Color[][] pixels = new Color[getHeight()][getWidth()]; for (int row = 0; row < pixels.length; row++) { for (int col = 0; col < pixels[0].length; col++) { // note axis inversion; x/y => col/row pixels[row][col] = getPixel(col, row); } } return pixels; } /** * Returns the colors of all pixels in this DrawingPanel as a 2-D array * of RGB integers. * The first index of the array is the y-coordinate, and the second index * is the x-coordinate. So, for example, index [r][c] represents the RGB * pixel data for the pixel at position (x=c, y=r). * The individual red, green, and blue components of each RGB integer can be * extracted from this by calling DrawingPanel.getRed, getGreen, and getBlue. * @return 2D array of RGB integers (row-major) */ public int[][] getPixelsRGB() { int[][] pixels = new int[getHeight()][getWidth()]; int backgroundRGB = backgroundColor.getRGB(); for (int row = 0; row < pixels.length; row++) { for (int col = 0; col < pixels[0].length; col++) { // note axis inversion; x/y => col/row int px = image.getRGB(col, row); if (getAlpha(px) == 0) { pixels[col][row] = backgroundRGB; } else { pixels[col][row] = px; } } } return pixels; } /** * Returns the drawing panel's pixel size (width, height) as a Dimension object. * @return panel's size */ public Dimension getSize() { return new Dimension(width, height); } /** * Returns the drawing panel's width in pixels. * @return panel's width */ public int getWidth() { return width; } /** * Returns the drawing panel's current zoom factor. * Initially this is 1 to indicate 100% zoom, the original size. * A factor of 2 would indicate 200% zoom, and so on. * @return zoom factor (default 1) */ public int getZoom() { return currentZoom; } /** * Internal method; * notifies the panel when images are loaded and updated. * This is a required method of ImageObserver interface. * This is an internal method not meant to be called by clients. * @param img internal method; do not call * @param infoflags internal method; do not call * @param x internal method; do not call * @param y internal method; do not call * @param width internal method; do not call * @param height internal method; do not call */ @Override public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { imagePanel.imageUpdate(img, infoflags, x, y, width, height); return false; } /* * Sets up state for drawing and saving frames of animation to a GIF image. */ private void initializeAnimation() { frames = new ArrayList(); encoder = new Gif89Encoder(); /* try { if (hasProperty(SAVE_PROPERTY)) { stream = new FileOutputStream(System.getProperty(SAVE_PROPERTY)); } // encoder.startEncoding(stream); } catch (IOException e) { System.out.println(e); } */ } /* * Returns whether this drawing panel is in animation mode. */ private boolean isAnimated() { return animated || propertyIsTrue(ANIMATED_PROPERTY); } /* * Returns whether this drawing panel is going to be displayed on screen. * This is almost always true except in some server environments where * the DrawingPanel is run 'headless' without a GUI, often for scripting * and automation purposes. */ private boolean isGraphical() { return !hasProperty(SAVE_PROPERTY) && !hasProperty(HEADLESS_PROPERTY); } /* * Returns true if the drawing panel class is in multiple mode. * This would be true if the current program pops up several drawing panels * and we want to save the state of each of them to a different file. */ private boolean isMultiple() { return propertyIsTrue(MULTIPLE_PROPERTY); } /** * Loads an image from the given file on disk and returns it * as an Image object. * @param filename name/path of the file to load * @return loaded image object * @throws NullPointerException if filename is null * @throws RuntimeException if the given file is not found */ public Image loadImage(String filename) { ensureNotNull("filename", filename); if (!(new File(filename)).exists()) { throw new RuntimeException("DrawingPanel.loadImage: File not found: " + filename); } Image img = Toolkit.getDefaultToolkit().getImage(filename); MediaTracker mt = new MediaTracker(imagePanel); mt.addImage(img, 0); try { mt.waitForAll(); } catch (InterruptedException ie) { // empty } return img; } /** * Adds an event handler for mouse clicks. * You can pass a lambda function here to be called when a mouse click event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onClick(DPMouseEventHandler e) { onMouseClick(e); } /** * Adds an event handler for mouse drags. * You can pass a lambda function here to be called when a mouse drag event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onDrag(DPMouseEventHandler e) { onMouseDrag(e); } /** * Adds an event handler for mouse enters. * You can pass a lambda function here to be called when a mouse enter event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onEnter(DPMouseEventHandler e) { onMouseEnter(e); } /** * Adds an event handler for mouse exits. * You can pass a lambda function here to be called when a mouse exit event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onExit(DPMouseEventHandler e) { onMouseExit(e); } /** * Adds an event handler for key presses. * You can pass a lambda function here to be called when a key press event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onKeyDown(DPKeyEventHandler e) { ensureNotNull("event handler", e); DPKeyEventHandlerAdapter adapter = new DPKeyEventHandlerAdapter(e, "press"); addKeyListener(adapter); } /** * Adds an event handler for key releases. * You can pass a lambda function here to be called when a key release event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onKeyUp(DPKeyEventHandler e) { ensureNotNull("event handler", e); DPKeyEventHandlerAdapter adapter = new DPKeyEventHandlerAdapter(e, "release"); addKeyListener(adapter); } /** * Adds an event handler for mouse clicks. * You can pass a lambda function here to be called when a mouse click event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onMouseClick(DPMouseEventHandler e) { ensureNotNull("event handler", e); DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "click"); addMouseListener((MouseListener) adapter); } /** * Adds an event handler for mouse button down events. * You can pass a lambda function here to be called when a mouse button down event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onMouseDown(DPMouseEventHandler e) { ensureNotNull("event handler", e); DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "press"); addMouseListener((MouseListener) adapter); } /** * Adds an event handler for mouse drags. * You can pass a lambda function here to be called when a mouse drag event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onMouseDrag(DPMouseEventHandler e) { ensureNotNull("event handler", e); DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "drag"); addMouseListener((MouseListener) adapter); } /** * Adds an event handler for mouse enters. * You can pass a lambda function here to be called when a mouse enter event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onMouseEnter(DPMouseEventHandler e) { ensureNotNull("event handler", e); DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "enter"); addMouseListener((MouseListener) adapter); } /** * Adds an event handler for mouse exits. * You can pass a lambda function here to be called when a mouse exit event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onMouseExit(DPMouseEventHandler e) { ensureNotNull("event handler", e); DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "exit"); addMouseListener((MouseListener) adapter); } /** * Adds an event handler for mouse movement. * You can pass a lambda function here to be called when a mouse move event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onMouseMove(DPMouseEventHandler e) { ensureNotNull("event handler", e); DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "move"); addMouseListener((MouseListener) adapter); } /** * Adds an event handler for mouse button up events. * You can pass a lambda function here to be called when a mouse button up event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onMouseUp(DPMouseEventHandler e) { ensureNotNull("event handler", e); DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "release"); addMouseListener((MouseListener) adapter); } /** * Adds an event handler for mouse movement. * You can pass a lambda function here to be called when a mouse move event occurs. * @param e event handler function to call * @throws NullPointerException if event handler is null */ public void onMove(DPMouseEventHandler e) { onMouseMove(e); } /* * Returns whether the drawing panel should be closed and the program * should be shut down. */ private boolean readyToClose() { /* if (isAnimated()) { // wait a little longer, in case animation is sleeping return System.currentTimeMillis() > createTime + 5 * DELAY; } else { return System.currentTimeMillis() > createTime + 4 * DELAY; } */ return (instances == 0 || shouldSave()) && !mainIsActive(); } /* * Replaces all occurrences of the given old color with the given new color. */ private void replaceColor(BufferedImage image, Color oldColor, Color newColor) { int oldRGB = oldColor.getRGB(); int newRGB = newColor.getRGB(); for (int y = 0; y < image.getHeight(); y++) { for (int x = 0; x < image.getWidth(); x++) { if (image.getRGB(x, y) == oldRGB) { image.setRGB(x, y, newRGB); } } } } /** * Takes the current contents of the drawing panel and writes them to * the given file. * @param filename name/path of the file to save * @throws NullPointerException if filename is null * @throws IOException if the given file cannot be written */ public void save(String filename) throws IOException { ensureNotNull("filename", filename); BufferedImage image2 = getImage(); // if zoomed, scale image before saving it if (SAVE_SCALED_IMAGES && currentZoom != 1) { BufferedImage zoomedImage = new BufferedImage(width * currentZoom, height * currentZoom, image.getType()); Graphics2D g = (Graphics2D) zoomedImage.getGraphics(); g.setColor(Color.BLACK); if (antialias) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } g.scale(currentZoom, currentZoom); g.drawImage(image2, 0, 0, imagePanel); image2 = zoomedImage; } // if saving multiple panels, append number // (e.g. output_*.png becomes output_1.png, output_2.png, etc.) if (isMultiple()) { filename = filename.replaceAll("\\*", String.valueOf(instanceNumber)); } int lastDot = filename.lastIndexOf("."); String extension = filename.substring(lastDot + 1); // write file // TODO: doesn't save background color I don't think ImageIO.write(image2, extension, new File(filename)); } /** * Takes the current contents of the drawing panel and writes them to * the given file. * @param filename name/path of the file to save * @throws NullPointerException if filename is null * @throws IOException if the given file cannot be written */ public void saveAnimated(String filename) throws IOException { ensureNotNull("filename", filename); // add one more final frame if (DEBUG) System.out.println("DrawingPanel.saveAnimated(" + filename + ")"); frames.add(new ImageFrame(getImage(), 5000)); // encoder.continueEncoding(stream, getImage(), 5000); // Gif89Encoder gifenc = new Gif89Encoder(); // add each frame of animation to the encoder try { for (int i = 0; i < frames.size(); i++) { ImageFrame imageFrame = frames.get(i); encoder.addFrame(imageFrame.image); encoder.getFrameAt(i).setDelay(imageFrame.delay); imageFrame.image.flush(); frames.set(i, null); } } catch (OutOfMemoryError e) { System.out.println("Out of memory when saving"); } // gifenc.setComments(annotation); // gifenc.setUniformDelay((int) Math.round(100 / frames_per_second)); // gifenc.setUniformDelay(DELAY); // encoder.setBackground(backgroundColor); encoder.setLoopCount(0); encoder.encode(new FileOutputStream(filename)); } /* * Called when the user presses the "Save As" menu item. * Pops up a file chooser prompting the user to save their panel to an image. */ private void saveAs() { String filename = saveAsHelper("png"); if (filename != null) { try { save(filename); // save the file } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex); } } } /* * Called when the user presses the "Save As" menu item on an animated panel. * Pops up a file chooser prompting the user to save their panel to an image. */ private void saveAsAnimated() { String filename = saveAsHelper("gif"); if (filename != null) { try { // record that the file should be saved next time PrintStream out = new PrintStream(new File(ANIMATION_FILE_NAME)); out.println(filename); out.close(); JOptionPane.showMessageDialog(frame, "Due to constraints about how DrawingPanel works, you'll need to\n" + "re-run your program. When you run it the next time, DrawingPanel will \n" + "automatically save your animated image as: " + new File(filename).getName() ); } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to store animation settings:\n" + ex); } } } /* * A helper method to facilitate the Save As action for both animated * and non-animated images. */ private String saveAsHelper(String extension) { // use file chooser dialog to get filename to save into checkChooser(); if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) { return null; } File selectedFile = chooser.getSelectedFile(); String filename = selectedFile.toString(); if (!filename.toLowerCase().endsWith(extension)) { // Windows is dumb about extensions with file choosers filename += "." + extension; } // confirm overwrite of file if (new File(filename).exists() && JOptionPane.showConfirmDialog( frame, "File exists. Overwrite?", "Overwrite?", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { return null; } return filename; } /* * Saves the drawing panel's image to a temporary file and returns * that file's name. */ private String saveToTempFile() throws IOException { File currentImageFile = File.createTempFile("current_image", ".png"); save(currentImageFile.toString()); return currentImageFile.toString(); } /** * Sets whether the panel will always cover other windows (default false). * @param alwaysOnTop true if the panel should always cover other windows */ public void setAlwaysOnTop(boolean alwaysOnTop) { if (frame != null) { frame.setAlwaysOnTop(alwaysOnTop); } } /** * Sets whether the panel should use anti-aliased / smoothed graphics (default true). * @param antiAlias true if the panel should be smoothed */ public void setAntiAlias(boolean antiAlias) { this.antialias = antiAlias; Object value = antiAlias ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF; if (g2 != null) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, value); } imagePanel.repaint(); } /** * Sets the background color of the drawing panel to be the given color. * @param c color to use as background * @throws NullPointerException if color is null */ public void setBackground(Color c) { ensureNotNull("color", c); Color oldBackgroundColor = backgroundColor; backgroundColor = c; if (isGraphical()) { panel.setBackground(c); imagePanel.setBackground(c); } // with animated images, need to palette-swap the old bg color for the new // because there's no notion of transparency in a palettized 8-bit image if (isAnimated()) { replaceColor(image, oldBackgroundColor, c); } } /** * Sets the background color of the drawing panel to be the color * represented by the given RGB integer. * @param rgb RGB integer to use as background color (full alpha assumed/applied) */ public void setBackground(int rgb) { setBackground(new Color(rgb & 0xff000000, /* hasAlpha */ true)); } /** * Enables or disables the drawing of grid lines on top of the image to help * with debugging sizes and coordinates. * By default the grid lines will be shown every 10 pixels in each dimension. * @param gridLines whether to show grid lines (true) or not (false) */ public void setGridLines(boolean gridLines) { setGridLines(gridLines, GRID_LINES_PX_GAP_DEFAULT); } /** * Enables or disables the drawing of grid lines on top of the image to help * with debugging sizes and coordinates. * The grid lines will be shown every pxGap pixels in each dimension. * @param gridLines whether to show grid lines (true) or not (false) * @param pxGap number of pixels between grid lines */ public void setGridLines(boolean gridLines, int pxGap) { this.gridLines = gridLines; this.gridLinesPxGap = pxGap; imagePanel.repaint(); } /** * Sets the drawing panel's height in pixels to the given value. * After calling this method, the client must call getGraphics() again * to get the new graphics context of the newly enlarged image buffer. * @param height height, in pixels * @throws IllegalArgumentException if height is negative or exceeds MAX_SIZE */ public void setHeight(int height) { setSize(getWidth(), height); } /** * Sets the color of the pixel at the given x/y coordinate to be the given color. * If the color is null, the call has no effect. * @param x x-coordinate of pixel to set * @param y y-coordinate of pixel to set * @param color Color to set the pixel to use * @throws IllegalArgumentException if x or y is out of bounds * @throws NullPointerException if color is null */ public void setPixel(int x, int y, Color color) { ensureInRange("x", x, 0, getWidth() - 1); ensureInRange("y", y, 0, getHeight() - 1); ensureNotNull("color", color); image.setRGB(x, y, color.getRGB()); } /** * Sets the color of the pixel at the given x/y coordinate to be the color * represented by the given RGB integer. * The passed RGB integer's alpha value is ignored and a full alpha of 255 * is always used here, to avoid common bugs with using a 0 value for alpha. * See also: setPixel. * See also: setPixelRGB. * @param x x-coordinate of pixel to set * @param y y-coordinate of pixel to set * @param rgb RGB integer representing the color to set the pixel to use * @throws IllegalArgumentException if x or y is out of bounds */ public void setPixel(int x, int y, int rgb) { setPixelRGB(x, y, rgb); } /** * Sets the color of the pixel at the given x/y coordinate to be the color * represented by the given RGB integer. * The passed RGB integer's alpha value is ignored and a full alpha of 255 * is always used here, to avoid common bugs with using a 0 value for alpha. * See also: setPixel. * @param x x-coordinate of pixel to set * @param y y-coordinate of pixel to set * @param rgb RGB integer representing the color to set the pixel to use * @throws IllegalArgumentException if x or y is out of bounds */ public void setPixelRGB(int x, int y, int rgb) { ensureInRange("x", x, 0, getWidth() - 1); ensureInRange("y", y, 0, getHeight() - 1); image.setRGB(x, y, rgb | PIXEL_ALPHA); } /** * Sets the colors of all pixels in this DrawingPanel to the colors * in the given 2-D array of Color objects. * The first index of the array is the y-coordinate, and the second index * is the x-coordinate. So, for example, index [r][c] represents the RGB * pixel data for the pixel at position (x=c, y=r). * If the given array's dimensions do not match the width/height of the * drawing panel, the panel is resized to match the array. * If the pixel array is null or size 0, the call has no effect. * If any rows or colors in the array are null, those pixels will be ignored. * The 2-D array passed is assumed to be rectangular in length (not jagged). * @param pixels 2D array of pixels (row-major) * @throws NullPointerException if pixels array is null */ public void setPixels(Color[][] pixels) { ensureNotNull("pixels", pixels); if (pixels != null && pixels.length > 0 && pixels[0] != null) { if (width != pixels[0].length || height != pixels.length) { setSize(pixels[0].length, pixels.length); } for (int row = 0; row < height; row++) { if (pixels[row] != null) { for (int col = 0; col < width; col++) { if (pixels[row][col] != null) { int rgb = pixels[row][col].getRGB(); image.setRGB(col, row, rgb); } } } } } } /** * Sets the colors of all pixels in this DrawingPanel to the colors * represented by the given 2-D array of RGB integers. * The first index of the array is the y-coordinate, and the second index * is the x-coordinate. So, for example, index [r][c] represents the RGB * pixel data for the pixel at position (x=c, y=r). * If the given array's dimensions do not match the width/height of the * drawing panel, the panel is resized to match the array. * If the pixel array is null or size 0, the call has no effect. * The 2-D array passed is assumed to be rectangular in length (not jagged). * @param pixels 2D array of pixels (row-major) * @throws NullPointerException if pixels array is null */ public void setPixels(int[][] pixels) { setPixelsRGB(pixels); } /** * Sets the colors of all pixels in this DrawingPanel to the colors * represented by the given 2-D array of RGB integers. * The first index of the array is the y-coordinate, and the second index * is the x-coordinate. So, for example, index [r][c] represents the RGB * pixel data for the pixel at position (x=c, y=r). * If the given array's dimensions do not match the width/height of the * drawing panel, the panel is resized to match the array. * If the pixel array is null or size 0, the call has no effect. * The 2-D array passed is assumed to be rectangular in length (not jagged). * @param pixels 2D array of pixels (row-major) * @throws NullPointerException if pixels array is null */ public void setPixelsRGB(int[][] pixels) { ensureNotNull("pixels", pixels); if (pixels != null && pixels.length > 0 && pixels[0] != null) { if (width != pixels[0].length || height != pixels.length) { setSize(pixels[0].length, pixels.length); } for (int row = 0; row < height; row++) { if (pixels[row] != null) { for (int col = 0; col < width; col++) { image.setRGB(col, row, pixels[col][row] | PIXEL_ALPHA); } } } } } /** * Sets the drawing panel's pixel size (width, height) to the given values. * After calling this method, the client must call getGraphics() again * to get the new graphics context of the newly enlarged image buffer. * @param width width, in pixels * @param height height, in pixels * @throws IllegalArgumentException if width/height is negative or exceeds MAX_SIZE */ public void setSize(int width, int height) { ensureInRange("width", width, 0, MAX_SIZE); ensureInRange("height", height, 0, MAX_SIZE); // replace the image buffer for drawing BufferedImage newImage = new BufferedImage(width, height, image.getType()); imagePanel.setImage(newImage); newImage.getGraphics().drawImage(image, 0, 0, imagePanel); this.width = width; this.height = height; image = newImage; g2 = (Graphics2D) newImage.getGraphics(); g2.setColor(Color.BLACK); if (antialias) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } zoom(currentZoom); if (isGraphical()) { frame.pack(); } } /* * Sets the text that will appear in the drawing panel's bottom status bar. */ private void setStatusBarText(String text) { if (currentZoom != 1) { text += " (current zoom: " + currentZoom + "x" + ")"; } statusBar.setText(text); } /* * Initializes the drawing panel's menu bar items. */ private void setupMenuBar() { // abort compare if we're running as an applet or in a secure environment boolean secure = (System.getSecurityManager() != null); JMenuItem saveAs = new JMenuItem("Save As...", 'A'); saveAs.addActionListener(actionListener); saveAs.setAccelerator(KeyStroke.getKeyStroke("ctrl S")); saveAs.setEnabled(!secure); JMenuItem saveAnimated = new JMenuItem("Save Animated GIF...", 'G'); saveAnimated.addActionListener(actionListener); saveAnimated.setAccelerator(KeyStroke.getKeyStroke("ctrl A")); saveAnimated.setEnabled(!secure); JMenuItem compare = new JMenuItem("Compare to File...", 'C'); compare.addActionListener(actionListener); compare.setEnabled(!secure); JMenuItem compareURL = new JMenuItem("Compare to Web File...", 'U'); compareURL.addActionListener(actionListener); compareURL.setAccelerator(KeyStroke.getKeyStroke("ctrl U")); compareURL.setEnabled(!secure); JMenuItem zoomIn = new JMenuItem("Zoom In", 'I'); zoomIn.addActionListener(actionListener); zoomIn.setAccelerator(KeyStroke.getKeyStroke("ctrl EQUALS")); JMenuItem zoomOut = new JMenuItem("Zoom Out", 'O'); zoomOut.addActionListener(actionListener); zoomOut.setAccelerator(KeyStroke.getKeyStroke("ctrl MINUS")); JMenuItem zoomNormal = new JMenuItem("Zoom Normal (100%)", 'N'); zoomNormal.addActionListener(actionListener); zoomNormal.setAccelerator(KeyStroke.getKeyStroke("ctrl 0")); JCheckBoxMenuItem gridLinesItem = new JCheckBoxMenuItem("Grid Lines"); gridLinesItem.setMnemonic('G'); gridLinesItem.setSelected(gridLines); gridLinesItem.addActionListener(actionListener); gridLinesItem.setAccelerator(KeyStroke.getKeyStroke("ctrl G")); JMenuItem exit = new JMenuItem("Exit", 'x'); exit.addActionListener(actionListener); JMenuItem about = new JMenuItem("About...", 'A'); about.addActionListener(actionListener); JMenu file = new JMenu("File"); file.setMnemonic('F'); file.add(compareURL); file.add(compare); file.addSeparator(); file.add(saveAs); file.add(saveAnimated); file.addSeparator(); file.add(exit); JMenu view = new JMenu("View"); view.setMnemonic('V'); view.add(zoomIn); view.add(zoomOut); view.add(zoomNormal); view.addSeparator(); view.add(gridLinesItem); JMenu help = new JMenu("Help"); help.setMnemonic('H'); help.add(about); JMenuBar bar = new JMenuBar(); bar.add(file); bar.add(view); bar.add(help); frame.setJMenuBar(bar); } /** * Show or hide the drawing panel on the screen. * @param visible true to show, false to hide */ public void setVisible(boolean visible) { if (isGraphical()) { frame.setVisible(visible); } } /** * Sets the drawing panel's width in pixels to the given value. * After calling this method, the client must call getGraphics() again * to get the new graphics context of the newly enlarged image buffer. * @param width width, in pixels * @throws IllegalArgumentException if height is negative or exceeds MAX_SIZE */ public void setWidth(int width) { ensureInRange("width", width, 0, MAX_SIZE); setSize(width, getHeight()); } /* * Returns whether the user wants to perform a 'diff' comparison of their * drawing panel with a given expected output image. */ private boolean shouldDiff() { return hasProperty(DIFF_PROPERTY); } /* * Returns whether the user wants to save the drawing panel contents to * a file automatically. */ private boolean shouldSave() { return hasProperty(SAVE_PROPERTY); } /* * Shows a dialog box with the given choices; * returns the index chosen (-1 == canceled). */ private int showOptionDialog(Frame parent, String title, String message, final String[] names) { final JDialog dialog = new JDialog(parent, title, true); JPanel center = new JPanel(new GridLayout(0, 1)); // just a hack to make the return value a mutable reference to an int final int[] hack = {-1}; for (int i = 0; i < names.length; i++) { if (names[i].endsWith(":")) { center.add(new JLabel("" + names[i] + "")); } else { final JButton button = new JButton(names[i]); button.setActionCommand(String.valueOf(i)); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { hack[0] = Integer.parseInt(button.getActionCommand()); dialog.setVisible(false); } }); center.add(button); } } JPanel south = new JPanel(); JButton cancel = new JButton("Cancel"); cancel.setMnemonic('C'); cancel.requestFocus(); cancel.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { dialog.setVisible(false); } }); south.add(cancel); dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); dialog.getContentPane().setLayout(new BorderLayout(10, 5)); if (message != null) { JLabel messageLabel = new JLabel(message); dialog.add(messageLabel, BorderLayout.NORTH); } dialog.add(center); dialog.add(south, BorderLayout.SOUTH); dialog.pack(); dialog.setResizable(false); center(dialog); cancel.requestFocus(); dialog.setVisible(true); cancel.requestFocus(); return hack[0]; } /** * Causes the program to pause for the given amount of time in milliseconds. * This allows for animation by calling pause in a loop. * If the DrawingPanel is not showing on the screen, has no effect. * @param millis number of milliseconds to sleep * @throws IllegalArgumentException if a negative number of ms is passed */ public void sleep(int millis) { ensureInRange("millis", millis, 0, Integer.MAX_VALUE); if (isGraphical() && frame.isVisible()) { // if not even displaying, we don't actually need to sleep if (millis > 0) { try { Thread.sleep(millis); panel.repaint(); // toFront(frame); } catch (Exception e) { // empty } } } // manually enable animation if necessary if (!isAnimated() && !isMultiple() && autoEnableAnimationOnSleep()) { animated = true; initializeAnimation(); } // capture a frame of animation if (isAnimated() && shouldSave() && !isMultiple()) { try { if (frames.size() < MAX_FRAMES) { frames.add(new ImageFrame(getImage(), millis)); } // reset creation timer so that we won't save/close just yet createTime = System.currentTimeMillis(); } catch (OutOfMemoryError e) { System.out.println("Out of memory after capturing " + frames.size() + " frames"); } } } /** * Moves the drawing panel window on top of other windows so it can be seen. */ public void toFront() { toFront(frame); } /* * Brings the given window to the front of the Z-ordering. */ private void toFront(final Window window) { // TODO: remove anonymous inner class EventQueue.invokeLater(new Runnable() { public void run() { if (window != null) { window.toFront(); window.repaint(); } } }); } /** * Zooms the drawing panel in/out to the given factor. * A zoom factor of 1, the default, indicates normal size. * A zoom factor of 2 would indicate 200% size, and so on. * The factor value passed should be at least 1; if not, 1 will be used. * @param zoomFactor the zoom factor to use (1 or greater) */ public void zoom(int zoomFactor) { currentZoom = Math.max(1, zoomFactor); if (isGraphical()) { Dimension size = new Dimension(width * currentZoom, height * currentZoom); imagePanel.setPreferredSize(size); panel.setPreferredSize(size); imagePanel.validate(); imagePanel.revalidate(); panel.validate(); panel.revalidate(); // imagePanel.setSize(size); frame.getContentPane().validate(); imagePanel.repaint(); setStatusBarText(" "); // resize frame if any more space for it exists or it's the wrong size Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); if (size.width <= screen.width || size.height <= screen.height) { frame.pack(); } if (currentZoom != 1) { frame.setTitle(TITLE + " (" + currentZoom + "x zoom)"); } else { frame.setTitle(TITLE); } } } // INNER/NESTED CLASSES /* * Internal action listener for handling events on buttons and GUI components. */ private class DPActionListener implements ActionListener { // used for an internal timer that keeps repainting public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Timer) { // redraw the screen at regular intervals to catch all paint operations panel.repaint(); if (shouldDiff() && System.currentTimeMillis() > createTime + 4 * DELAY) { String expected = System.getProperty(DIFF_PROPERTY); try { String actual = saveToTempFile(); DiffImage diff = new DiffImage(expected, actual); diff.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } catch (IOException ioe) { System.err.println("Error diffing image: " + ioe); } timer.stop(); } else if (shouldSave() && readyToClose()) { // auto-save-and-close if desired try { if (isAnimated()) { saveAnimated(System.getProperty(SAVE_PROPERTY)); } else { save(System.getProperty(SAVE_PROPERTY)); } } catch (IOException ioe) { System.err.println("Error saving image: " + ioe); } exit(); } } else if (e.getActionCommand().equals("Exit")) { exit(); } else if (e.getActionCommand().equals("Compare to File...")) { compareToFile(); } else if (e.getActionCommand().equals("Compare to Web File...")) { new Thread(new Runnable() { public void run() { compareToURL(); } }).start(); } else if (e.getActionCommand().equals("Save As...")) { saveAs(); } else if (e.getActionCommand().equals("Save Animated GIF...")) { saveAsAnimated(); } else if (e.getActionCommand().equals("Zoom In")) { zoom(currentZoom + 1); } else if (e.getActionCommand().equals("Zoom Out")) { zoom(currentZoom - 1); } else if (e.getActionCommand().equals("Zoom Normal (100%)")) { zoom(1); } else if (e.getActionCommand().equals("Grid Lines")) { setGridLines(((JCheckBoxMenuItem) e.getSource()).isSelected()); } else if (e.getActionCommand().equals("About...")) { JOptionPane.showMessageDialog(frame, ABOUT_MESSAGE, ABOUT_MESSAGE_TITLE, JOptionPane.INFORMATION_MESSAGE); } } } /* * Internal file filter class for showing image files in JFileChooser. */ private class DPFileFilter extends FileFilter { public boolean accept(File file) { return file.isDirectory() || (file.getName().toLowerCase().endsWith(".png") || file.getName().toLowerCase().endsWith(".gif")); } public String getDescription() { return "Image files (*.png; *.gif)"; } } // BEGIN EVENT ADAPTER CODE FOR JAVA 8 FUNCTIONAL INTERFACE CLIENTS // EXAMPLE: // panel.onClick( (x, y) -> System.out.println(x + " " + y) ); /** * This functional interface is provided to allow Java 8 clients to write * lambda functions to handle mouse events that occur in a DrawingPanel. */ @FunctionalInterface public static interface DPMouseEventHandler { /** * Called when a mouse event occurs at the given (x, y) position * in the drawing panel window. * @param x x-coordinate at which the event occurred * @param y y-coordinate at which the event occurred */ public void onMouseEvent(int x, int y); } /** * This functional interface is provided to allow Java 8 clients to write * lambda functions to handle key events that occur in a DrawingPanel. */ @FunctionalInterface public static interface DPKeyEventHandler { /** * Called when a key event occurs involving the given key character * in the drawing panel window. * @param keyCode char value that was typed */ public void onKeyEvent(char keyCode); } // internal class to implement DPKeyEventHandler behavior. private class DPKeyEventHandlerAdapter implements KeyListener { private DPKeyEventHandler handler; private String eventType; /** * Constructs a new key handler adapter. * @param handler event handler function * @param eventType type of event to print */ public DPKeyEventHandlerAdapter(DPKeyEventHandler handler, String eventType) { this.handler = handler; this.eventType = eventType.intern(); } /** * Called when a key press occurs. * @param e event that occurred */ @Override public void keyPressed(KeyEvent e) { // empty; see keyTyped } /** * Called when a key release occurs. * @param e event that occurred */ @Override public void keyReleased(KeyEvent e) { if (eventType == "release") { int keyCode = e.getKeyCode(); if (keyCode < ' ') { return; } handler.onKeyEvent(e.getKeyChar()); } } /** * Called when a key type event occurs. * @param e event that occurred */ @Override public void keyTyped(KeyEvent e) { if (eventType == "press") { handler.onKeyEvent(e.getKeyChar()); } } } // internal class to implement DPMouseEventHandler behavior. private class DPMouseEventHandlerAdapter implements MouseInputListener { private DPMouseEventHandler handler; private String eventType; /** * Constructs a new mouse handler adapter. * @param handler event handler function * @param eventType type of event to print */ public DPMouseEventHandlerAdapter(DPMouseEventHandler handler, String eventType) { this.handler = handler; this.eventType = eventType.intern(); } /** * Called when a mouse press occurs. * @param e event that occurred */ @Override public void mousePressed(MouseEvent e) { if (eventType == "press") { handler.onMouseEvent(e.getX(), e.getY()); } } /** * Called when a mouse release occurs. * @param e event that occurred */ @Override public void mouseReleased(MouseEvent e) { if (eventType == "release") { handler.onMouseEvent(e.getX(), e.getY()); } } /** * Called when a mouse click occurs. * @param e event that occurred */ @Override public void mouseClicked(MouseEvent e) { if (eventType == "click") { handler.onMouseEvent(e.getX(), e.getY()); } } /** * Called when a mouse enter occurs. * @param e event that occurred */ @Override public void mouseEntered(MouseEvent e) { if (eventType == "enter") { handler.onMouseEvent(e.getX(), e.getY()); } } /** * Called when a mouse exit occurs. * @param e event that occurred */ @Override public void mouseExited(MouseEvent e) { if (eventType == "exit") { handler.onMouseEvent(e.getX(), e.getY()); } } /** * Called when a mouse movement occurs. * @param e event that occurred */ @Override public void mouseMoved(MouseEvent e) { if (eventType == "move") { handler.onMouseEvent(e.getX(), e.getY()); } } /** * Called when a mouse drag occurs. * @param e event that occurred */ @Override public void mouseDragged(MouseEvent e) { if (eventType == "drag") { handler.onMouseEvent(e.getX(), e.getY()); } } } // END EVENT ADAPTER CODE FOR JAVA 8 FUNCTIONAL INTERFACE CLIENTS // Internal MouseListener class for handling mouse events in the panel. private class DPMouseListener extends MouseInputAdapter { // listens to mouse movement public void mouseMoved(MouseEvent e) { int x = e.getX() / currentZoom; int y = e.getY() / currentZoom; String status = "(x=" + x + ", y=" + y + ")"; if (x >= 0 && x < width && y >= 0 && y < height) { int rgb = getPixelRGB(x, y); int r = getRed(rgb); int g = getGreen(rgb); int b = getBlue(rgb); status += ", r=" + r + " g=" + g + " b=" + b; } setStatusBarText(status); } } // Internal WindowListener class for handling window events in the panel. private class DPWindowListener extends WindowAdapter { // called when DrawingPanel closes, to potentially exit the program public void windowClosing(WindowEvent event) { frame.setVisible(false); synchronized (LOCK) { instances--; } frame.dispose(); } } /* * This inner class passes through calls to the panel's Graphics object g2 * but also records a count of how many times various basic drawing methods * are called. This is used for debugging purposes, so that a client can * compare their counts of various graphical method calls to those from an * expected output as a "sanity check" on their program's behavior. * Notice that it extends Graphics and not Graphics2D, so it is more limited * than g2. * @author Stuart Reges */ private class DebuggingGraphics extends Graphics { public Graphics create() { return g2.create(); } public void translate(int x, int y) { g2.translate(x, y); } public Color getColor() { return g2.getColor(); } public void setPaintMode() { g2.setPaintMode(); } public void setXORMode(Color c1) { g2.setXORMode(c1); } public Font getFont() { return g2.getFont(); } public void setFont(Font font) { g2.setFont(font); } public FontMetrics getFontMetrics(Font f) { return g2.getFontMetrics(); } public Rectangle getClipBounds() { return g2.getClipBounds(); } public void clipRect(int x, int y, int width, int height) { g2.clipRect(x, y, width, height); } public void setClip(int x, int y, int width, int height) { g2.setClip(x, y, width, height); } public Shape getClip() { return g2.getClip(); } public void setClip(Shape clip) { g2.setClip(clip); } public void copyArea(int x, int y, int width, int height, int dx, int dy) { g2.copyArea(x, y, width, height, dx, dy); } public void clearRect(int x, int y, int width, int height) { g2.clearRect(x, y, width, height); } public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { g2.drawRoundRect(x, y, width, height, arcWidth, arcHeight); } public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { g2.fillRoundRect(x, y, width, height, arcWidth, arcHeight); } public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { g2.drawArc(x, y, width, height, startAngle, arcAngle); } public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { g2.fillArc(x, y, width, height, startAngle, arcAngle); } public void drawPolyline(int xPoints[], int yPoints[], int nPoints) { g2.drawPolyline(xPoints, yPoints, nPoints); } public void drawPolygon(int xPoints[], int yPoints[], int nPoints) { g2.drawPolygon(xPoints, yPoints, nPoints); } public void fillPolygon(int xPoints[], int yPoints[], int nPoints) { g2.fillPolygon(xPoints, yPoints, nPoints); } public void drawString(AttributedCharacterIterator iterator, int x, int y) { g2.drawString(iterator, x, y); } public boolean drawImage(Image img, int x, int y, ImageObserver observer) { return g2.drawImage(img, x, y, observer); }; public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { return g2.drawImage(img, x, y, width, height, observer); }; public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { return g2.drawImage(img, x, y, bgcolor, observer); }; public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) { return g2.drawImage(img, x, y, width, height, bgcolor, observer); } public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { return g2.drawImage(img, dx1, dy1, dx2, dy2, sx1, dy1, dx2, sy2, observer); } public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) { return g2.drawImage(img, dx1, dy1, dx2, dy2, sx1, dy1, sx2, sy2, bgcolor, observer); } public void dispose() { g2.dispose(); } public void drawOval(int x, int y, int width, int height) { g2.drawOval(x, y, width, height); recordString("drawOval"); } public void fillOval(int x, int y, int width, int height) { g2.fillOval(x, y, width, height); recordString("fillOval"); } public void drawString(String str, int x, int y) { g2.drawString(str, x, y); recordString("drawString"); } public void drawLine(int x1, int y1, int x2, int y2) { g2.drawLine(x1, y1, x2, y2); recordString("drawLine"); } public void fillRect(int x, int y, int width, int height) { g2.fillRect(x, y, width, height); recordString("fillRect"); } public void drawRect(int x, int y, int width, int height) { g2.drawRect(x, y, width, height); recordString("drawRect"); } public void setColor(Color c) { g2.setColor(c); // recordString("setColor"); } public void recordString(String s) { if (!counts.containsKey(s)) { counts.put(s, 1); } else { counts.put(s, counts.get(s) + 1); } } } // end class DebuggingGraphics /* * This internal class represents a graphical panel that can pop up on the * screen to report the differences between two images. * It is used to allow the client to compare their program's output against * a known correct output and view which pixels differ between the two. */ private class DiffImage extends JPanel implements ActionListener, ChangeListener { private static final long serialVersionUID = 0; private BufferedImage image1; private BufferedImage image2; private String image1name; private int numDiffPixels; private int opacity = 50; private String label1Text = "Expected"; private String label2Text = "Actual"; private boolean highlightDiffs = false; private Color highlightColor = new Color(224, 0, 224); private JLabel image1Label; private JLabel image2Label; private JLabel diffPixelsLabel; private JSlider slider; private JCheckBox box; private JMenuItem saveAsItem; private JMenuItem setImage1Item; private JMenuItem setImage2Item; private JFrame frame; private JButton colorButton; public DiffImage(String file1, String file2) throws IOException { setImage1(file1); setImage2(file2); display(); } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == box) { highlightDiffs = box.isSelected(); repaint(); } else if (source == colorButton) { Color color = JColorChooser.showDialog(frame, "Choose highlight color", highlightColor); if (color != null) { highlightColor = color; colorButton.setBackground(color); colorButton.setForeground(color); repaint(); } } else if (source == saveAsItem) { saveAs(); } else if (source == setImage1Item) { setImage1(); } else if (source == setImage2Item) { setImage2(); } } // Counts number of pixels that differ between the two images. public void countDiffPixels() { if (image1 == null || image2 == null) { return; } int w1 = image1.getWidth(); int h1 = image1.getHeight(); int w2 = image2.getWidth(); int h2 = image2.getHeight(); int wmax = Math.max(w1, w2); int hmax = Math.max(h1, h2); // check each pair of pixels numDiffPixels = 0; for (int y = 0; y < hmax; y++) { for (int x = 0; x < wmax; x++) { int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0; int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0; if (pixel1 != pixel2) { numDiffPixels++; } } } } // initializes diffimage panel public void display() { countDiffPixels(); setupComponents(); setupEvents(); setupLayout(); frame.pack(); center(frame); frame.setVisible(true); toFront(frame); } // draws the given image onto the given graphics context public void drawImageFull(Graphics2D g2, BufferedImage image) { int iw = image.getWidth(); int ih = image.getHeight(); int w = getWidth(); int h = getHeight(); int dw = w - iw; int dh = h - ih; if (dw > 0) { g2.fillRect(iw, 0, dw, ih); } if (dh > 0) { g2.fillRect(0, ih, iw, dh); } if (dw > 0 && dh > 0) { g2.fillRect(iw, ih, dw, dh); } g2.drawImage(image, 0, 0, this); } // paints the DiffImage panel public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; // draw the expected output (image 1) if (image1 != null) { drawImageFull(g2, image1); } // draw the actual output (image 2) if (image2 != null) { Composite oldComposite = g2.getComposite(); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, ((float) opacity) / 100)); drawImageFull(g2, image2); g2.setComposite(oldComposite); } g2.setColor(Color.BLACK); // draw the highlighted diffs (if so desired) if (highlightDiffs && image1 != null && image2 != null) { int w1 = image1.getWidth(); int h1 = image1.getHeight(); int w2 = image2.getWidth(); int h2 = image2.getHeight(); int wmax = Math.max(w1, w2); int hmax = Math.max(h1, h2); // check each pair of pixels g2.setColor(highlightColor); for (int y = 0; y < hmax; y++) { for (int x = 0; x < wmax; x++) { int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0; int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0; if (pixel1 != pixel2) { g2.fillRect(x, y, 1, 1); } } } } } public void save(File file) throws IOException { // String extension = filename.substring(filename.lastIndexOf(".") + 1); // ImageIO.write(diffImage, extension, new File(filename)); String filename = file.getName(); String extension = filename.substring(filename.lastIndexOf(".") + 1); BufferedImage img = new BufferedImage(getPreferredSize().width, getPreferredSize().height, BufferedImage.TYPE_INT_ARGB); img.getGraphics().setColor(getBackground()); img.getGraphics().fillRect(0, 0, img.getWidth(), img.getHeight()); paintComponent(img.getGraphics()); ImageIO.write(img, extension, file); } public void save(String filename) throws IOException { save(new File(filename)); } // Called when "Save As" menu item is clicked public void saveAs() { checkChooser(); if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) { return; } File selectedFile = chooser.getSelectedFile(); try { save(selectedFile.toString()); } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex); } } // called when "Set Image 1" menu item is clicked public void setImage1() { checkChooser(); if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) { return; } File selectedFile = chooser.getSelectedFile(); try { setImage1(selectedFile.toString()); countDiffPixels(); diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)"); image1Label.setText(selectedFile.getName()); frame.pack(); } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to set image 1:\n" + ex); } } // sets image 1 to be the given image public void setImage1(BufferedImage image) { if (image == null) { throw new NullPointerException(); } image1 = image; setPreferredSize(new Dimension( Math.max(getPreferredSize().width, image.getWidth()), Math.max(getPreferredSize().height, image.getHeight())) ); if (frame != null) { frame.pack(); } repaint(); } // loads image 1 from the given filename or URL public void setImage1(String filename) throws IOException { image1name = new File(filename).getName(); if (filename.startsWith("http")) { setImage1(ImageIO.read(new URL(filename))); } else { setImage1(ImageIO.read(new File(filename))); } } // called when "Set Image 2" menu item is clicked public void setImage2() { checkChooser(); if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) { return; } File selectedFile = chooser.getSelectedFile(); try { setImage2(selectedFile.toString()); countDiffPixels(); diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)"); image2Label.setText(selectedFile.getName()); frame.pack(); } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to set image 2:\n" + ex); } } // sets image 2 to be the given image public void setImage2(BufferedImage image) { if (image == null) { throw new NullPointerException(); } image2 = image; setPreferredSize(new Dimension( Math.max(getPreferredSize().width, image.getWidth()), Math.max(getPreferredSize().height, image.getHeight())) ); if (frame != null) { frame.pack(); } repaint(); } // loads image 2 from the given filename public void setImage2(String filename) throws IOException { if (filename.startsWith("http")) { setImage2(ImageIO.read(new URL(filename))); } else { setImage2(ImageIO.read(new File(filename))); } } private void setupComponents() { String title = "DiffImage"; if (image1name != null) { title = "Compare to " + image1name; } frame = new JFrame(title); frame.setResizable(false); // frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); slider = new JSlider(); slider.setPaintLabels(false); slider.setPaintTicks(true); slider.setSnapToTicks(true); slider.setMajorTickSpacing(25); slider.setMinorTickSpacing(5); box = new JCheckBox("Highlight diffs in color: ", highlightDiffs); colorButton = new JButton(); colorButton.setBackground(highlightColor); colorButton.setForeground(highlightColor); colorButton.setPreferredSize(new Dimension(24, 24)); diffPixelsLabel = new JLabel("(" + numDiffPixels + " pixels differ)"); diffPixelsLabel.setFont(diffPixelsLabel.getFont().deriveFont(Font.BOLD)); image1Label = new JLabel(label1Text); image2Label = new JLabel(label2Text); setupMenuBar(); } // initializes layout of components private void setupLayout() { JPanel southPanel1 = new JPanel(); southPanel1.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY)); southPanel1.add(image1Label); southPanel1.add(slider); southPanel1.add(image2Label); southPanel1.add(Box.createHorizontalStrut(20)); JPanel southPanel2 = new JPanel(); southPanel2.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY)); southPanel2.add(diffPixelsLabel); southPanel2.add(Box.createHorizontalStrut(20)); southPanel2.add(box); southPanel2.add(colorButton); Container southPanel = javax.swing.Box.createVerticalBox(); southPanel.add(southPanel1); southPanel.add(southPanel2); frame.add(this, BorderLayout.CENTER); frame.add(southPanel, BorderLayout.SOUTH); } // initializes main menu bar private void setupMenuBar() { saveAsItem = new JMenuItem("Save As...", 'A'); saveAsItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S")); setImage1Item = new JMenuItem("Set Image 1...", '1'); setImage1Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 1")); setImage2Item = new JMenuItem("Set Image 2...", '2'); setImage2Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 2")); JMenu file = new JMenu("File"); file.setMnemonic('F'); file.add(setImage1Item); file.add(setImage2Item); file.addSeparator(); file.add(saveAsItem); JMenuBar bar = new JMenuBar(); bar.add(file); // disabling menu bar to simplify code // frame.setJMenuBar(bar); } // method of ChangeListener interface public void stateChanged(ChangeEvent e) { opacity = slider.getValue(); repaint(); } // adds event listeners to various components private void setupEvents() { slider.addChangeListener(this); box.addActionListener(this); colorButton.addActionListener(this); saveAsItem.addActionListener(this); this.setImage1Item.addActionListener(this); this.setImage2Item.addActionListener(this); } } // inner class to represent one frame of an animated GIF private static class ImageFrame { public Image image; public int delay; public ImageFrame(Image image, int delay) { this.image = image; this.delay = delay / 10; // strangely, gif stores delay as sec/100 } } // inner class to do the actual drawing onto the DrawingPanel private class ImagePanel extends JPanel { private static final long serialVersionUID = 0; private Image image; // constructs the image panel public ImagePanel(Image image) { super(/* isDoubleBuffered */ true); setImage(image); setBackground(Color.WHITE); setPreferredSize(new Dimension(image.getWidth(this), image.getHeight(this))); setAlignmentX(0.0f); } // draws everything onto the panel public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; if (currentZoom != 1) { g2.scale(currentZoom, currentZoom); } g2.drawImage(image, 0, 0, this); // possibly draw grid lines for debugging if (gridLines) { g2.setPaint(GRID_LINE_COLOR); for (int row = 1; row <= getHeight() / gridLinesPxGap; row++) { g2.drawLine(0, row * gridLinesPxGap, getWidth(), row * gridLinesPxGap); } for (int col = 1; col <= getWidth() / gridLinesPxGap; col++) { g2.drawLine(col * gridLinesPxGap, 0, col * gridLinesPxGap, getHeight()); } } } public void setImage(Image image) { this.image = image; repaint(); } } // BEGIN GIF ENCODING CLASSES //****************************************************************************** // DirectGif89Frame.java //****************************************************************************** // ============================================================================== /** * Instances of this Gif89Frame subclass are constructed from RGB image * info, either in the form of an Image object or a pixel array. *

* There is an important restriction to note. It is only permissible to add * DirectGif89Frame objects to a Gif89Encoder constructed without an * explicit color map. The GIF color table will be automatically generated * from pixel information. * * @version 0.90 beta (15-Jul-2000) * @author J. M. G. Elliott (tep@jmge.net) * @see Gif89Encoder * @see Gif89Frame * @see IndexGif89Frame */ class DirectGif89Frame extends Gif89Frame { private int[] argbPixels; // ---------------------------------------------------------------------------- /** * Construct an DirectGif89Frame from a Java image. * * @param img * A java.awt.Image object that supports pixel-grabbing. * @exception IOException * If the image is unencodable due to failure of * pixel-grabbing. */ public DirectGif89Frame(Image img) throws IOException { PixelGrabber pg = new PixelGrabber(img, 0, 0, -1, -1, true); String errmsg = null; try { if (!pg.grabPixels()) errmsg = "can't grab pixels from image"; } catch (InterruptedException e) { errmsg = "interrupted grabbing pixels from image"; } if (errmsg != null) throw new IOException(errmsg + " (" + getClass().getName() + ")"); theWidth = pg.getWidth(); theHeight = pg.getHeight(); argbPixels = (int[]) pg.getPixels(); ciPixels = new byte[argbPixels.length]; // flush to conserve resources img.flush(); } // ---------------------------------------------------------------------------- /** * Construct an DirectGif89Frame from ARGB pixel data. * * @param width * Width of the bitmap. * @param height * Height of the bitmap. * @param argb_pixels * Array containing at least width*height pixels in the * format returned by java.awt.Color.getRGB(). */ public DirectGif89Frame(int width, int height, int argb_pixels[]) { theWidth = width; theHeight = height; argbPixels = new int[theWidth * theHeight]; System.arraycopy(argb_pixels, 0, argbPixels, 0, argbPixels.length); ciPixels = new byte[argbPixels.length]; } // ---------------------------------------------------------------------------- Object getPixelSource() { return argbPixels; } } // ****************************************************************************** // Gif89Encoder.java // ****************************************************************************** // ============================================================================== /** * This is the central class of a JDK 1.1 compatible GIF encoder that, * AFAIK, supports more features of the extended GIF spec than any other * Java open source encoder. Some sections of the source are lifted or * adapted from Jef Poskanzer's Acme GifEncoder (so please see * the readme containing his notice), but much * of it, including nearly all of the present class, is original code. My * main motivation for writing a new encoder was to support animated GIFs, * but the package also adds support for embedded textual comments. *

* There are still some limitations. For instance, animations are limited to * a single global color table. But that is usually what you want anyway, so * as to avoid irregularities on some displays. (So this is not really a * limitation, but a "disciplinary feature" :) Another rather more serious * restriction is that the total number of RGB colors in a given input-batch * mustn't exceed 256. Obviously, there is an opening here for someone who * would like to add a color-reducing preprocessor. *

* The encoder, though very usable in its present form, is at bottom only a * partial implementation skewed toward my own particular needs. Hence a * couple of caveats are in order. (1) During development it was in the back * of my mind that an encoder object should be reusable - i.e., you should * be able to make multiple calls to encode() on the same object, with or * without intervening frame additions or changes to options. But I haven't * reviewed the code with such usage in mind, much less tested it, so it's * likely I overlooked something. (2) The encoder classes aren't thread * safe, so use caution in a context where access is shared by multiple * threads. (Better yet, finish the library and re-release it :) *

* There follow a couple of simple examples illustrating the most common way * to use the encoder, i.e., to encode AWT Image objects created elsewhere * in the program. Use of some of the most popular format options is also * shown, though you will want to peruse the API for additional features. * *

* Animated GIF Example * *

     *  import net.jmge.gif.Gif89Encoder;
     *  // ...
     *  void writeAnimatedGIF(Image[] still_images,
     *                      String annotation,
     *                      boolean looped,
     *                      double frames_per_second,
     *                      OutputStream out) throws IOException
     *  {
     *  Gif89Encoder gifenc = new Gif89Encoder();
     *  for (int i = 0; i < still_images.length; ++i)
     *    gifenc.addFrame(still_images[i]);
     *  gifenc.setComments(annotation);
     *  gifenc.setLoopCount(looped ? 0 : 1);
     *  gifenc.setUniformDelay((int) Math.round(100 / frames_per_second));
     *  gifenc.encode(out);
     *  }
     * 
* * Static GIF Example * *
     *  import net.jmge.gif.Gif89Encoder;
     *  // ...
     *  void writeNormalGIF(Image img,
     *                    String annotation,
     *                    int transparent_index,  // pass -1 for none
     *                    boolean interlaced,
     *                    OutputStream out) throws IOException
     *  {
     *  Gif89Encoder gifenc = new Gif89Encoder(img);
     *  gifenc.setComments(annotation);
     *  gifenc.setTransparentIndex(transparent_index);
     *  gifenc.getFrameAt(0).setInterlaced(interlaced);
     *  gifenc.encode(out);
     *  }
     * 
* * @version 0.90 beta (15-Jul-2000) * @author J. M. G. Elliott (tep@jmge.net) * @see Gif89Frame * @see DirectGif89Frame * @see IndexGif89Frame */ class Gif89Encoder { private static final boolean DEBUG = false; private Dimension dispDim = new Dimension(0, 0); private GifColorTable colorTable; private int bgIndex = 0; private int loopCount = 1; private String theComments; private Vector vFrames = new Vector(); // ---------------------------------------------------------------------------- /** * Use this default constructor if you'll be adding multiple frames * constructed from RGB data (i.e., AWT Image objects or ARGB-pixel * arrays). */ public Gif89Encoder() { // empty color table puts us into "palette autodetect" mode colorTable = new GifColorTable(); } // ---------------------------------------------------------------------------- /** * Like the default except that it also adds a single frame, for * conveniently encoding a static GIF from an image. * * @param static_image * Any Image object that supports pixel-grabbing. * @exception IOException * See the addFrame() methods. */ public Gif89Encoder(Image static_image) throws IOException { this(); addFrame(static_image); } // ---------------------------------------------------------------------------- /** * This constructor installs a user color table, overriding the * detection of of a palette from ARBG pixels. * * Use of this constructor imposes a couple of restrictions: (1) Frame * objects can't be of type DirectGif89Frame (2) Transparency, if * desired, must be set explicitly. * * @param colors * Array of color values; no more than 256 colors will be * read, since that's the limit for a GIF. */ public Gif89Encoder(Color[] colors) { colorTable = new GifColorTable(colors); } // ---------------------------------------------------------------------------- /** * Convenience constructor for encoding a static GIF from index-model * data. Adds a single frame as specified. * * @param colors * Array of color values; no more than 256 colors will be * read, since that's the limit for a GIF. * @param width * Width of the GIF bitmap. * @param height * Height of same. * @param ci_pixels * Array of color-index pixels no less than width * height in * length. * @exception IOException * See the addFrame() methods. */ public Gif89Encoder(Color[] colors, int width, int height, byte ci_pixels[]) throws IOException { this(colors); addFrame(width, height, ci_pixels); } // ---------------------------------------------------------------------------- /** * Get the number of frames that have been added so far. * * @return Number of frame items. */ public int getFrameCount() { return vFrames.size(); } // ---------------------------------------------------------------------------- /** * Get a reference back to a Gif89Frame object by position. * * @param index * Zero-based index of the frame in the sequence. * @return Gif89Frame object at the specified position (or null if no * such frame). */ public Gif89Frame getFrameAt(int index) { return isOk(index) ? vFrames.elementAt(index) : null; } // ---------------------------------------------------------------------------- /** * Add a Gif89Frame frame to the end of the internal sequence. Note that * there are restrictions on the Gif89Frame type: if the encoder object * was constructed with an explicit color table, an attempt to add a * DirectGif89Frame will throw an exception. * * @param gf * An externally constructed Gif89Frame. * @exception IOException * If Gif89Frame can't be accommodated. This could happen * if either (1) the aggregate cross-frame RGB color * count exceeds 256, or (2) the Gif89Frame subclass is * incompatible with the present encoder object. */ public void addFrame(Gif89Frame gf) throws IOException { accommodateFrame(gf); vFrames.addElement(gf); } // ---------------------------------------------------------------------------- /** * Convenience version of addFrame() that takes a Java Image, internally * constructing the requisite DirectGif89Frame. * * @param image * Any Image object that supports pixel-grabbing. * @exception IOException * If either (1) pixel-grabbing fails, (2) the aggregate * cross-frame RGB color count exceeds 256, or (3) this * encoder object was constructed with an explicit color * table. */ public void addFrame(Image image) throws IOException { DirectGif89Frame frame = new DirectGif89Frame(image); addFrame(frame); } // ---------------------------------------------------------------------------- /** * The index-model convenience version of addFrame(). * * @param width * Width of the GIF bitmap. * @param height * Height of same. * @param ci_pixels * Array of color-index pixels no less than width * height in * length. * @exception IOException * Actually, in the present implementation, there aren't * any unchecked exceptions that can be thrown when * adding an IndexGif89Frame per se. But I might * add some pedantic check later, to justify the * generality :) */ public void addFrame(int width, int height, byte ci_pixels[]) throws IOException { addFrame(new IndexGif89Frame(width, height, ci_pixels)); } // ---------------------------------------------------------------------------- /** * Like addFrame() except that the frame is inserted at a specific point * in the sequence rather than appended. * * @param index * Zero-based index at which to insert frame. * @param gf * An externally constructed Gif89Frame. * @exception IOException * If Gif89Frame can't be accommodated. This could happen * if either (1) the aggregate cross-frame RGB color * count exceeds 256, or (2) the Gif89Frame subclass is * incompatible with the present encoder object. */ public void insertFrame(int index, Gif89Frame gf) throws IOException { accommodateFrame(gf); vFrames.insertElementAt(gf, index); } // ---------------------------------------------------------------------------- /** * Set the color table index for the transparent color, if any. * * @param index * Index of the color that should be rendered as transparent, * if any. A value of -1 turns off transparency. (Default: * -1) */ public void setTransparentIndex(int index) { colorTable.setTransparent(index); } // ---------------------------------------------------------------------------- /** * Sets attributes of the multi-image display area, if applicable. * * @param dim * Width/height of display. (Default: largest detected frame * size) * @param background * Color table index of background color. (Default: 0) * @see Gif89Frame#setPosition */ public void setLogicalDisplay(Dimension dim, int background) { dispDim = new Dimension(dim); bgIndex = background; } // ---------------------------------------------------------------------------- /** * Set animation looping parameter, if applicable. * * @param count * Number of times to play sequence. Special value of 0 * specifies indefinite looping. (Default: 1) */ public void setLoopCount(int count) { loopCount = count; } // ---------------------------------------------------------------------------- /** * Specify some textual comments to be embedded in GIF. * * @param comments * String containing ASCII comments. */ public void setComments(String comments) { theComments = comments; } // ---------------------------------------------------------------------------- /** * A convenience method for setting the "animation speed". It simply * sets the delay parameter for each frame in the sequence to the * supplied value. Since this is actually frame-level rather than * animation-level data, take care to add your frames before calling * this method. * * @param interval * Interframe interval in centiseconds. */ public void setUniformDelay(int interval) { for (int i = 0; i < vFrames.size(); ++i) vFrames.elementAt(i).setDelay(interval); } // ---------------------------------------------------------------------------- /** * After adding your frame(s) and setting your options, simply call this * method to write the GIF to the passed stream. Multiple calls are * permissible if for some reason that is useful to your application. * (The method simply encodes the current state of the object with no * thought to previous calls.) * * @param out * The stream you want the GIF written to. * @exception IOException * If a write error is encountered. */ public void encode(OutputStream out) throws IOException { int nframes = getFrameCount(); boolean is_sequence = nframes > 1; // N.B. must be called before writing screen descriptor colorTable.closePixelProcessing(); // write GIF HEADER putAscii("GIF89a", out); // write global blocks writeLogicalScreenDescriptor(out); colorTable.encode(out); if (is_sequence && loopCount != 1) writeNetscapeExtension(out); if (theComments != null && theComments.length() > 0) writeCommentExtension(out); // write out the control and rendering data for each frame for (int i = 0; i < nframes; ++i) { DirectGif89Frame frame = (DirectGif89Frame) vFrames .elementAt(i); frame.encode(out, is_sequence, colorTable.getDepth(), colorTable.getTransparent()); vFrames.set(i, null); // for GC's sake System.gc(); } // write GIF TRAILER out.write((int) ';'); out.flush(); } public boolean hasStarted = false; // ---------------------------------------------------------------------------- /** * After adding your frame(s) and setting your options, simply call this * method to write the GIF to the passed stream. Multiple calls are * permissible if for some reason that is useful to your application. * (The method simply encodes the current state of the object with no * thought to previous calls.) * * @param out * The stream you want the GIF written to. * @exception IOException * If a write error is encountered. */ public void startEncoding(OutputStream out, Image image, int delay) throws IOException { hasStarted = true; boolean is_sequence = true; Gif89Frame gf = new DirectGif89Frame(image); accommodateFrame(gf); // N.B. must be called before writing screen descriptor colorTable.closePixelProcessing(); // write GIF HEADER putAscii("GIF89a", out); // write global blocks writeLogicalScreenDescriptor(out); colorTable.encode(out); if (is_sequence && loopCount != 1) writeNetscapeExtension(out); if (theComments != null && theComments.length() > 0) writeCommentExtension(out); } public void continueEncoding(OutputStream out, Image image, int delay) throws IOException { // write out the control and rendering data for each frame Gif89Frame gf = new DirectGif89Frame(image); accommodateFrame(gf); gf.encode(out, true, colorTable.getDepth(), colorTable.getTransparent()); out.flush(); image.flush(); } public void endEncoding(OutputStream out) throws IOException { // write GIF TRAILER out.write((int) ';'); out.flush(); } public void setBackground(Color color) { bgIndex = colorTable.indexOf(color); if (bgIndex < 0) { try { BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED); Graphics g = img.getGraphics(); g.setColor(color); g.fillRect(0, 0, 2, 2); DirectGif89Frame frame = new DirectGif89Frame(img); accommodateFrame(frame); bgIndex = colorTable.indexOf(color); } catch (IOException e) { if (DEBUG) System.out .println("Error while setting background color: " + e); } } if (DEBUG) System.out.println("Setting bg index to " + bgIndex); } // ---------------------------------------------------------------------------- private void accommodateFrame(Gif89Frame gf) throws IOException { dispDim.width = Math.max(dispDim.width, gf.getWidth()); dispDim.height = Math.max(dispDim.height, gf.getHeight()); colorTable.processPixels(gf); } // ---------------------------------------------------------------------------- private void writeLogicalScreenDescriptor(OutputStream os) throws IOException { putShort(dispDim.width, os); putShort(dispDim.height, os); // write 4 fields, packed into a byte (bitfieldsize:value) // global color map present? (1:1) // bits per primary color less 1 (3:7) // sorted color table? (1:0) // bits per pixel less 1 (3:varies) os.write(0xf0 | colorTable.getDepth() - 1); // write background color index os.write(bgIndex); // Jef Poskanzer's notes on the next field, for our possible // edification: // Pixel aspect ratio - 1:1. // Putbyte( (byte) 49, outs ); // Java's GIF reader currently has a bug, if the aspect ratio byte // is // not zero it throws an ImageFormatException. It doesn't know that // 49 means a 1:1 aspect ratio. Well, whatever, zero works with all // the other decoders I've tried so it probably doesn't hurt. // OK, if it's good enough for Jef, it's definitely good enough for // us: os.write(0); } // ---------------------------------------------------------------------------- private void writeNetscapeExtension(OutputStream os) throws IOException { // n.b. most software seems to interpret the count as a repeat count // (i.e., interations beyond 1) rather than as an iteration count // (thus, to avoid repeating we have to omit the whole extension) os.write((int) '!'); // GIF Extension Introducer os.write(0xff); // Application Extension Label os.write(11); // application ID block size putAscii("NETSCAPE2.0", os); // application ID data os.write(3); // data sub-block size os.write(1); // a looping flag? dunno // we finally write the relevent data putShort(loopCount > 1 ? loopCount - 1 : 0, os); os.write(0); // block terminator } // ---------------------------------------------------------------------------- private void writeCommentExtension(OutputStream os) throws IOException { os.write((int) '!'); // GIF Extension Introducer os.write(0xfe); // Comment Extension Label int remainder = theComments.length() % 255; int nsubblocks_full = theComments.length() / 255; int nsubblocks = nsubblocks_full + (remainder > 0 ? 1 : 0); int ibyte = 0; for (int isb = 0; isb < nsubblocks; ++isb) { int size = isb < nsubblocks_full ? 255 : remainder; os.write(size); putAscii(theComments.substring(ibyte, ibyte + size), os); ibyte += size; } os.write(0); // block terminator } // ---------------------------------------------------------------------------- private boolean isOk(int frame_index) { return frame_index >= 0 && frame_index < vFrames.size(); } } // ============================================================================== class GifColorTable { // the palette of ARGB colors, packed as returned by Color.getRGB() private int[] theColors = new int[256]; // other basic attributes private int colorDepth; private int transparentIndex = -1; // these fields track color-index info across frames private int ciCount = 0; // count of distinct color indices private ReverseColorMap ciLookup; // cumulative rgb-to-ci lookup table // ---------------------------------------------------------------------------- GifColorTable() { ciLookup = new ReverseColorMap(); // puts us into "auto-detect mode" } // ---------------------------------------------------------------------------- GifColorTable(Color[] colors) { int n2copy = Math.min(theColors.length, colors.length); for (int i = 0; i < n2copy; ++i) theColors[i] = colors[i].getRGB(); } int indexOf(Color color) { int rgb = color.getRGB(); for (int i = 0; i < theColors.length; i++) { if (rgb == theColors[i]) { return i; } } return -1; } // ---------------------------------------------------------------------------- int getDepth() { return colorDepth; } // ---------------------------------------------------------------------------- int getTransparent() { return transparentIndex; } // ---------------------------------------------------------------------------- // default: -1 (no transparency) void setTransparent(int color_index) { transparentIndex = color_index; } // ---------------------------------------------------------------------------- void processPixels(Gif89Frame gf) throws IOException { if (gf instanceof DirectGif89Frame) filterPixels((DirectGif89Frame) gf); else trackPixelUsage((IndexGif89Frame) gf); } // ---------------------------------------------------------------------------- void closePixelProcessing() // must be called before encode() { colorDepth = computeColorDepth(ciCount); } // ---------------------------------------------------------------------------- void encode(OutputStream os) throws IOException { // size of palette written is the smallest power of 2 that can // accomdate // the number of RGB colors detected (or largest color index, in // case of // index pixels) int palette_size = 1 << colorDepth; for (int i = 0; i < palette_size; ++i) { os.write(theColors[i] >> 16 & 0xff); os.write(theColors[i] >> 8 & 0xff); os.write(theColors[i] & 0xff); } } // ---------------------------------------------------------------------------- // This method accomplishes three things: // (1) converts the passed rgb pixels to indexes into our rgb lookup // table // (2) fills the rgb table as new colors are encountered // (3) looks for transparent pixels so as to set the transparent index // The information is cumulative across multiple calls. // // (Note: some of the logic is borrowed from Jef Poskanzer's code.) // ---------------------------------------------------------------------------- private void filterPixels(DirectGif89Frame dgf) throws IOException { if (ciLookup == null) throw new IOException( "RGB frames require palette autodetection"); int[] argb_pixels = (int[]) dgf.getPixelSource(); byte[] ci_pixels = dgf.getPixelSink(); int npixels = argb_pixels.length; for (int i = 0; i < npixels; ++i) { int argb = argb_pixels[i]; // handle transparency if ((argb >>> 24) < 0x80) // transparent pixel? if (transparentIndex == -1) // first transparent color // encountered? transparentIndex = ciCount; // record its index else if (argb != theColors[transparentIndex]) // different // pixel // value? { // collapse all transparent pixels into one color index ci_pixels[i] = (byte) transparentIndex; continue; // CONTINUE - index already in table } // try to look up the index in our "reverse" color table int color_index = ciLookup.getPaletteIndex(argb & 0xffffff); if (color_index == -1) // if it isn't in there yet { if (ciCount == 256) throw new IOException( "can't encode as GIF (> 256 colors)"); // store color in our accumulating palette theColors[ciCount] = argb; // store index in reverse color table ciLookup.put(argb & 0xffffff, ciCount); // send color index to our output array ci_pixels[i] = (byte) ciCount; // increment count of distinct color indices ++ciCount; } else // we've already snagged color into our palette ci_pixels[i] = (byte) color_index; // just send filtered // pixel } } // ---------------------------------------------------------------------------- private void trackPixelUsage(IndexGif89Frame igf) throws IOException { byte[] ci_pixels = (byte[]) igf.getPixelSource(); int npixels = ci_pixels.length; for (int i = 0; i < npixels; ++i) if (ci_pixels[i] >= ciCount) ciCount = ci_pixels[i] + 1; } // ---------------------------------------------------------------------------- private int computeColorDepth(int colorcount) { // color depth = log-base-2 of maximum number of simultaneous // colors, i.e. // bits per color-index pixel if (colorcount <= 2) return 1; if (colorcount <= 4) return 2; if (colorcount <= 16) return 4; return 8; } } // ============================================================================== // We're doing a very simple linear hashing thing here, which seems // sufficient // for our needs. I make no claims for this approach other than that it // seems // an improvement over doing a brute linear search for each pixel on the one // hand, and creating a Java object for each pixel (if we were to use a Java // Hashtable) on the other. Doubtless my little hash could be improved by // tuning the capacity (at the very least). Suggestions are welcome. // ============================================================================== class ReverseColorMap { private class ColorRecord { int rgb; int ipalette; ColorRecord(int rgb, int ipalette) { this.rgb = rgb; this.ipalette = ipalette; } } // I wouldn't really know what a good hashing capacity is, having missed // out // on data structures and algorithms class :) Alls I know is, we've got // a lot // more space than we have time. So let's try a sparse table with a // maximum // load of about 1/8 capacity. private static final int HCAPACITY = 2053; // a nice prime number // our hash table proper private ColorRecord[] hTable = new ColorRecord[HCAPACITY]; // ---------------------------------------------------------------------------- // Assert: rgb is not negative (which is the same as saying, be sure the // alpha transparency byte - i.e., the high byte - has been masked out). // ---------------------------------------------------------------------------- int getPaletteIndex(int rgb) { ColorRecord rec; for (int itable = rgb % hTable.length; (rec = hTable[itable]) != null && rec.rgb != rgb; itable = ++itable % hTable.length) ; if (rec != null) return rec.ipalette; return -1; } // ---------------------------------------------------------------------------- // Assert: (1) same as above; (2) rgb key not already present // ---------------------------------------------------------------------------- void put(int rgb, int ipalette) { int itable; for (itable = rgb % hTable.length; hTable[itable] != null; itable = ++itable % hTable.length) ; hTable[itable] = new ColorRecord(rgb, ipalette); } } // ****************************************************************************** // Gif89Frame.java // ****************************************************************************** // ============================================================================== /** * First off, just to dispel any doubt, this class and its subclasses have * nothing to do with GUI "frames" such as java.awt.Frame. We merely use the * term in its very common sense of a still picture in an animation * sequence. It's hoped that the restricted context will prevent any * confusion. *

* An instance of this class is used in conjunction with a Gif89Encoder * object to represent and encode a single static image and its associated * "control" data. A Gif89Frame doesn't know or care whether it is encoding * one of the many animation frames in a GIF movie, or the single bitmap in * a "normal" GIF. (FYI, this design mirrors the encoded GIF structure.) *

* Since Gif89Frame is an abstract class we don't instantiate it directly, * but instead create instances of its concrete subclasses, IndexGif89Frame * and DirectGif89Frame. From the API standpoint, these subclasses differ * only in the sort of data their instances are constructed from. Most folks * will probably work with DirectGif89Frame, since it can be constructed * from a java.awt.Image object, but the lower-level IndexGif89Frame class * offers advantages in specialized circumstances. (Of course, in routine * situations you might not explicitly instantiate any frames at all, * instead letting Gif89Encoder's convenience methods do the honors.) *

* As far as the public API is concerned, objects in the Gif89Frame * hierarchy interact with a Gif89Encoder only via the latter's methods for * adding and querying frames. (As a side note, you should know that while * Gif89Encoder objects are permanently modified by the addition of * Gif89Frames, the reverse is NOT true. That is, even though the ultimate * encoding of a Gif89Frame may be affected by the context its parent * encoder object provides, it retains its original condition and can be * reused in a different context.) *

* The core pixel-encoding code in this class was essentially lifted from * Jef Poskanzer's well-known Acme GifEncoder, so please see * the readme containing his notice. * * @version 0.90 beta (15-Jul-2000) * @author J. M. G. Elliott (tep@jmge.net) * @see Gif89Encoder * @see DirectGif89Frame * @see IndexGif89Frame */ abstract class Gif89Frame { // // Public "Disposal Mode" constants //// /** * The animated GIF renderer shall decide how to dispose of this * Gif89Frame's display area. * * @see Gif89Frame#setDisposalMode */ public static final int DM_UNDEFINED = 0; /** * The animated GIF renderer shall take no display-disposal action. * * @see Gif89Frame#setDisposalMode */ public static final int DM_LEAVE = 1; /** * The animated GIF renderer shall replace this Gif89Frame's area with * the background color. * * @see Gif89Frame#setDisposalMode */ public static final int DM_BGCOLOR = 2; /** * The animated GIF renderer shall replace this Gif89Frame's area with * the previous frame's bitmap. * * @see Gif89Frame#setDisposalMode */ public static final int DM_REVERT = 3; // // Bitmap variables set in package subclass constructors //// int theWidth = -1; int theHeight = -1; byte[] ciPixels; // // GIF graphic frame control options //// private Point thePosition = new Point(0, 0); private boolean isInterlaced; private int csecsDelay; private int disposalCode = DM_LEAVE; // ---------------------------------------------------------------------------- /** * Set the position of this frame within a larger animation display * space. * * @param p * Coordinates of the frame's upper left corner in the * display space. (Default: The logical display's origin [0, * 0]) * @see Gif89Encoder#setLogicalDisplay */ public void setPosition(Point p) { thePosition = new Point(p); } // ---------------------------------------------------------------------------- /** * Set or clear the interlace flag. * * @param b * true if you want interlacing. (Default: false) */ public void setInterlaced(boolean b) { isInterlaced = b; } // ---------------------------------------------------------------------------- /** * Set the between-frame interval. * * @param interval * Centiseconds to wait before displaying the subsequent * frame. (Default: 0) */ public void setDelay(int interval) { csecsDelay = interval; } // ---------------------------------------------------------------------------- /** * Setting this option determines (in a cooperative GIF-viewer) what * will be done with this frame's display area before the subsequent * frame is displayed. For instance, a setting of DM_BGCOLOR can be used * for erasure when redrawing with displacement. * * @param code * One of the four int constants of the Gif89Frame.DM_* * series. (Default: DM_LEAVE) */ public void setDisposalMode(int code) { disposalCode = code; } // ---------------------------------------------------------------------------- Gif89Frame() { } // package-visible default constructor // ---------------------------------------------------------------------------- abstract Object getPixelSource(); // ---------------------------------------------------------------------------- int getWidth() { return theWidth; } // ---------------------------------------------------------------------------- int getHeight() { return theHeight; } // ---------------------------------------------------------------------------- byte[] getPixelSink() { return ciPixels; } // ---------------------------------------------------------------------------- void encode(OutputStream os, boolean epluribus, int color_depth, int transparent_index) throws IOException { writeGraphicControlExtension(os, epluribus, transparent_index); writeImageDescriptor(os); new GifPixelsEncoder(theWidth, theHeight, ciPixels, isInterlaced, color_depth).encode(os); } // ---------------------------------------------------------------------------- private void writeGraphicControlExtension(OutputStream os, boolean epluribus, int itransparent) throws IOException { int transflag = itransparent == -1 ? 0 : 1; if (transflag == 1 || epluribus) // using transparency or animating // ? { os.write((int) '!'); // GIF Extension Introducer os.write(0xf9); // Graphic Control Label os.write(4); // subsequent data block size os.write((disposalCode << 2) | transflag); // packed fields (1 // byte) putShort(csecsDelay, os); // delay field (2 bytes) os.write(itransparent); // transparent index field os.write(0); // block terminator } } // ---------------------------------------------------------------------------- private void writeImageDescriptor(OutputStream os) throws IOException { os.write((int) ','); // Image Separator putShort(thePosition.x, os); putShort(thePosition.y, os); putShort(theWidth, os); putShort(theHeight, os); os.write(isInterlaced ? 0x40 : 0); // packed fields (1 byte) } } // ============================================================================== class GifPixelsEncoder { private static final int EOF = -1; private int imgW, imgH; private byte[] pixAry; private boolean wantInterlaced; private int initCodeSize; // raster data navigators private int countDown; private int xCur, yCur; private int curPass; // ---------------------------------------------------------------------------- GifPixelsEncoder(int width, int height, byte[] pixels, boolean interlaced, int color_depth) { imgW = width; imgH = height; pixAry = pixels; wantInterlaced = interlaced; initCodeSize = Math.max(2, color_depth); } // ---------------------------------------------------------------------------- void encode(OutputStream os) throws IOException { os.write(initCodeSize); // write "initial code size" byte countDown = imgW * imgH; // reset navigation variables xCur = yCur = curPass = 0; compress(initCodeSize + 1, os); // compress and write the pixel data os.write(0); // write block terminator } // **************************************************************************** // (J.E.) The logic of the next two methods is largely intact from // Jef Poskanzer. Some stylistic changes were made for consistency sake, // plus the second method accesses the pixel value from a prefiltered // linear // array. That's about it. // **************************************************************************** // ---------------------------------------------------------------------------- // Bump the 'xCur' and 'yCur' to point to the next pixel. // ---------------------------------------------------------------------------- private void bumpPosition() { // Bump the current X position ++xCur; // If we are at the end of a scan line, set xCur back to the // beginning // If we are interlaced, bump the yCur to the appropriate spot, // otherwise, just increment it. if (xCur == imgW) { xCur = 0; if (!wantInterlaced) ++yCur; else switch (curPass) { case 0: yCur += 8; if (yCur >= imgH) { ++curPass; yCur = 4; } break; case 1: yCur += 8; if (yCur >= imgH) { ++curPass; yCur = 2; } break; case 2: yCur += 4; if (yCur >= imgH) { ++curPass; yCur = 1; } break; case 3: yCur += 2; break; } } } // ---------------------------------------------------------------------------- // Return the next pixel from the image // ---------------------------------------------------------------------------- private int nextPixel() { if (countDown == 0) return EOF; --countDown; byte pix = pixAry[yCur * imgW + xCur]; bumpPosition(); return pix & 0xff; } // **************************************************************************** // (J.E.) I didn't touch Jef Poskanzer's code from this point on. (Well, // OK, // I changed the name of the sole outside method it accesses.) I figure // if I have no idea how something works, I shouldn't play with it :) // // Despite its unencapsulated structure, this section is actually highly // self-contained. The calling code merely calls compress(), and the // present // code calls nextPixel() in the caller. That's the sum total of their // communication. I could have dumped it in a separate class with a // callback // via an interface, but it didn't seem worth messing with. // **************************************************************************** // GIFCOMPR.C - GIF Image compression routines // // Lempel-Ziv compression based on 'compress'. GIF modifications by // David Rowley (mgardi@watdcsu.waterloo.edu) // General DEFINEs static final int BITS = 12; static final int HSIZE = 5003; // 80% occupancy // GIF Image compression - modified 'compress' // // Based on: compress.c - File compression ala IEEE Computer, June 1984. // // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) // Jim McKie (decvax!mcvax!jim) // Steve Davies (decvax!vax135!petsd!peora!srd) // Ken Turkowski (decvax!decwrl!turtlevax!ken) // James A. Woods (decvax!ihnp4!ames!jaw) // Joe Orost (decvax!vax135!petsd!joe) int n_bits; // number of bits/code int maxbits = BITS; // user settable max # bits/code int maxcode; // maximum code, given n_bits int maxmaxcode = 1 << BITS; // should NEVER generate this code final int MAXCODE(int n_bits) { return (1 << n_bits) - 1; } int[] htab = new int[HSIZE]; int[] codetab = new int[HSIZE]; int hsize = HSIZE; // for dynamic table sizing int free_ent = 0; // first unused entry // block compression parameters -- after all codes are used up, // and compression rate changes, start over. boolean clear_flg = false; // Algorithm: use open addressing double hashing (no chaining) on the // prefix code / next character combination. We do a variant of Knuth's // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime // secondary probe. Here, the modular division first probe is gives way // to a faster exclusive-or manipulation. Also do block compression with // an adaptive reset, whereby the code table is cleared when the // compression // ratio decreases, but after the table fills. The variable-length // output // codes are re-sized at this point, and a special CLEAR code is // generated // for the decompressor. Late addition: construct the table according to // file size for noticeable speed improvement on small files. Please // direct // questions about this implementation to ames!jaw. int g_init_bits; int ClearCode; int EOFCode; void compress(int init_bits, OutputStream outs) throws IOException { int fcode; int i /* = 0 */; int c; int ent; int disp; int hsize_reg; int hshift; // Set up the globals: g_init_bits - initial number of bits g_init_bits = init_bits; // Set up the necessary values clear_flg = false; n_bits = g_init_bits; maxcode = MAXCODE(n_bits); ClearCode = 1 << (init_bits - 1); EOFCode = ClearCode + 1; free_ent = ClearCode + 2; char_init(); ent = nextPixel(); hshift = 0; for (fcode = hsize; fcode < 65536; fcode *= 2) ++hshift; hshift = 8 - hshift; // set hash code range bound hsize_reg = hsize; cl_hash(hsize_reg); // clear hash table output(ClearCode, outs); outer_loop: while ((c = nextPixel()) != EOF) { fcode = (c << maxbits) + ent; i = (c << hshift) ^ ent; // xor hashing if (htab[i] == fcode) { ent = codetab[i]; continue; } else if (htab[i] >= 0) // non-empty slot { disp = hsize_reg - i; // secondary hash (after G. Knott) if (i == 0) disp = 1; do { if ((i -= disp) < 0) i += hsize_reg; if (htab[i] == fcode) { ent = codetab[i]; continue outer_loop; } } while (htab[i] >= 0); } output(ent, outs); ent = c; if (free_ent < maxmaxcode) { codetab[i] = free_ent++; // code -> hashtable htab[i] = fcode; } else cl_block(outs); } // Put out the final code. output(ent, outs); output(EOFCode, outs); } // output // // Output the given code. // Inputs: // code: A n_bits-bit integer. If == -1, then EOF. This assumes // that n_bits =< wordsize - 1. // Outputs: // Outputs code to the file. // Assumptions: // Chars are 8 bits long. // Algorithm: // Maintain a BITS character long buffer (so that 8 codes will // fit in it exactly). Use the VAX insv instruction to insert each // code in turn. When the buffer fills up empty it and start over. int cur_accum = 0; int cur_bits = 0; int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF }; void output(int code, OutputStream outs) throws IOException { cur_accum &= masks[cur_bits]; if (cur_bits > 0) cur_accum |= (code << cur_bits); else cur_accum = code; cur_bits += n_bits; while (cur_bits >= 8) { char_out((byte) (cur_accum & 0xff), outs); cur_accum >>= 8; cur_bits -= 8; } // If the next entry is going to be too big for the code size, // then increase it, if possible. if (free_ent > maxcode || clear_flg) { if (clear_flg) { maxcode = MAXCODE(n_bits = g_init_bits); clear_flg = false; } else { ++n_bits; if (n_bits == maxbits) maxcode = maxmaxcode; else maxcode = MAXCODE(n_bits); } } if (code == EOFCode) { // At EOF, write the rest of the buffer. while (cur_bits > 0) { char_out((byte) (cur_accum & 0xff), outs); cur_accum >>= 8; cur_bits -= 8; } flush_char(outs); } } // Clear out the hash table // table clear for block compress void cl_block(OutputStream outs) throws IOException { cl_hash(hsize); free_ent = ClearCode + 2; clear_flg = true; output(ClearCode, outs); } // reset code table void cl_hash(int hsize) { for (int i = 0; i < hsize; ++i) htab[i] = -1; } // GIF Specific routines // Number of characters so far in this 'packet' int a_count; // Set up the 'byte output' routine void char_init() { a_count = 0; } // Define the storage for the packet accumulator byte[] accum = new byte[256]; // Add a character to the end of the current packet, and if it is 254 // characters, flush the packet to disk. void char_out(byte c, OutputStream outs) throws IOException { accum[a_count++] = c; if (a_count >= 254) flush_char(outs); } // Flush the packet to disk, and reset the accumulator void flush_char(OutputStream outs) throws IOException { if (a_count > 0) { outs.write(a_count); outs.write(accum, 0, a_count); a_count = 0; } } } // ****************************************************************************** // IndexGif89Frame.java // ****************************************************************************** // ============================================================================== /** * Instances of this Gif89Frame subclass are constructed from bitmaps in the * form of color-index pixels, which accords with a GIF's native palettized * color model. The class is useful when complete control over a GIF's color * palette is desired. It is also much more efficient when one is using an * algorithmic frame generator that isn't interested in RGB values (such as * a cellular automaton). *

* Objects of this class are normally added to a Gif89Encoder object that * has been provided with an explicit color table at construction. While you * may also add them to "auto-map" encoders without an exception being * thrown, there obviously must be at least one DirectGif89Frame object in * the sequence so that a color table may be detected. * * @version 0.90 beta (15-Jul-2000) * @author J. M. G. Elliott (tep@jmge.net) * @see Gif89Encoder * @see Gif89Frame * @see DirectGif89Frame */ class IndexGif89Frame extends Gif89Frame { // ---------------------------------------------------------------------------- /** * Construct a IndexGif89Frame from color-index pixel data. * * @param width * Width of the bitmap. * @param height * Height of the bitmap. * @param ci_pixels * Array containing at least width*height color-index pixels. */ public IndexGif89Frame(int width, int height, byte ci_pixels[]) { theWidth = width; theHeight = height; ciPixels = new byte[theWidth * theHeight]; System.arraycopy(ci_pixels, 0, ciPixels, 0, ciPixels.length); } // ---------------------------------------------------------------------------- Object getPixelSource() { return ciPixels; } } // ---------------------------------------------------------------------------- /** * Internal method; * write just the low bytes of a String. (This sucks, but the concept of an * encoding seems inapplicable to a binary file ID string. I would think * flexibility is just what we don't want - but then again, maybe I'm slow.) * This is an internal method not meant to be called by clients. */ private static void putAscii(String s, OutputStream os) throws IOException { byte[] bytes = new byte[s.length()]; for (int i = 0; i < bytes.length; ++i) { bytes[i] = (byte) s.charAt(i); // discard the high byte } os.write(bytes); } // ---------------------------------------------------------------------------- /** * Internal method; * write a 16-bit integer in little endian byte order. * This is an internal method not meant to be called by clients. */ private static void putShort(int i16, OutputStream os) throws IOException { os.write(i16 & 0xff); os.write(i16 >> 8 & 0xff); } }