JAVA   26

DrawingPanel.java

Guest on 12th January 2022 05:36:27 PM

  1. /*
  2.  * =====================================================================
  3.  * DrawingPanel.java
  4.  * Simplified Java drawing window class
  5.  * to accompany Building Java Programs textbook and associated materials
  6.  *
  7.  * authors: Marty Stepp, Stanford University
  8.  *          Stuart Reges, University of Washington
  9.  * version: 4.04 (BJP 4th edition)
  10.  * (make sure to also update version string in Javadoc header below!)
  11.  * =====================================================================
  12.  *
  13.  * COMPATIBILITY NOTE: This version of DrawingPanel requires Java 8 or higher.
  14.  * If you need a version that works on Java 7 or lower, please see our
  15.  * web site at http://www.buildingjavaprograms.com/ .
  16.  * To make this file work on Java 7 and lower, you must make two small
  17.  * modifications to its source code.
  18.  * Search for the two occurrences of the annotation @FunctionalInterface
  19.  * and comment them out or remove those lines.
  20.  * Then the file should compile and run properly on older versions of Java.
  21.  *
  22.  * =====================================================================
  23.  *
  24.  * The DrawingPanel class provides a simple interface for drawing persistent
  25.  * images using a Graphics object.  An internal BufferedImage object is used
  26.  * to keep track of what has been drawn.  A client of the class simply
  27.  * constructs a DrawingPanel of a particular size and then draws on it with
  28.  * the Graphics object, setting the background color if they so choose.
  29.  * See JavaDoc comments below for more information.
  30.  */
  31.  
  32. import java.awt.FontMetrics;
  33. import java.awt.Rectangle;
  34. import java.awt.Shape;
  35. import java.awt.image.ImageObserver;
  36. import java.text.AttributedCharacterIterator;
  37. import java.util.Collections;
  38. import java.awt.AlphaComposite;
  39. import java.awt.BorderLayout;
  40. import java.awt.Color;
  41. import java.awt.Composite;
  42. import java.awt.Container;
  43. import java.awt.Dimension;
  44. import java.awt.EventQueue;
  45. import java.awt.FlowLayout;
  46. import java.awt.Font;
  47. import java.awt.Frame;
  48. import java.awt.Graphics;
  49. import java.awt.Graphics2D;
  50. import java.awt.GridLayout;
  51. import java.awt.Image;
  52. import java.awt.MediaTracker;
  53. import java.awt.Point;
  54. import java.awt.RenderingHints;
  55. import java.awt.Toolkit;
  56. import java.awt.Window;
  57. import java.awt.event.ActionEvent;
  58. import java.awt.event.ActionListener;
  59. import java.awt.event.KeyEvent;
  60. import java.awt.event.KeyListener;
  61. import java.awt.event.MouseEvent;
  62. import java.awt.event.MouseListener;
  63. import java.awt.event.MouseMotionListener;
  64. import java.awt.event.WindowAdapter;
  65. import java.awt.event.WindowEvent;
  66. import java.awt.event.WindowListener;
  67. import java.awt.image.BufferedImage;
  68. import java.awt.image.PixelGrabber;
  69. import java.io.File;
  70. import java.io.FileNotFoundException;
  71. import java.io.FileOutputStream;
  72. import java.io.IOException;
  73. import java.io.OutputStream;
  74. import java.io.PrintStream;
  75. import java.lang.Exception;
  76. import java.lang.Integer;
  77. import java.lang.InterruptedException;
  78. import java.lang.Math;
  79. import java.lang.Object;
  80. import java.lang.OutOfMemoryError;
  81. import java.lang.SecurityException;
  82. import java.lang.String;
  83. import java.lang.System;
  84. import java.lang.Thread;
  85. import java.net.URL;
  86. import java.net.NoRouteToHostException;
  87. import java.net.SocketException;
  88. import java.net.UnknownHostException;
  89. import java.util.ArrayList;
  90. import java.util.List;
  91. import java.util.Map;
  92. import java.util.Scanner;
  93. import java.util.TreeMap;
  94. import java.util.Vector;
  95. import javax.imageio.ImageIO;
  96. import javax.swing.BorderFactory;
  97. import javax.swing.Box;
  98. import javax.swing.JButton;
  99. import javax.swing.JCheckBox;
  100. import javax.swing.JCheckBoxMenuItem;
  101. import javax.swing.JColorChooser;
  102. import javax.swing.JDialog;
  103. import javax.swing.JFileChooser;
  104. import javax.swing.JFrame;
  105. import javax.swing.JLabel;
  106. import javax.swing.JMenu;
  107. import javax.swing.JMenuBar;
  108. import javax.swing.JMenuItem;
  109. import javax.swing.JOptionPane;
  110. import javax.swing.JPanel;
  111. import javax.swing.JScrollPane;
  112. import javax.swing.JSlider;
  113. import javax.swing.KeyStroke;
  114. import javax.swing.Timer;
  115. import javax.swing.UIManager;
  116. import javax.swing.event.ChangeEvent;
  117. import javax.swing.event.ChangeListener;
  118. import javax.swing.event.MouseInputAdapter;
  119. import javax.swing.event.MouseInputListener;
  120. import javax.swing.filechooser.FileFilter;
  121.  
  122. /**
  123.  *
  124.  * DrawingPanel is a simplified Java drawing window class to accompany
  125.  * Building Java Programs textbook and associated materials.
  126.  *
  127.  * <p>
  128.  * Authors: Marty Stepp (Stanford University) and Stuart Reges (University of Washington).
  129.  *
  130.  * <p>
  131.  * Version: 4.04, 2016/08/17 (to accompany BJP 4th edition).
  132.  *
  133.  * <p>
  134.  * You can always download the latest {@code DrawingPanel} from
  135.  * <a target="_blank" href="http://www.buildingjavaprograms.com/drawingpanel/DrawingPanel.java">
  136.  * http://www.buildingjavaprograms.com/drawingpanel/DrawingPanel.java</a> .
  137.  *
  138.  * <p>
  139.  * For more information and related materials, please visit
  140.  * <a target="_blank" href="http://www.buildingjavaprograms.com">
  141.  * www.buildingjavaprograms.com</a> .
  142.  *
  143.  * <p>
  144.  * COMPATIBILITY NOTE: This version of DrawingPanel requires Java 8 or higher.
  145.  * To make this file work on Java 7 and lower, you must make two small
  146.  * modifications to its source code.
  147.  * Search for the two occurrences of the annotation @FunctionalInterface
  148.  * and comment them out or remove those lines.
  149.  * Then the file should compile and run properly on older versions of Java.
  150.  *
  151.  * <h3>Description:</h3>
  152.  *
  153.  * <p>
  154.  * The {@code DrawingPanel} class provides a simple interface for drawing persistent
  155.  * images using a {@code Graphics} object.  An internal {@code BufferedImage} object is used
  156.  * to keep track of what has been drawn.  A client of the class simply
  157.  * constructs a {@code DrawingPanel} of a particular size and then draws on it with
  158.  * the {@code Graphics} object, setting the background color if they so choose.
  159.  * </p>
  160.  *
  161.  * <p>
  162.  * The intention is that this custom library will mostly "stay out of the way"
  163.  * so that the client mostly interacts with a standard Java {@code java.awt.Graphics}
  164.  * object, and therefore most of the experience gained while using this library
  165.  * will transfer to Java graphics programming in other contexts.
  166.  * {@code DrawingPanel} is not intended to be a full rich graphical library for things
  167.  * like object-oriented drawing of shapes, animations, creating games, etc.
  168.  * </p>
  169.  *
  170.  * <h3>Example basic usage:</h3>
  171.  *
  172.  * <p>
  173.  * Here is a canonical example of creating a {@code DrawingPanel} of a given size and
  174.  * using it to draw a few shapes.
  175.  * </p>
  176.  *
  177.  * <pre>
  178.  * // basic usage example
  179.  * DrawingPanel panel = new DrawingPanel(600, 400);
  180.  * Graphics g = panel.getGraphics();
  181.  * g.setColor(Color.RED);
  182.  * g.fillRect(17, 45, 139, 241);
  183.  * g.drawOval(234, 77, 100, 100);
  184.  * ...
  185.  * </pre>
  186.  *
  187.  * <p>
  188.  * To ensure that the image is always displayed, a timer calls repaint at
  189.  * regular intervals.
  190.  * </p>
  191.  *
  192.  * <h3>Pixel processing (new in BJP 4th edition):</h3>
  193.  *
  194.  * <p>
  195.  * This version of {@code DrawingPanel} allows you to loop over the pixels of an image.
  196.  * You can process each pixel as a {@code Color} object (easier OO interface, but takes
  197.  * more CPU and memory to run) or as a 32-bit RGB integer (clunkier to use, but
  198.  * much more efficient in runtime and memory usage).
  199.  * Look at the methods get/setPixel(s) to get a better idea.
  200.  *
  201.  * <pre>
  202.  * // example of horizontally flipping an image
  203.  * public static void flipHorizontal(DrawingPanel panel) {
  204.  *     int width  = panel.getWidth();
  205.  *     int height = panel.getHeight();
  206.  *     int[] pixels = panel.getPixelsRGB();
  207.  *     for (int row = 0; row &lt; height; row++) {
  208.  *         for (int col = 0; col &lt; width / 2; col++) {
  209.  *             // swap this pixel with the one opposite it
  210.  *             int col2 = width - 1 - col;
  211.  *             int temp = pixels[row][col];
  212.  *             pixels[row][col] = pixels[row][col2];
  213.  *             pixels[row][col2] = temp;
  214.  *         }
  215.  *     }
  216.  *     panel.setPixels(pixels);
  217.  * }
  218.  * </pre>
  219.  *
  220.  * <h3>Event listeners and lambdas (new in BJP 4th edition):</h3>
  221.  *
  222.  * <p>
  223.  * With Java 8, you can now attach event handlers to listen to keyboard and mouse
  224.  * events that occur in a {@code DrawingPanel} using a lambda function.  For example:
  225.  *
  226.  * <pre>
  227.  * // example of attaching a mouse click handler using Java 8
  228.  * panel.onClick( (x, y) -&gt; System.out.println(x + " " + y) );
  229.  * </pre>
  230.  
  231.  * <h3>Debugging facilities (new in BJP 4th edition):</h3>
  232.  *
  233.  * <p>
  234.  * This version now includes an inner class named {@code DebuggingGraphics}
  235.  * that keeps track of how many times various drawing methods are called.
  236.  * It includes a {@code showCounts} method for the {@code DrawingPanel} itself
  237.  * that allows a client to examine this.  The panel will record basic drawing
  238.  * methods performed by a version of the {@code Graphics} class obtained by
  239.  * calling {@code getDebuggingGraphics} :
  240.  *
  241.  * <pre>
  242.  * // example of debugging counts of graphics method calls
  243.  * Graphics g = panel.getDebuggingGraphics();
  244.  * </pre>
  245.  *
  246.  * <p>
  247.  * Novices will be encouraged to simply print it at the end of {@code main}, as in:
  248.  *
  249.  * <pre>
  250.  * System.out.println(panel.getCounts());
  251.  * </pre>
  252.  *
  253.  * <h3>History and recent changes:</h3>
  254.  *
  255.  * 2016/07/25
  256.  * - Added and cleaned up BJP4 features, static anti-alias settings, bug fixes.
  257.  * <p>
  258.  *
  259.  * 2016/03/07
  260.  * - Code cleanup and improvements to JavaDoc comments for BJP4 release.
  261.  * <p>
  262.  *
  263.  * 2015/09/04
  264.  * - Now includes methods for get/setting individual pixels and all pixels on the
  265.  *   drawing panel.  This helps facilitate 2D array-based pixel-processing
  266.  *   exercises and problems for Building Java Programs, 4th edition.
  267.  * - Code cleanup and reorganization.
  268.  *   Now better alphabetization/formatting of members and encapsulation.
  269.  *   Commenting also augmented throughout code.
  270.  * <p>
  271.  *
  272.  * 2015/04/09
  273.  * - Now includes a DebuggingGraphics inner class that keeps track of how many
  274.  *   times various drawing methods are called.
  275.  *   All additions are commented (search for "DebuggingGraphics")
  276.  * <p>
  277.  *
  278.  * 2011/10/25
  279.  * - save zoomed images (2011/10/25)
  280.  * <p>
  281.  *
  282.  * 2011/10/21
  283.  * - window no longer moves when zoom changes
  284.  * - grid lines
  285.  *
  286.  * @author Marty Stepp, Stanford University, and Stuart Reges, University of Washington
  287.  * @version 4.04, 2016/08/17 (BJP 4th edition)
  288.  */
  289. public final class DrawingPanel implements ImageObserver {
  290.     // class constants
  291.     private static final Color GRID_LINE_COLOR      = new Color(64, 64, 64, 128);   // color of grid lines on panel
  292.     private static final Object LOCK                = new Object();                 // object used for concurrency locking
  293.    
  294.     private static final boolean SAVE_SCALED_IMAGES = true;    // if true, when panel is zoomed, saves images at that zoom factor
  295.     private static final int DELAY                  = 100;     // delay between repaints in millis
  296.     private static final int MAX_FRAMES             = 100;     // max animation frames
  297.     private static final int MAX_SIZE               = 10000;   // max width/height
  298.     private static final int GRID_LINES_PX_GAP_DEFAULT = 10;   // default px between grid lines
  299.    
  300.     private static final String VERSION             = "4.04 (2016/08/17)";
  301.     private static final String ABOUT_MESSAGE       = "DrawingPanel\n"
  302.             + "Graphical library class to support Building Java Programs textbook\n"
  303.             + "written by Marty Stepp, Stanford University\n"
  304.             + "and Stuart Reges, University of Washington\n\n"
  305.             + "Version: " + VERSION + "\n\n"
  306.             + "please visit our web site at:\n"
  307.             + "http://www.buildingjavaprograms.com/";
  308.     private static final String ABOUT_MESSAGE_TITLE = "About DrawingPanel";
  309.     private static final String COURSE_WEB_SITE     = "http://courses.cs.washington.edu/courses/cse142/CurrentQtr/drawingpanel.txt";
  310.     private static final String TITLE               = "Drawing Panel";
  311.    
  312.     /** An RGB integer representing alpha at 100% opacity (0xff000000). */
  313.     public static final int PIXEL_ALPHA             = 0xff000000;   // rgb integer for alpha 100% opacity
  314.    
  315.     /** An RGB integer representing 100% blue (0x000000ff). */
  316.     public static final int PIXEL_BLUE              = 0x000000ff;   // rgb integer for 100% blue
  317.    
  318.     /** An RGB integer representing 100% green (0x0000ff00). */
  319.     public static final int PIXEL_GREEN             = 0x0000ff00;   // rgb integer for 100% green
  320.    
  321.     /** An RGB integer representing 100% red (0x00ff0000). */
  322.     public static final int PIXEL_RED               = 0x00ff0000;   // rgb integer for 100% red
  323.    
  324.     /**
  325.      * The default width of a DrawingPanel in pixels, if none is supplied at construction (500 pixels).
  326.      */
  327.     public static final int DEFAULT_WIDTH           = 500;
  328.    
  329.     /**
  330.      * The default height of a DrawingPanel in pixels, if none is supplied at construction (400 pixels).
  331.      */
  332.     public static final int DEFAULT_HEIGHT          = 400;
  333.    
  334.     /** An internal constant for setting system properties; clients should not use this. */
  335.     public static final String ANIMATED_PROPERTY    = "drawingpanel.animated";
  336.  
  337.     /** An internal constant for setting system properties; clients should not use this. */
  338.     public static final String ANIMATION_FILE_NAME  = "_drawingpanel_animation_save.txt";
  339.  
  340.     /** An internal constant for setting system properties; clients should not use this. */
  341.     public static final String ANTIALIAS_PROPERTY    = "drawingpanel.antialias";
  342.  
  343.     /** An internal constant for setting system properties; clients should not use this. */
  344.     public static final String AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY = "drawingpanel.animateonsleep";
  345.  
  346.     /** An internal constant for setting system properties; clients should not use this. */
  347.     public static final String DIFF_PROPERTY        = "drawingpanel.diff";
  348.  
  349.     /** An internal constant for setting system properties; clients should not use this. */
  350.     public static final String HEADLESS_PROPERTY    = "drawingpanel.headless";
  351.  
  352.     /** An internal constant for setting system properties; clients should not use this. */
  353.     public static final String MULTIPLE_PROPERTY    = "drawingpanel.multiple";
  354.  
  355.     /** An internal constant for setting system properties; clients should not use this. */
  356.     public static final String SAVE_PROPERTY        = "drawingpanel.save";
  357.    
  358.     /* a list of all DrawingPanel instances ever created; used for saving graphical output */
  359.     private static final List<DrawingPanel> INSTANCES = new ArrayList<DrawingPanel>();
  360.    
  361.     // static variables
  362.     private static boolean DEBUG = false;
  363.     private static int instances = 0;
  364.     private static String saveFileName = null;
  365.     private static Boolean headless = null;
  366.     private static Boolean antiAliasDefault = true;
  367.     private static Thread shutdownThread = null;
  368.    
  369.     // static class initializer - sets up thread to close program if
  370.     // last DrawingPanel is closed
  371.     static {
  372.         try {
  373.             String debugProp = String.valueOf(System.getProperty("drawingpanel.debug")).toLowerCase();
  374.             DEBUG = DEBUG || "true".equalsIgnoreCase(debugProp)
  375.                     || "on".equalsIgnoreCase(debugProp)
  376.                     || "yes".equalsIgnoreCase(debugProp)
  377.                     || "1".equals(debugProp);
  378.         } catch (Throwable t) {
  379.             // empty
  380.         }
  381.     }
  382.    
  383.     /*
  384.      * Called when DrawingPanel class loads up.
  385.      * Checks whether the user wants to save an animation to a file.
  386.      */
  387.     private static void checkAnimationSettings() {
  388.         try {
  389.             File settingsFile = new File(ANIMATION_FILE_NAME);
  390.             if (settingsFile.exists()) {
  391.                 Scanner input = new Scanner(settingsFile);
  392.                 String animationSaveFileName = input.nextLine();
  393.                 input.close();
  394.                 System.out.println("***");
  395.                 System.out.println("*** DrawingPanel saving animated GIF: " +
  396.                         new File(animationSaveFileName).getName());
  397.                 System.out.println("***");
  398.                 settingsFile.delete();
  399.  
  400.                 System.setProperty(ANIMATED_PROPERTY, "1");
  401.                 System.setProperty(SAVE_PROPERTY, animationSaveFileName);
  402.             }
  403.         } catch (Exception e) {
  404.             if (DEBUG) {
  405.                 System.out.println("error checking animation settings: " + e);
  406.             }
  407.         }
  408.     }
  409.    
  410.     /*
  411.      * Helper that throws an IllegalArgumentException if the given integer
  412.      * is not between the given min-max inclusive
  413.      */
  414.     private static void ensureInRange(String name, int value, int min, int max) {
  415.         if (value < min || value > max) {
  416.             throw new IllegalArgumentException(name + " must be between " + min
  417.                     + " and " + max + ", but saw " + value);
  418.         }
  419.     }
  420.    
  421.     /*
  422.      * Helper that throws a NullPointerException if the given value is null
  423.      */
  424.     private static void ensureNotNull(String name, Object value) {
  425.         if (value == null) {
  426.             throw new NullPointerException("null value was passed for " + name);
  427.         }
  428.     }
  429.    
  430.     /**
  431.      * Returns the alpha (opacity) component of the given RGB pixel from 0-255.
  432.      * Often used in conjunction with the methods getPixelRGB, setPixelRGB, etc.
  433.      * @param rgb RGB integer with alpha in bits 0-7, red in bits 8-15, green in
  434.      * bits 16-23, and blue in bits 24-31
  435.      * @return alpha component from 0-255
  436.      */
  437.     public static int getAlpha(int rgb) {
  438.         return (rgb & 0xff000000) >> 24;
  439.     }
  440.    
  441.     /**
  442.      * Returns the blue component of the given RGB pixel from 0-255.
  443.      * Often used in conjunction with the methods getPixelRGB, setPixelRGB, etc.
  444.      * @param rgb RGB integer with alpha in bits 0-7, red in bits 8-15, green in
  445.      * bits 16-23, and blue in bits 24-31
  446.      * @return blue component from 0-255
  447.      */
  448.     public static int getBlue(int rgb) {
  449.         return (rgb & 0x000000ff);
  450.     }
  451.    
  452.     /**
  453.      * Returns the green component of the given RGB pixel from 0-255.
  454.      * Often used in conjunction with the methods getPixelRGB, setPixelRGB, etc.
  455.      * @param rgb RGB integer with alpha in bits 0-7, red in bits 8-15, green in
  456.      * bits 16-23, and blue in bits 24-31
  457.      * @return green component from 0-255
  458.      */
  459.     public static int getGreen(int rgb) {
  460.         return (rgb & 0x0000ff00) >> 8;
  461.     }
  462.    
  463.     /**
  464.      * Returns the red component of the given RGB pixel from 0-255.
  465.      * Often used in conjunction with the methods getPixelRGB, setPixelRGB, etc.
  466.      * @param rgb RGB integer with alpha in bits 0-7, red in bits 8-15, green in
  467.      * bits 16-23, and blue in bits 24-31
  468.      * @return red component from 0-255
  469.      */
  470.     public static int getRed(int rgb) {
  471.         return (rgb & 0x00ff0000) >> 16;
  472.     }
  473.    
  474.     /*
  475.      * Returns the given Java system property as a Boolean.
  476.      * Note uppercase-B meaning that if the property isn't set, this will return null.
  477.      * That also means that if you call it and try to store as lowercase-B boolean and
  478.      * it's null, you will crash the program.  You have been warned.
  479.      */
  480.     private static Boolean getPropertyBoolean(String name) {
  481.         try {
  482.             String prop = System.getProperty(name);
  483.             if (prop == null) {
  484.                 return null;
  485.             } else {
  486.                 return name.equalsIgnoreCase("true")
  487.                         || name.equals("1")
  488.                         || name.equalsIgnoreCase("on")
  489.                         || name.equalsIgnoreCase("yes");
  490.             }
  491.         } catch (SecurityException e) {
  492.             if (DEBUG) System.out.println("Security exception when trying to read " + name);
  493.             return null;
  494.         }
  495.     }
  496.    
  497.     /**
  498.      * Returns the file name used for saving all DrawingPanel instances.
  499.      * By default this is null, but it can be set using setSaveFileName
  500.      * or by setting the SAVE_PROPERTY env variable.
  501.      * @return the shared save file name
  502.      */
  503.     public static String getSaveFileName() {
  504.         if (saveFileName == null) {
  505.             try {
  506.                 saveFileName = System.getProperty(SAVE_PROPERTY);
  507.             } catch (SecurityException e) {
  508.                 // empty
  509.             }
  510.         }
  511.         return saveFileName;
  512.     }
  513.    
  514.     /*
  515.      * Returns whether the given Java system property has been set.
  516.      */
  517.     private static boolean hasProperty(String name) {
  518.         try {
  519.             return System.getProperty(name) != null;
  520.         } catch (SecurityException e) {
  521.             if (DEBUG) System.out.println("Security exception when trying to read " + name);
  522.             return false;
  523.         }
  524.     }
  525.    
  526.     /**
  527.      * Returns true if DrawingPanel instances should anti-alias (smooth) their graphics.
  528.      * By default this is true, but it can be set to false using the ANTIALIAS_PROPERTY.
  529.      * @return true if anti-aliasing is enabled (default true)
  530.      */
  531.     public static boolean isAntiAliasDefault() {
  532.         if (antiAliasDefault != null) {
  533.             return antiAliasDefault;
  534.         } else if (hasProperty(ANTIALIAS_PROPERTY)) {
  535.             return getPropertyBoolean(ANTIALIAS_PROPERTY);
  536.         } else {
  537.             return true;   // default
  538.         }
  539.     }
  540.    
  541.     /**
  542.      * Returns true if the class is in "headless" mode, meaning that it is running on
  543.      * a server without a graphical user interface.
  544.      * @return true if we are in headless mode (default false)
  545.      */
  546.     public static boolean isHeadless() {
  547.         if (headless != null) {
  548.             return headless;
  549.         } else {
  550.             return hasProperty(HEADLESS_PROPERTY) && getPropertyBoolean(HEADLESS_PROPERTY);
  551.         }
  552.     }
  553.    
  554.     /**
  555.      * Internal method; returns whether the 'main' thread is still running.
  556.      * Used to determine whether to exit the program when the drawing panel
  557.      * is closed by the user.
  558.      * This is an internal method not meant to be called by clients.
  559.      * @return true if main thread is still running
  560.      */
  561.     public static boolean mainIsActive() {
  562.         ThreadGroup group = Thread.currentThread().getThreadGroup();
  563.         int activeCount = group.activeCount();
  564.        
  565.         // look for the main thread in the current thread group
  566.         Thread[] threads = new Thread[activeCount];
  567.         group.enumerate(threads);
  568.         for (int i = 0; i < threads.length; i++) {
  569.             Thread thread = threads[i];
  570.             String name = String.valueOf(thread.getName()).toLowerCase();
  571.             if (DEBUG) System.out.println("    DrawingPanel.mainIsActive(): " + thread.getName() + ", priority=" + thread.getPriority() + ", alive=" + thread.isAlive() + ", stack=" + java.util.Arrays.toString(thread.getStackTrace()));
  572.             if (name.indexOf("main") >= 0 ||
  573.                 name.indexOf("testrunner-assignmentrunner") >= 0) {
  574.                 // found main thread!
  575.                 // (TestRunnerApplet's main runner also counts as "main" thread)
  576.                 return thread.isAlive();
  577.             }
  578.         }
  579.        
  580.         // didn't find a running main thread; guess that main is done running
  581.         return false;
  582.     }
  583.    
  584.     /*
  585.      * Returns whether the given Java system property has been set to a
  586.      * "truthy" value such as "yes" or "true" or "1".
  587.      */
  588.     private static boolean propertyIsTrue(String name) {
  589.         try {
  590.             String prop = System.getProperty(name);
  591.             return prop != null && (prop.equalsIgnoreCase("true")
  592.                     || prop.equalsIgnoreCase("yes")
  593.                     || prop.equalsIgnoreCase("1"));
  594.         } catch (SecurityException e) {
  595.             if (DEBUG) System.out.println("Security exception when trying to read " + name);
  596.             return false;
  597.         }
  598.     }
  599.    
  600.     /**
  601.      * Saves every DrawingPanel instance that is active.
  602.      * @throws IOException if unable to save any of the files.
  603.      */
  604.     public static void saveAll() throws IOException {
  605.         for (DrawingPanel panel : INSTANCES) {
  606.             if (!panel.hasBeenSaved) {
  607.                 panel.save(getSaveFileName());
  608.             }
  609.         }
  610.     }
  611.    
  612.     /**
  613.      * Sets whether DrawingPanel instances should anti-alias (smooth) their pixels by default.
  614.      * Default true.  You can set this on a given DrawingPanel instance with setAntialias(boolean).
  615.      * @param value whether to enable anti-aliasing (default true)
  616.      */
  617.     public static void setAntiAliasDefault(Boolean value) {
  618.         antiAliasDefault = value;
  619.     }
  620.    
  621.     /**
  622.      * Sets the class to run in "headless" mode, with no graphical output on screen.
  623.      * @param value whether to enable headless mode (default false)
  624.      */
  625.     public static void setHeadless(Boolean value) {
  626.         headless = value;
  627.         if (headless != null) {
  628.             if (headless) {
  629.                 // Set up Java AWT graphics configuration so that it can draw in 'headless' mode
  630.                 // (without popping up actual graphical windows on the server's monitor)
  631.                 // creating the buffered image below will prep the classloader so that Image
  632.                 // classes are available later to the JVM
  633.                 System.setProperty("java.awt.headless", "true");
  634.                 java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(100, 100, java.awt.image.BufferedImage.TYPE_INT_RGB);
  635.                 img.getGraphics().drawRect(10, 20, 30, 40);
  636.             } else {
  637.                 System.setProperty("java.awt.headless", "false");
  638.             }
  639.         }
  640.     }
  641.    
  642.     /**
  643.      * Sets the file to be used when saving graphical output for all DrawingPanels.
  644.      * @param file the file to use as default save file
  645.      */
  646.     public static void setSaveFile(File file) {
  647.         setSaveFileName(file.toString());
  648.     }
  649.    
  650.     /**
  651.      * Sets the filename to be used when saving graphical output for all DrawingPanels.
  652.      * @param filename the name/path of the file to use as default save file
  653.      */
  654.     public static void setSaveFileName(String filename) {
  655.         try {
  656.             System.setProperty(SAVE_PROPERTY, filename);
  657.         } catch (SecurityException e) {
  658.             // empty
  659.         }
  660.         saveFileName = filename;
  661.     }
  662.    
  663.     /**
  664.      * Returns an RGB integer made from the given red, green, and blue components
  665.      * from 0-255.  The returned integer is suitable for use with various RGB
  666.      * integer methods in this class such as setPixel.
  667.      * @param r red component from 0-255 (bits 8-15)
  668.      * @param g green component from 0-255 (bits 16-23)
  669.      * @param b blue component from 0-255 (bits 24-31)
  670.      * @return RGB integer with full 255 for alpha and r-g-b in bits 8-31
  671.      * @throws IllegalArgumentException if r, g, or b is not in 0-255 range
  672.      */
  673.     public static int toRgbInteger(int r, int g, int b) {
  674.         return toRgbInteger(/* alpha */ 255, r, g, b);
  675.     }
  676.    
  677.    
  678.     /**
  679.      * Returns an RGB integer made from the given alpha, red, green, and blue components
  680.      * from 0-255.  The returned integer is suitable for use with various RGB
  681.      * integer methods in this class such as setPixel.
  682.      * @param alpha alpha (transparency) component from 0-255 (bits 0-7)
  683.      * @param r red component from 0-255 (bits 8-15)
  684.      * @param g green component from 0-255 (bits 16-23)
  685.      * @param b blue component from 0-255 (bits 24-31)
  686.      * @return RGB integer with the given four components
  687.      * @throws IllegalArgumentException if alpha, r, g, or b is not in 0-255 range
  688.      */
  689.     public static int toRgbInteger(int alpha, int r, int g, int b) {
  690.         ensureInRange("alpha", alpha, 0, 255);
  691.         ensureInRange("red", r, 0, 255);
  692.         ensureInRange("green", g, 0, 255);
  693.         ensureInRange("blue", b, 0, 255);
  694.         return ((alpha & 0x000000ff) << 24)
  695.                 | ((r & 0x000000ff) << 16)
  696.                 | ((g & 0x000000ff) << 8)
  697.                 | ((b & 0x000000ff));
  698.     }
  699.    
  700.     /*
  701.      * Returns whether the current program is running in the DrJava editor.
  702.      * This was needed in the past because DrJava messed with some settings.
  703.      */
  704.     private static boolean usingDrJava() {
  705.         try {
  706.             return System.getProperty("drjava.debug.port") != null ||
  707.                 System.getProperty("java.class.path").toLowerCase().indexOf("drjava") >= 0;
  708.         } catch (SecurityException e) {
  709.             // running as an applet, or something
  710.             return false;
  711.         }
  712.     }
  713.    
  714.     // fields
  715.     private ActionListener actionListener;
  716.     private List<ImageFrame> frames;       // stores frames of animation to save
  717.     private boolean animated = false;      // changes to true if sleep() is called
  718.     private boolean antialias = isAntiAliasDefault();   // true to smooth corners of shapes
  719.     private boolean gridLines = false;     // grid lines every 10px on screen
  720.     private boolean hasBeenSaved = false;  // set true once saved to file (to avoid re-saving same panel)
  721.     private BufferedImage image;           // remembers drawing commands
  722.     private Color backgroundColor = Color.WHITE;
  723.     private Gif89Encoder encoder;          // for saving animations
  724.     private Graphics g3;                   // new field to support DebuggingGraphics
  725.     private Graphics2D g2;                 // graphics context for painting
  726.     private ImagePanel imagePanel;         // real drawing surface
  727.     private int currentZoom = 1;           // panel's zoom factor for drawing
  728.     private int gridLinesPxGap = GRID_LINES_PX_GAP_DEFAULT;   // px between grid lines
  729.     private int initialPixel;              // initial value in each pixel, for clear()
  730.     private int instanceNumber;            // every DPanel has a unique number
  731.     private int width;                     // dimensions of window frame
  732.     private int height;                    // dimensions of window frame
  733.     private JFileChooser chooser;          // file chooser to save files
  734.     private JFrame frame;                  // overall window frame
  735.     private JLabel statusBar;              // status bar showing mouse position
  736.     private JPanel panel;                  // overall drawing surface
  737.     private long createTime;               // time at which DrawingPanel was constructed
  738.     private Map<String, Integer> counts;   // new field to support DebuggingGraphics
  739.     private MouseInputListener mouseListener;
  740.     private String callingClassName;       // name of class that constructed this panel
  741.     private Timer timer;                   // animation timer
  742.     private WindowListener windowListener;
  743.  
  744.     /**
  745.      * Constructs a drawing panel with a default width and height enclosed in a window.
  746.      * Uses DEFAULT_WIDTH and DEFAULT_HEIGHT for the panel's size.
  747.      */
  748.     public DrawingPanel() {
  749.         this(DEFAULT_WIDTH, DEFAULT_HEIGHT);
  750.     }
  751.  
  752.     /**
  753.      * Constructs a drawing panel of given width and height enclosed in a window.
  754.      * @param width panel's width in pixels
  755.      * @param height panel's height in pixels
  756.      */
  757.     public DrawingPanel(int width, int height) {
  758.         ensureInRange("width", width, 0, MAX_SIZE);
  759.         ensureInRange("height", height, 0, MAX_SIZE);
  760.        
  761.         checkAnimationSettings();
  762.        
  763.         if (DEBUG) System.out.println("DrawingPanel(): going to grab lock");
  764.         synchronized (LOCK) {
  765.             instances++;
  766.             instanceNumber = instances;  // each DrawingPanel stores its own int number
  767.             INSTANCES.add(this);
  768.            
  769.             if (shutdownThread == null && !usingDrJava()) {
  770.                 if (DEBUG) System.out.println("DrawingPanel(): starting idle thread");
  771.                 shutdownThread = new Thread(new Runnable() {
  772.                     // Runnable implementation; used for shutdown thread.
  773.                     public void run() {
  774.                         boolean save = shouldSave();
  775.                         try {
  776.                             while (true) {
  777.                                 // maybe shut down the program, if no more DrawingPanels are onscreen
  778.                                 // and main has finished executing
  779.                                 save |= shouldSave();
  780.                                 if (DEBUG) System.out.println("DrawingPanel idle thread: instances=" + instances + ", save=" + save + ", main active=" + mainIsActive());
  781.                                 if ((instances == 0 || save) && !mainIsActive()) {
  782.                                     try {
  783.                                         System.exit(0);
  784.                                     } catch (SecurityException sex) {
  785.                                         if (DEBUG) System.out.println("DrawingPanel idle thread: unable to exit program: " + sex);
  786.                                     }
  787.                                 }
  788.  
  789.                                 Thread.sleep(250);
  790.                             }
  791.                         } catch (Exception e) {
  792.                             if (DEBUG) System.out.println("DrawingPanel idle thread: exception caught: " + e);
  793.                         }
  794.                     }
  795.                 });
  796.                 // shutdownThread.setPriority(Thread.MIN_PRIORITY);
  797.                 shutdownThread.setName("DrawingPanel-shutdown");
  798.                 shutdownThread.start();
  799.             }
  800.         }
  801.        
  802.         this.width = width;
  803.         this.height = height;
  804.        
  805.         if (DEBUG) System.out.println("DrawingPanel(w=" + width + ",h=" + height + ",anim=" + isAnimated() + ",graph=" + isGraphical() + ",save=" + shouldSave());
  806.        
  807.         if (isAnimated() && shouldSave()) {
  808.             // image must be no more than 256 colors
  809.             image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
  810.             // image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
  811.             antialias = false;   // turn off anti-aliasing to save palette colors
  812.            
  813.             // initially fill the entire frame with the background color,
  814.             // because it won't show through via transparency like with a full ARGB image
  815.             Graphics g = image.getGraphics();
  816.             g.setColor(backgroundColor);
  817.             g.fillRect(0, 0, width + 1, height + 1);
  818.         } else {
  819.             image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
  820.         }
  821.         initialPixel = image.getRGB(0, 0);
  822.        
  823.         g2 = (Graphics2D) image.getGraphics();
  824.         // new field assignments for DebuggingGraphics
  825.         g3 = new DebuggingGraphics();
  826.         counts = new TreeMap<String, Integer>();
  827.         g2.setColor(Color.BLACK);
  828.         if (antialias) {
  829.             g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  830.         }
  831.        
  832.         if (isAnimated()) {
  833.             initializeAnimation();
  834.         }
  835.        
  836.         if (isGraphical()) {
  837.             try {
  838.                 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
  839.             } catch (Exception e) {
  840.                 // empty
  841.             }
  842.            
  843.             statusBar = new JLabel(" ");
  844.             statusBar.setBorder(BorderFactory.createLineBorder(Color.BLACK));
  845.            
  846.             panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
  847.             panel.setBackground(backgroundColor);
  848.             panel.setPreferredSize(new Dimension(width, height));
  849.             imagePanel = new ImagePanel(image);
  850.             imagePanel.setBackground(backgroundColor);
  851.             panel.add(imagePanel);
  852.            
  853.             // listen to mouse movement
  854.             mouseListener = new DPMouseListener();
  855.             panel.addMouseMotionListener(mouseListener);
  856.            
  857.             // main window frame
  858.             frame = new JFrame(TITLE);
  859.             // frame.setResizable(false);
  860.             windowListener = new DPWindowListener();
  861.             frame.addWindowListener(windowListener);
  862.             // JPanel center = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
  863.             JScrollPane center = new JScrollPane(panel);
  864.             // center.add(panel);
  865.             frame.getContentPane().add(center);
  866.             frame.getContentPane().add(statusBar, "South");
  867.             frame.setBackground(Color.DARK_GRAY);
  868.  
  869.             // menu bar
  870.             actionListener = new DPActionListener();
  871.             setupMenuBar();
  872.            
  873.             frame.pack();
  874.             center(frame);
  875.             frame.setVisible(true);
  876.             if (!shouldSave()) {
  877.                 toFront(frame);
  878.             }
  879.            
  880.             // repaint timer so that the screen will update
  881.             createTime = System.currentTimeMillis();
  882.             timer = new Timer(DELAY, actionListener);
  883.             timer.start();
  884.         } else if (shouldSave()) {
  885.             // headless mode; just set a hook on shutdown to save the image
  886.             callingClassName = getCallingClassName();
  887.             try {
  888.                 Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
  889.                     // run on shutdown to save the image
  890.                     public void run() {
  891.                         if (DEBUG) System.out.println("DrawingPanel.run(): Running shutdown hook");
  892.                         if (DEBUG) System.out.println("DrawingPanel shutdown hook: instances=" + instances);
  893.                         try {
  894.                             String filename = System.getProperty(SAVE_PROPERTY);
  895.                             if (filename == null) {
  896.                                 filename = callingClassName + ".png";
  897.                             }
  898.  
  899.                             if (isAnimated()) {
  900.                                 saveAnimated(filename);
  901.                             } else {
  902.                                 save(filename);
  903.                             }
  904.                         } catch (SecurityException e) {
  905.                             System.err.println("Security error while saving image: " + e);
  906.                         } catch (IOException e) {
  907.                             System.err.println("Error saving image: " + e);
  908.                         }
  909.                     }
  910.                 }));
  911.             } catch (Exception e) {
  912.                 if (DEBUG) System.out.println("DrawingPanel(): unable to add shutdown hook: " + e);
  913.             }
  914.         }
  915.     }
  916.    
  917.     /**
  918.      * Constructs a drawing panel that displays the image from the given file enclosed in a window.
  919.      * The panel will be sized exactly to fit the image inside it.
  920.      * @param imageFile the image file to load
  921.      * @throws RuntimeException if the image file is not found
  922.      */
  923.     public DrawingPanel(File imageFile) {
  924.         this(imageFile.toString());
  925.     }
  926.    
  927.     /**
  928.      * Constructs a drawing panel that displays the image from the given file name enclosed in a window.
  929.      * The panel will be sized exactly to fit the image inside it.
  930.      * @param imageFileName the file name/path of the image file to load
  931.      * @throws RuntimeException if the image file is not found
  932.      */
  933.     public DrawingPanel(String imageFileName) {
  934.         this();
  935.         Image image = loadImage(imageFileName);
  936.         setSize(image.getWidth(this), image.getHeight(this));
  937.         getGraphics().drawImage(image, 0, 0, this);
  938.     }
  939.    
  940.     /**
  941.      * Adds the given event listener to respond to key events on this panel.
  942.      * @param listener the key event listener to attach
  943.      */
  944.     public void addKeyListener(KeyListener listener) {
  945.         ensureNotNull("listener", listener);
  946.         frame.addKeyListener(listener);
  947.         panel.setFocusable(false);
  948.         frame.requestFocusInWindow();
  949.         frame.requestFocus();
  950.     }
  951.    
  952.     /**
  953.      * Adds the given event listener to respond to mouse events on this panel.
  954.      * @param listener the mouse event listener to attach
  955.      */
  956.     public void addMouseListener(MouseListener listener) {
  957.         ensureNotNull("listener", listener);
  958.         panel.addMouseListener(listener);
  959.         if (listener instanceof MouseMotionListener) {
  960.             panel.addMouseMotionListener((MouseMotionListener) listener);
  961.         }
  962.     }
  963.    
  964.     /**
  965.      * Adds the given event listener to respond to mouse events on this panel.
  966.      */
  967. //    public void addMouseListener(MouseMotionListener listener) {
  968. //        panel.addMouseMotionListener(listener);
  969. //        if (listener instanceof MouseListener) {
  970. //            panel.addMouseListener((MouseListener) listener);
  971. //        }
  972. //    }
  973.    
  974. //    /**
  975. //     * Adds the given event listener to respond to mouse events on this panel.
  976. //     */
  977. //    public void addMouseListener(MouseInputListener listener) {
  978. //        addMouseListener((MouseListener) listener);
  979. //    }
  980.    
  981.     /*
  982.      * Whether the panel should automatically switch to animated mode
  983.      * if it calls the sleep method.
  984.      */
  985.     private boolean autoEnableAnimationOnSleep() {
  986.         return propertyIsTrue(AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY);
  987.     }
  988.    
  989.     /*
  990.      * Moves the given JFrame to the center of the screen.
  991.      */
  992.     private void center(Window frame) {
  993.         Toolkit tk = Toolkit.getDefaultToolkit();
  994.         Dimension screen = tk.getScreenSize();
  995.         int x = Math.max(0, (screen.width - frame.getWidth()) / 2);
  996.         int y = Math.max(0, (screen.height - frame.getHeight()) / 2);
  997.         frame.setLocation(x, y);
  998.     }
  999.    
  1000.     /*
  1001.      * Constructs and initializes our JFileChooser field if necessary.
  1002.      */
  1003.     private void checkChooser() {
  1004.         if (chooser == null) {
  1005.             chooser = new JFileChooser();
  1006.             try {
  1007.                 chooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
  1008.             } catch (Exception e) {
  1009.                 // empty
  1010.             }
  1011.             chooser.setMultiSelectionEnabled(false);
  1012.             chooser.setFileFilter(new DPFileFilter());
  1013.         }
  1014.     }
  1015.    
  1016.     /**
  1017.      * Erases all drawn shapes/lines/colors from the panel.
  1018.      */
  1019.     public void clear() {
  1020.         int[] pixels = new int[width * height];
  1021.         for (int i = 0; i < pixels.length; i++) {
  1022.             pixels[i] = initialPixel;
  1023.         }
  1024.         image.setRGB(0, 0, width, height, pixels, 0, 1);
  1025.     }
  1026.    
  1027.     /*
  1028.      * Compares the current DrawingPanel image to an image file on disk.
  1029.      */
  1030.     private void compareToFile() {
  1031.         // save current image to a temp file
  1032.         try {
  1033.             String tempFile = saveToTempFile();
  1034.            
  1035.             // use file chooser dialog to find image to compare against
  1036.             checkChooser();
  1037.             if (chooser.showOpenDialog(frame) != JFileChooser.APPROVE_OPTION) {
  1038.                 return;
  1039.             }
  1040.            
  1041.             // user chose a file; let's diff it
  1042.             new DiffImage(chooser.getSelectedFile().toString(), tempFile);
  1043.         } catch (IOException ioe) {
  1044.             JOptionPane.showMessageDialog(frame,
  1045.                                           "Unable to compare images: \n" + ioe);
  1046.         }
  1047.     }
  1048.    
  1049.     /*
  1050.      * Compares the current DrawingPanel image to an image file on the web.
  1051.      */
  1052.     private void compareToURL() {
  1053.         // save current image to a temp file
  1054.         try {
  1055.             String tempFile = saveToTempFile();
  1056.            
  1057.             // get list of images to compare against from web site
  1058.             URL url = new URL(COURSE_WEB_SITE);
  1059.             Scanner input = new Scanner(url.openStream());
  1060.             List<String> lines = new ArrayList<String>();
  1061.             List<String> filenames = new ArrayList<String>();
  1062.             while (input.hasNextLine()) {
  1063.                 String line = input.nextLine().trim();
  1064.                 if (line.length() == 0) { continue; }
  1065.                
  1066.                 if (line.startsWith("#")) {
  1067.                     // a comment
  1068.                     if (line.endsWith(":")) {
  1069.                         // category label
  1070.                         lines.add(line);
  1071.                         line = line.replaceAll("#\\s*", "");
  1072.                         filenames.add(line);
  1073.                     }
  1074.                 } else {
  1075.                     lines.add(line);
  1076.                    
  1077.                     // get filename
  1078.                     int lastSlash = line.lastIndexOf('/');
  1079.                     if (lastSlash >= 0) {
  1080.                         line = line.substring(lastSlash + 1);
  1081.                     }
  1082.                    
  1083.                     // remove extension
  1084.                     int dot = line.lastIndexOf('.');
  1085.                     if (dot >= 0) {
  1086.                         line = line.substring(0, dot);
  1087.                     }
  1088.                    
  1089.                     filenames.add(line);
  1090.                 }
  1091.             }
  1092.             input.close();
  1093.            
  1094.             if (filenames.isEmpty()) {
  1095.                 JOptionPane.showMessageDialog(frame,
  1096.                     "No valid web files found to compare against.",
  1097.                     "Error: no web files found",
  1098.                     JOptionPane.ERROR_MESSAGE);
  1099.                 return;
  1100.             } else {
  1101.                 String fileURL = null;
  1102.                 if (filenames.size() == 1) {
  1103.                     // only one choice; take it
  1104.                     fileURL = lines.get(0);
  1105.                 } else {
  1106.                     // user chooses file to compare against
  1107.                     int choice = showOptionDialog(frame, "File to compare against?",
  1108.                             "Choose File", filenames.toArray(new String[0]));
  1109.                     if (choice < 0) {
  1110.                         return;
  1111.                     }
  1112.  
  1113.                     // user chose a file; let's diff it
  1114.                     fileURL = lines.get(choice);
  1115.                 }
  1116.                 new DiffImage(fileURL, tempFile);
  1117.             }
  1118.         } catch (NoRouteToHostException nrthe) {
  1119.             JOptionPane.showMessageDialog(frame, "You do not appear to have a working internet connection.\nPlease check your internet settings and try again.\n\n" + nrthe);
  1120.         } catch (UnknownHostException uhe) {
  1121.             JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + uhe);
  1122.         } catch (SocketException se) {
  1123.             JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + se);
  1124.         } catch (IOException ioe) {
  1125.             JOptionPane.showMessageDialog(frame, "Unable to compare images: \n" + ioe);
  1126.         }
  1127.     }
  1128.    
  1129.     /*
  1130.      * Closes the DrawingPanel and exits the program.
  1131.      */
  1132.     private void exit() {
  1133.         if (isGraphical()) {
  1134.             frame.setVisible(false);
  1135.             frame.dispose();
  1136.         }
  1137.         try {
  1138.             System.exit(0);
  1139.         } catch (SecurityException e) {
  1140.             // if we're running in an applet or something, can't do System.exit
  1141.         }
  1142.     }
  1143.    
  1144.     /*
  1145.      * Returns a best guess about the name of the class that constructed this panel.
  1146.      */
  1147.     private String getCallingClassName() {
  1148.         StackTraceElement[] stack = new RuntimeException().getStackTrace();
  1149.         String className = this.getClass().getName();
  1150.         for (StackTraceElement element : stack) {
  1151.             String cl = element.getClassName();
  1152.             if (!className.equals(cl)) {
  1153.                 className = cl;
  1154.                 break;
  1155.             }
  1156.         }
  1157.        
  1158.         return className;
  1159.     }
  1160.    
  1161.     /**
  1162.      * Returns a map of counts of occurrences of calls of various drawing methods.
  1163.      * You can print this map to see how many times your graphics methods have
  1164.      * been called to aid in debugging.
  1165.      * @return map of {method name, count} pairs
  1166.      */
  1167.     public Map<String, Integer> getCounts() {
  1168.         return Collections.unmodifiableMap(counts);
  1169.     }
  1170.  
  1171.     /**
  1172.      * A variation of getGraphics that returns an object that records
  1173.      * a count for various drawing methods.
  1174.      * See also: getCounts
  1175.      * @return debug Graphics object
  1176.      */
  1177.     public Graphics getDebuggingGraphics() {
  1178.         if (g3 == null) {
  1179.             g3 = new DebuggingGraphics();
  1180.         }
  1181.         return g3;
  1182.     }
  1183.    
  1184.     /**
  1185.      * Obtain the Graphics object to draw on the panel.
  1186.      * @return panel's Graphics object
  1187.      */
  1188.     public Graphics2D getGraphics() {
  1189.         return g2;
  1190.     }
  1191.    
  1192.     /*
  1193.      * Creates the buffered image for drawing on this panel.
  1194.      */
  1195.     private BufferedImage getImage() {
  1196.         // create second image so we get the background color
  1197.         BufferedImage image2;
  1198.         if (isAnimated()) {
  1199.             image2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
  1200.         } else {
  1201.             image2 = new BufferedImage(width, height, image.getType());
  1202.         }
  1203.         Graphics g = image2.getGraphics();
  1204.         // if (DEBUG) System.out.println("DrawingPanel getImage setting background to " + backgroundColor);
  1205.         g.setColor(backgroundColor);
  1206.         g.fillRect(0, 0, width, height);
  1207.         g.drawImage(image, 0, 0, panel);
  1208.         return image2;
  1209.     }
  1210.    
  1211.     /**
  1212.      * Returns the drawing panel's height in pixels.
  1213.      * @return drawing panel's height in pixels
  1214.      */
  1215.     public int getHeight() {
  1216.         return height;
  1217.     }
  1218.    
  1219.     /**
  1220.      * Returns the color of the pixel at the given x/y coordinate as a Color object.
  1221.      * If nothing has been explicitly drawn on this particular pixel, the panel's
  1222.      * background color is returned.
  1223.      * @param x x-coordinate of pixel to retrieve
  1224.      * @param y y-coordinate of pixel to retrieve
  1225.      * @return pixel (x, y) color as a Color object
  1226.      * @throws IllegalArgumentException if (x, y) is out of range
  1227.      */
  1228.     public Color getPixel(int x, int y) {
  1229.         int rgb = getPixelRGB(x, y);
  1230.         if (getAlpha(rgb) == 0) {
  1231.             return backgroundColor;
  1232.         } else {
  1233.             return new Color(rgb, /* hasAlpha */ true);
  1234.         }
  1235.     }
  1236.    
  1237.     /**
  1238.      * Returns the color of the pixel at the given x/y coordinate as an RGB integer.
  1239.      * The individual red, green, and blue components of the RGB integer can be
  1240.      * extracted from this by calling DrawingPanel.getRed, getGreen, and getBlue.
  1241.      * If nothing has been explicitly drawn on this particular pixel, the panel's
  1242.      * background color is returned.
  1243.      * See also: getPixel.
  1244.      * @param x x-coordinate of pixel to retrieve
  1245.      * @param y y-coordinate of pixel to retrieve
  1246.      * @return pixel (x, y) color as an RGB integer
  1247.      * @throws IllegalArgumentException if (x, y) is out of range
  1248.      */
  1249.     public int getPixelRGB(int x, int y) {
  1250.         ensureInRange("x", x, 0, getWidth() - 1);
  1251.         ensureInRange("y", y, 0, getHeight() - 1);
  1252.         int rgb = image.getRGB(x, y);
  1253.         if (getAlpha(rgb) == 0) {
  1254.             return backgroundColor.getRGB();
  1255.         } else {
  1256.             return rgb;
  1257.         }
  1258.     }
  1259.      
  1260.     /**
  1261.      * Returns the colors of all pixels in this DrawingPanel as a 2-D array
  1262.      * of Color objects.
  1263.      * The first index of the array is the y-coordinate, and the second index
  1264.      * is the x-coordinate.  So, for example, index [r][c] represents the RGB
  1265.      * pixel data for the pixel at position (x=c, y=r).
  1266.      * @return 2D array of colors (row-major)
  1267.      */
  1268.     public Color[][] getPixels() {
  1269.         Color[][] pixels = new Color[getHeight()][getWidth()];
  1270.         for (int row = 0; row < pixels.length; row++) {
  1271.             for (int col = 0; col < pixels[0].length; col++) {
  1272.                 // note axis inversion; x/y => col/row
  1273.                 pixels[row][col] = getPixel(col, row);
  1274.             }
  1275.         }
  1276.         return pixels;
  1277.     }
  1278.    
  1279.     /**
  1280.      * Returns the colors of all pixels in this DrawingPanel as a 2-D array
  1281.      * of RGB integers.
  1282.      * The first index of the array is the y-coordinate, and the second index
  1283.      * is the x-coordinate.  So, for example, index [r][c] represents the RGB
  1284.      * pixel data for the pixel at position (x=c, y=r).
  1285.      * The individual red, green, and blue components of each RGB integer can be
  1286.      * extracted from this by calling DrawingPanel.getRed, getGreen, and getBlue.
  1287.      * @return 2D array of RGB integers (row-major)
  1288.      */
  1289.     public int[][] getPixelsRGB() {
  1290.         int[][] pixels = new int[getHeight()][getWidth()];
  1291.         int backgroundRGB = backgroundColor.getRGB();
  1292.         for (int row = 0; row < pixels.length; row++) {
  1293.             for (int col = 0; col < pixels[0].length; col++) {
  1294.                 // note axis inversion; x/y => col/row
  1295.                 int px = image.getRGB(col, row);
  1296.                 if (getAlpha(px) == 0) {
  1297.                     pixels[row][col] = backgroundRGB;
  1298.                 } else {
  1299.                     pixels[row][col] = px;
  1300.                 }
  1301.             }
  1302.         }
  1303.         return pixels;
  1304.     }
  1305.    
  1306.     /**
  1307.      * Returns the drawing panel's pixel size (width, height) as a Dimension object.
  1308.      * @return panel's size
  1309.      */
  1310.     public Dimension getSize() {
  1311.         return new Dimension(width, height);
  1312.     }
  1313.    
  1314.     /**
  1315.      * Returns the drawing panel's width in pixels.
  1316.      * @return panel's width
  1317.      */
  1318.     public int getWidth() {
  1319.         return width;
  1320.     }
  1321.    
  1322.     /**
  1323.      * Returns the drawing panel's x-coordinate on the screen.
  1324.      * @return panel's x-coordinate
  1325.      */
  1326.     public int getX() {
  1327.         return frame.getX();
  1328.     }
  1329.    
  1330.     /**
  1331.      * Returns the drawing panel's y-coordinate on the screen.
  1332.      * @return panel's y-coordinate
  1333.      */
  1334.     public int getY() {
  1335.         return frame.getY();
  1336.     }
  1337.    
  1338.     /**
  1339.      * Returns the drawing panel's current zoom factor.
  1340.      * Initially this is 1 to indicate 100% zoom, the original size.
  1341.      * A factor of 2 would indicate 200% zoom, and so on.
  1342.      * @return zoom factor (default 1)
  1343.      */
  1344.     public int getZoom() {
  1345.         return currentZoom;
  1346.     }
  1347.    
  1348.  
  1349.     /**
  1350.      * Internal method;
  1351.      * notifies the panel when images are loaded and updated.
  1352.      * This is a required method of ImageObserver interface.
  1353.      * This is an internal method not meant to be called by clients.
  1354.      * @param img internal method; do not call
  1355.      * @param infoflags internal method; do not call
  1356.      * @param x internal method; do not call
  1357.      * @param y internal method; do not call
  1358.      * @param width internal method; do not call
  1359.      * @param height internal method; do not call
  1360.      */
  1361.     @Override
  1362.     public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
  1363.         imagePanel.imageUpdate(img, infoflags, x, y, width, height);
  1364.         return false;
  1365.     }
  1366.    
  1367.     /*
  1368.      * Sets up state for drawing and saving frames of animation to a GIF image.
  1369.      */
  1370.     private void initializeAnimation() {
  1371.         frames = new ArrayList<ImageFrame>();
  1372.         encoder = new Gif89Encoder();
  1373.         /*
  1374.         try {
  1375.             if (hasProperty(SAVE_PROPERTY)) {
  1376.                 stream = new FileOutputStream(System.getProperty(SAVE_PROPERTY));
  1377.             }
  1378.             // encoder.startEncoding(stream);
  1379.         } catch (IOException e) {
  1380.             System.out.println(e);
  1381.         }
  1382.         */
  1383.     }
  1384.    
  1385.     /*
  1386.      * Returns whether this drawing panel is in animation mode.
  1387.      */
  1388.     private boolean isAnimated() {
  1389.         return animated || propertyIsTrue(ANIMATED_PROPERTY);
  1390.     }
  1391.    
  1392.     /*
  1393.      * Returns whether this drawing panel is going to be displayed on screen.
  1394.      * This is almost always true except in some server environments where
  1395.      * the DrawingPanel is run 'headless' without a GUI, often for scripting
  1396.      * and automation purposes.
  1397.      */
  1398.     private boolean isGraphical() {
  1399.         return !hasProperty(SAVE_PROPERTY) && !hasProperty(HEADLESS_PROPERTY);
  1400.     }
  1401.    
  1402.     /*
  1403.      * Returns true if the drawing panel class is in multiple mode.
  1404.      * This would be true if the current program pops up several drawing panels
  1405.      * and we want to save the state of each of them to a different file.
  1406.      */
  1407.     private boolean isMultiple() {
  1408.         return propertyIsTrue(MULTIPLE_PROPERTY);
  1409.     }
  1410.    
  1411.     /**
  1412.      * Loads an image from the given file on disk and returns it
  1413.      * as an Image object.
  1414.      * @param file the file to load
  1415.      * @return loaded image object
  1416.      * @throws NullPointerException if filename is null
  1417.      * @throws RuntimeException if the given file is not found
  1418.      */
  1419.     public Image loadImage(File file) {
  1420.         ensureNotNull("file", file);
  1421.         return loadImage(file.toString());
  1422.     }
  1423.    
  1424.     /**
  1425.      * Loads an image from the given file on disk and returns it
  1426.      * as an Image object.
  1427.      * @param filename name/path of the file to load
  1428.      * @return loaded image object
  1429.      * @throws NullPointerException if filename is null
  1430.      * @throws RuntimeException if the given file is not found
  1431.      */
  1432.     public Image loadImage(String filename) {
  1433.         ensureNotNull("filename", filename);
  1434.         if (!(new File(filename)).exists()) {
  1435.             throw new RuntimeException("DrawingPanel.loadImage: File not found: " + filename);
  1436.         }
  1437.         Image img = Toolkit.getDefaultToolkit().getImage(filename);
  1438.         MediaTracker mt = new MediaTracker(imagePanel);
  1439.         mt.addImage(img, 0);
  1440.         try {
  1441.             mt.waitForAll();
  1442.         } catch (InterruptedException ie) {
  1443.             // empty
  1444.         }
  1445.         return img;
  1446.     }
  1447.    
  1448.     /**
  1449.      * Adds an event handler for mouse clicks.
  1450.      * You can pass a lambda function here to be called when a mouse click event occurs.
  1451.      * @param e event handler function to call
  1452.      * @throws NullPointerException if event handler is null
  1453.      */
  1454.     public void onClick(DPMouseEventHandler e) {
  1455.         onMouseClick(e);
  1456.     }
  1457.    
  1458.     /**
  1459.      * Adds an event handler for mouse drags.
  1460.      * You can pass a lambda function here to be called when a mouse drag event occurs.
  1461.      * @param e event handler function to call
  1462.      * @throws NullPointerException if event handler is null
  1463.      */
  1464.     public void onDrag(DPMouseEventHandler e) {
  1465.         onMouseDrag(e);
  1466.     }
  1467.    
  1468.     /**
  1469.      * Adds an event handler for mouse enters.
  1470.      * You can pass a lambda function here to be called when a mouse enter event occurs.
  1471.      * @param e event handler function to call
  1472.      * @throws NullPointerException if event handler is null
  1473.      */
  1474.     public void onEnter(DPMouseEventHandler e) {
  1475.         onMouseEnter(e);
  1476.     }
  1477.    
  1478.     /**
  1479.      * Adds an event handler for mouse exits.
  1480.      * You can pass a lambda function here to be called when a mouse exit event occurs.
  1481.      * @param e event handler function to call
  1482.      * @throws NullPointerException if event handler is null
  1483.      */
  1484.     public void onExit(DPMouseEventHandler e) {
  1485.         onMouseExit(e);
  1486.     }
  1487.    
  1488.     /**
  1489.      * Adds an event handler for key presses.
  1490.      * You can pass a lambda function here to be called when a key press event occurs.
  1491.      * @param e event handler function to call
  1492.      * @throws NullPointerException if event handler is null
  1493.      */
  1494.     public void onKeyDown(DPKeyEventHandler e) {
  1495.         ensureNotNull("event handler", e);
  1496.         DPKeyEventHandlerAdapter adapter = new DPKeyEventHandlerAdapter(e, "press");
  1497.         addKeyListener(adapter);
  1498.     }
  1499.    
  1500.     /**
  1501.      * Adds an event handler for key releases.
  1502.      * You can pass a lambda function here to be called when a key release event occurs.
  1503.      * @param e event handler function to call
  1504.      * @throws NullPointerException if event handler is null
  1505.      */
  1506.     public void onKeyUp(DPKeyEventHandler e) {
  1507.         ensureNotNull("event handler", e);
  1508.         DPKeyEventHandlerAdapter adapter = new DPKeyEventHandlerAdapter(e, "release");
  1509.         addKeyListener(adapter);
  1510.     }
  1511.    
  1512.     /**
  1513.      * Adds an event handler for mouse clicks.
  1514.      * You can pass a lambda function here to be called when a mouse click event occurs.
  1515.      * @param e event handler function to call
  1516.      * @throws NullPointerException if event handler is null
  1517.      */
  1518.     public void onMouseClick(DPMouseEventHandler e) {
  1519.         ensureNotNull("event handler", e);
  1520.         DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "click");
  1521.         addMouseListener((MouseListener) adapter);
  1522.     }
  1523.    
  1524.     /**
  1525.      * Adds an event handler for mouse button down events.
  1526.      * You can pass a lambda function here to be called when a mouse button down event occurs.
  1527.      * @param e event handler function to call
  1528.      * @throws NullPointerException if event handler is null
  1529.      */
  1530.     public void onMouseDown(DPMouseEventHandler e) {
  1531.         ensureNotNull("event handler", e);
  1532.         DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "press");
  1533.         addMouseListener((MouseListener) adapter);
  1534.     }
  1535.    
  1536.     /**
  1537.      * Adds an event handler for mouse drags.
  1538.      * You can pass a lambda function here to be called when a mouse drag event occurs.
  1539.      * @param e event handler function to call
  1540.      * @throws NullPointerException if event handler is null
  1541.      */
  1542.     public void onMouseDrag(DPMouseEventHandler e) {
  1543.         ensureNotNull("event handler", e);
  1544.         DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "drag");
  1545.         addMouseListener((MouseListener) adapter);
  1546.     }
  1547.    
  1548.     /**
  1549.      * Adds an event handler for mouse enters.
  1550.      * You can pass a lambda function here to be called when a mouse enter event occurs.
  1551.      * @param e event handler function to call
  1552.      * @throws NullPointerException if event handler is null
  1553.      */
  1554.     public void onMouseEnter(DPMouseEventHandler e) {
  1555.         ensureNotNull("event handler", e);
  1556.         DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "enter");
  1557.         addMouseListener((MouseListener) adapter);
  1558.     }
  1559.    
  1560.     /**
  1561.      * Adds an event handler for mouse exits.
  1562.      * You can pass a lambda function here to be called when a mouse exit event occurs.
  1563.      * @param e event handler function to call
  1564.      * @throws NullPointerException if event handler is null
  1565.      */
  1566.     public void onMouseExit(DPMouseEventHandler e) {
  1567.         ensureNotNull("event handler", e);
  1568.         DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "exit");
  1569.         addMouseListener((MouseListener) adapter);
  1570.     }
  1571.    
  1572.     /**
  1573.      * Adds an event handler for mouse movement.
  1574.      * You can pass a lambda function here to be called when a mouse move event occurs.
  1575.      * @param e event handler function to call
  1576.      * @throws NullPointerException if event handler is null
  1577.      */
  1578.     public void onMouseMove(DPMouseEventHandler e) {
  1579.         ensureNotNull("event handler", e);
  1580.         DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "move");
  1581.         addMouseListener((MouseListener) adapter);
  1582.     }
  1583.    
  1584.     /**
  1585.      * Adds an event handler for mouse button up events.
  1586.      * You can pass a lambda function here to be called when a mouse button up event occurs.
  1587.      * @param e event handler function to call
  1588.      * @throws NullPointerException if event handler is null
  1589.      */
  1590.     public void onMouseUp(DPMouseEventHandler e) {
  1591.         ensureNotNull("event handler", e);
  1592.         DPMouseEventHandlerAdapter adapter = new DPMouseEventHandlerAdapter(e, "release");
  1593.         addMouseListener((MouseListener) adapter);
  1594.     }
  1595.  
  1596.     /**
  1597.      * Adds an event handler for mouse movement.
  1598.      * You can pass a lambda function here to be called when a mouse move event occurs.
  1599.      * @param e event handler function to call
  1600.      * @throws NullPointerException if event handler is null
  1601.      */
  1602.     public void onMove(DPMouseEventHandler e) {
  1603.         onMouseMove(e);
  1604.     }
  1605.    
  1606.     /*
  1607.      * Returns whether the drawing panel should be closed and the program
  1608.      * should be shut down.
  1609.      */
  1610.     private boolean readyToClose() {
  1611. /*
  1612.         if (isAnimated()) {
  1613.             // wait a little longer, in case animation is sleeping
  1614.             return System.currentTimeMillis() > createTime + 5 * DELAY;
  1615.         } else {
  1616.             return System.currentTimeMillis() > createTime + 4 * DELAY;
  1617.         }
  1618. */
  1619.         return (instances == 0 || shouldSave()) && !mainIsActive();
  1620.     }
  1621.    
  1622.     /*
  1623.      * Replaces all occurrences of the given old color with the given new color.
  1624.      */
  1625.     private void replaceColor(BufferedImage image, Color oldColor, Color newColor) {
  1626.         int oldRGB = oldColor.getRGB();
  1627.         int newRGB = newColor.getRGB();
  1628.         for (int y = 0; y < image.getHeight(); y++) {
  1629.             for (int x = 0; x < image.getWidth(); x++) {
  1630.                 if (image.getRGB(x, y) == oldRGB) {
  1631.                     image.setRGB(x, y, newRGB);
  1632.                 }
  1633.             }
  1634.         }
  1635.     }
  1636.    
  1637.     /**
  1638.      * Takes the current contents of the drawing panel and writes them to
  1639.      * the given file.
  1640.      * @param file the file to save
  1641.      * @throws NullPointerException if filename is null
  1642.      * @throws IOException if the given file cannot be written
  1643.      */
  1644.     public void save(File file) throws IOException {
  1645.         ensureNotNull("file", file);
  1646.         save(file.toString());
  1647.     }
  1648.    
  1649.    
  1650.     /**
  1651.      * Takes the current contents of the drawing panel and writes them to
  1652.      * the given file.
  1653.      * @param filename name/path of the file to save
  1654.      * @throws NullPointerException if filename is null
  1655.      * @throws IOException if the given file cannot be written
  1656.      */
  1657.     public void save(String filename) throws IOException {
  1658.         ensureNotNull("filename", filename);
  1659.         BufferedImage image2 = getImage();
  1660.        
  1661.         // if zoomed, scale image before saving it
  1662.         if (SAVE_SCALED_IMAGES && currentZoom != 1) {
  1663.             BufferedImage zoomedImage = new BufferedImage(width * currentZoom, height * currentZoom, image.getType());
  1664.             Graphics2D g = (Graphics2D) zoomedImage.getGraphics();
  1665.             g.setColor(Color.BLACK);
  1666.             if (antialias) {
  1667.                 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  1668.             }
  1669.             g.scale(currentZoom, currentZoom);
  1670.             g.drawImage(image2, 0, 0, imagePanel);
  1671.             image2 = zoomedImage;
  1672.         }
  1673.        
  1674.         // if saving multiple panels, append number
  1675.         // (e.g. output_*.png becomes output_1.png, output_2.png, etc.)
  1676.         if (isMultiple()) {
  1677.             filename = filename.replaceAll("\\*", String.valueOf(instanceNumber));
  1678.         }
  1679.  
  1680.         int lastDot = filename.lastIndexOf(".");
  1681.         String extension = filename.substring(lastDot + 1);
  1682.        
  1683.         // write file
  1684.         // (for some reason, NPEs throw sometimes for no reason; just squish them)
  1685.         try {
  1686.             // System.out.println("DrawingPanel DEBUG: saving to " + new File(filename).getAbsolutePath());
  1687.             ImageIO.write(image2, extension, new File(filename));
  1688.         } catch (NullPointerException npe) {
  1689.             // empty
  1690.         } catch (FileNotFoundException fnfe) {
  1691.             // this is a dumb file overwrite issue related to file locking; ignore
  1692.         }
  1693.        
  1694.         hasBeenSaved = true;
  1695.     }
  1696.    
  1697.     /**
  1698.      * Takes the current contents of the drawing panel and writes them to
  1699.      * the given file.
  1700.      * @param file the file to save
  1701.      * @throws NullPointerException if filename is null
  1702.      * @throws IOException if the given file cannot be written
  1703.      */
  1704.     public void saveAnimated(File file) throws IOException {
  1705.         ensureNotNull("file", file);
  1706.         saveAnimated(file.toString());
  1707.     }
  1708.    
  1709.     /**
  1710.      * Takes the current contents of the drawing panel and writes them to
  1711.      * the given file.
  1712.      * @param filename name/path of the file to save
  1713.      * @throws NullPointerException if filename is null
  1714.      * @throws IOException if the given file cannot be written
  1715.      */
  1716.     public void saveAnimated(String filename) throws IOException {
  1717.         ensureNotNull("filename", filename);
  1718.        
  1719.         // add one more final frame
  1720.         if (DEBUG) System.out.println("DrawingPanel.saveAnimated(" + filename + ")");
  1721.         frames.add(new ImageFrame(getImage(), 5000));
  1722.         // encoder.continueEncoding(stream, getImage(), 5000);
  1723.        
  1724.         // Gif89Encoder gifenc = new Gif89Encoder();
  1725.        
  1726.         // add each frame of animation to the encoder
  1727.         try {
  1728.             for (int i = 0; i < frames.size(); i++) {
  1729.                 ImageFrame imageFrame = frames.get(i);
  1730.                 encoder.addFrame(imageFrame.image);
  1731.                 encoder.getFrameAt(i).setDelay(imageFrame.delay);
  1732.                 imageFrame.image.flush();
  1733.                 frames.set(i, null);
  1734.             }
  1735.         } catch (OutOfMemoryError e) {
  1736.             System.out.println("Out of memory when saving");
  1737.         }
  1738.        
  1739.         // gifenc.setComments(annotation);
  1740.         // gifenc.setUniformDelay((int) Math.round(100 / frames_per_second));
  1741.         // gifenc.setUniformDelay(DELAY);
  1742.         // encoder.setBackground(backgroundColor);
  1743.         encoder.setLoopCount(0);
  1744.         encoder.encode(new FileOutputStream(filename));
  1745.     }
  1746.    
  1747.     /*
  1748.      * Called when the user presses the "Save As" menu item.
  1749.      * Pops up a file chooser prompting the user to save their panel to an image.
  1750.      */
  1751.     private void saveAs() {
  1752.         String filename = saveAsHelper("png");
  1753.         if (filename != null) {
  1754.             try {
  1755.                 save(filename);  // save the file
  1756.             } catch (IOException ex) {
  1757.                 JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex);
  1758.             }
  1759.         }
  1760.     }
  1761.    
  1762.     /*
  1763.      * Called when the user presses the "Save As" menu item on an animated panel.
  1764.      * Pops up a file chooser prompting the user to save their panel to an image.
  1765.      */
  1766.     private void saveAsAnimated() {
  1767.         String filename = saveAsHelper("gif");
  1768.         if (filename != null) {
  1769.             try {
  1770.                 // record that the file should be saved next time
  1771.                 PrintStream out = new PrintStream(new File(ANIMATION_FILE_NAME));
  1772.                 out.println(filename);
  1773.                 out.close();
  1774.                
  1775.                 JOptionPane.showMessageDialog(frame,
  1776.                     "Due to constraints about how DrawingPanel works, you'll need to\n" +
  1777.                     "re-run your program.  When you run it the next time, DrawingPanel will \n" +
  1778.                     "automatically save your animated image as: " + new File(filename).getName()
  1779.                 );
  1780.             } catch (IOException ex) {
  1781.                 JOptionPane.showMessageDialog(frame, "Unable to store animation settings:\n" + ex);
  1782.             }
  1783.         }
  1784.     }
  1785.    
  1786.     /*
  1787.      * A helper method to facilitate the Save As action for both animated
  1788.      * and non-animated images.
  1789.      */
  1790.     private String saveAsHelper(String extension) {
  1791.         // use file chooser dialog to get filename to save into
  1792.         checkChooser();
  1793.         if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
  1794.             return null;
  1795.         }
  1796.        
  1797.         File selectedFile = chooser.getSelectedFile();
  1798.         String filename = selectedFile.toString();
  1799.         if (!filename.toLowerCase().endsWith(extension)) {
  1800.             // Windows is dumb about extensions with file choosers
  1801.             filename += "." + extension;
  1802.         }
  1803.  
  1804.         // confirm overwrite of file
  1805.         if (new File(filename).exists() && JOptionPane.showConfirmDialog(
  1806.                 frame, "File exists.  Overwrite?", "Overwrite?",
  1807.                 JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
  1808.             return null;
  1809.         }
  1810.  
  1811.         return filename;
  1812.     }
  1813.    
  1814.     /*
  1815.      * Saves the drawing panel's image to a temporary file and returns
  1816.      * that file's name.
  1817.      */
  1818.     private String saveToTempFile() throws IOException {
  1819.         File currentImageFile = File.createTempFile("current_image", ".png");
  1820.         save(currentImageFile.toString());
  1821.         return currentImageFile.toString();
  1822.     }
  1823.    
  1824.         /**
  1825.          * Sets whether the panel will always cover other windows (default false).
  1826.          * @param alwaysOnTop true if the panel should always cover other windows
  1827.          */
  1828.         public void setAlwaysOnTop(boolean alwaysOnTop) {
  1829.                 if (frame != null) {
  1830.                         frame.setAlwaysOnTop(alwaysOnTop);
  1831.                 }
  1832.         }
  1833.        
  1834.         /**
  1835.          * Sets whether the panel should use anti-aliased / smoothed graphics (default true).
  1836.          * @param antiAlias true if the panel should be smoothed
  1837.          */
  1838.         public void setAntiAlias(boolean antiAlias) {
  1839.                 this.antialias = antiAlias;
  1840.                 Object value = antiAlias ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
  1841.                 if (g2 != null) {
  1842.                         g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, value);
  1843.                 }
  1844.                 imagePanel.repaint();
  1845.         }
  1846.    
  1847.     /**
  1848.      * Sets the background color of the drawing panel to be the given color.
  1849.      * @param c color to use as background
  1850.      * @throws NullPointerException if color is null
  1851.      */
  1852.     public void setBackground(Color c) {
  1853.         ensureNotNull("color", c);
  1854.         Color oldBackgroundColor = backgroundColor;
  1855.         backgroundColor = c;
  1856.         if (isGraphical()) {
  1857.             panel.setBackground(c);
  1858.             imagePanel.setBackground(c);
  1859.         }
  1860.        
  1861.         // with animated images, need to palette-swap the old bg color for the new
  1862.         // because there's no notion of transparency in a palettized 8-bit image
  1863.         if (isAnimated()) {
  1864.             replaceColor(image, oldBackgroundColor, c);
  1865.         }
  1866.     }
  1867.    
  1868.     /**
  1869.      * Sets the background color of the drawing panel to be the color
  1870.      * represented by the given RGB integer.
  1871.      * @param rgb RGB integer to use as background color (full alpha assumed/applied)
  1872.      */
  1873.     public void setBackground(int rgb) {
  1874.         setBackground(new Color(rgb & 0xff000000, /* hasAlpha */ true));
  1875.     }
  1876.    
  1877.     /**
  1878.      * Enables or disables the drawing of grid lines on top of the image to help
  1879.      * with debugging sizes and coordinates.
  1880.      * By default the grid lines will be shown every 10 pixels in each dimension.
  1881.      * @param gridLines whether to show grid lines (true) or not (false)
  1882.      */
  1883.     public void setGridLines(boolean gridLines) {
  1884.         setGridLines(gridLines, GRID_LINES_PX_GAP_DEFAULT);
  1885.     }
  1886.    
  1887.     /**
  1888.      * Enables or disables the drawing of grid lines on top of the image to help
  1889.      * with debugging sizes and coordinates.
  1890.      * The grid lines will be shown every pxGap pixels in each dimension.
  1891.      * @param gridLines whether to show grid lines (true) or not (false)
  1892.      * @param pxGap number of pixels between grid lines
  1893.      */
  1894.     public void setGridLines(boolean gridLines, int pxGap) {
  1895.         this.gridLines = gridLines;
  1896.         this.gridLinesPxGap = pxGap;
  1897.         imagePanel.repaint();
  1898.     }
  1899.    
  1900.     /**
  1901.      * Sets the drawing panel's height in pixels to the given value.
  1902.      * After calling this method, the client must call getGraphics() again
  1903.      * to get the new graphics context of the newly enlarged image buffer.
  1904.      * @param height height, in pixels
  1905.      * @throws IllegalArgumentException if height is negative or exceeds MAX_SIZE
  1906.      */
  1907.     public void setHeight(int height) {
  1908.         setSize(getWidth(), height);
  1909.     }
  1910.    
  1911.     /**
  1912.      * Sets the color of the pixel at the given x/y coordinate to be the given color.
  1913.      * If the color is null, the call has no effect.
  1914.      * @param x x-coordinate of pixel to set
  1915.      * @param y y-coordinate of pixel to set
  1916.      * @param color Color to set the pixel to use
  1917.      * @throws IllegalArgumentException if x or y is out of bounds
  1918.      * @throws NullPointerException if color is null
  1919.      */
  1920.     public void setPixel(int x, int y, Color color) {
  1921.         ensureInRange("x", x, 0, getWidth() - 1);
  1922.         ensureInRange("y", y, 0, getHeight() - 1);
  1923.         ensureNotNull("color", color);
  1924.         image.setRGB(x, y, color.getRGB());
  1925.     }
  1926.      
  1927.     /**
  1928.      * Sets the color of the pixel at the given x/y coordinate to be the color
  1929.      * represented by the given RGB integer.
  1930.      * The passed RGB integer's alpha value is ignored and a full alpha of 255
  1931.      * is always used here, to avoid common bugs with using a 0 value for alpha.
  1932.      * See also: setPixel.
  1933.      * See also: setPixelRGB.
  1934.      * @param x x-coordinate of pixel to set
  1935.      * @param y y-coordinate of pixel to set
  1936.      * @param rgb RGB integer representing the color to set the pixel to use
  1937.      * @throws IllegalArgumentException if x or y is out of bounds
  1938.      */
  1939.     public void setPixel(int x, int y, int rgb) {
  1940.         setPixelRGB(x, y, rgb);
  1941.     }
  1942.      
  1943.     /**
  1944.      * Sets the color of the pixel at the given x/y coordinate to be the color
  1945.      * represented by the given RGB integer.
  1946.      * The passed RGB integer's alpha value is ignored and a full alpha of 255
  1947.      * is always used here, to avoid common bugs with using a 0 value for alpha.
  1948.      * See also: setPixel.
  1949.      * @param x x-coordinate of pixel to set
  1950.      * @param y y-coordinate of pixel to set
  1951.      * @param rgb RGB integer representing the color to set the pixel to use
  1952.      * @throws IllegalArgumentException if x or y is out of bounds
  1953.      */
  1954.     public void setPixelRGB(int x, int y, int rgb) {
  1955.         ensureInRange("x", x, 0, getWidth() - 1);
  1956.         ensureInRange("y", y, 0, getHeight() - 1);
  1957.         image.setRGB(x, y, rgb | PIXEL_ALPHA);
  1958.     }
  1959.    
  1960.     /**
  1961.      * Sets the colors of all pixels in this DrawingPanel to the colors
  1962.      * in the given 2-D array of Color objects.
  1963.      * The first index of the array is the y-coordinate, and the second index
  1964.      * is the x-coordinate.  So, for example, index [r][c] represents the RGB
  1965.      * pixel data for the pixel at position (x=c, y=r).
  1966.      * If the given array's dimensions do not match the width/height of the
  1967.      * drawing panel, the panel is resized to match the array.
  1968.      * If the pixel array is null or size 0, the call has no effect.
  1969.      * If any rows or colors in the array are null, those pixels will be ignored.
  1970.      * The 2-D array passed is assumed to be rectangular in length (not jagged).
  1971.      * @param pixels 2D array of pixels (row-major)
  1972.      * @throws NullPointerException if pixels array is null
  1973.      */
  1974.     public void setPixels(Color[][] pixels) {
  1975.         ensureNotNull("pixels", pixels);
  1976.         if (pixels != null && pixels.length > 0 && pixels[0] != null) {
  1977.             if (width != pixels[0].length || height != pixels.length) {
  1978.                 setSize(pixels[0].length, pixels.length);
  1979.             }
  1980.             for (int row = 0; row < height; row++) {
  1981.                 if (pixels[row] != null) {
  1982.                     for (int col = 0; col < width; col++) {
  1983.                         if (pixels[row][col] != null) {
  1984.                             int rgb = pixels[row][col].getRGB();
  1985.                             image.setRGB(col, row, rgb);
  1986.                         }
  1987.                     }
  1988.                 }
  1989.             }
  1990.         }
  1991.     }
  1992.    
  1993.     /**
  1994.      * Sets the colors of all pixels in this DrawingPanel to the colors
  1995.      * represented by the given 2-D array of RGB integers.
  1996.      * The first index of the array is the y-coordinate, and the second index
  1997.      * is the x-coordinate.  So, for example, index [r][c] represents the RGB
  1998.      * pixel data for the pixel at position (x=c, y=r).
  1999.      * If the given array's dimensions do not match the width/height of the
  2000.      * drawing panel, the panel is resized to match the array.
  2001.      * If the pixel array is null or size 0, the call has no effect.
  2002.      * The 2-D array passed is assumed to be rectangular in length (not jagged).
  2003.      * @param pixels 2D array of pixels (row-major)
  2004.      * @throws NullPointerException if pixels array is null
  2005.      */
  2006.     public void setPixels(int[][] pixels) {
  2007.         setPixelsRGB(pixels);
  2008.     }
  2009.    
  2010.     /**
  2011.      * Sets the colors of all pixels in this DrawingPanel to the colors
  2012.      * represented by the given 2-D array of RGB integers.
  2013.      * The first index of the array is the y-coordinate, and the second index
  2014.      * is the x-coordinate.  So, for example, index [r][c] represents the RGB
  2015.      * pixel data for the pixel at position (x=c, y=r).
  2016.      * If the given array's dimensions do not match the width/height of the
  2017.      * drawing panel, the panel is resized to match the array.
  2018.      * If the pixel array is null or size 0, the call has no effect.
  2019.      * The 2-D array passed is assumed to be rectangular in length (not jagged).
  2020.      * @param pixels 2D array of pixels (row-major)
  2021.      * @throws NullPointerException if pixels array is null
  2022.      */
  2023.     public void setPixelsRGB(int[][] pixels) {
  2024.         ensureNotNull("pixels", pixels);
  2025.         if (pixels != null && pixels.length > 0 && pixels[0] != null) {
  2026.             if (width != pixels[0].length || height != pixels.length) {
  2027.                 setSize(pixels[0].length, pixels.length);
  2028.             }
  2029.             for (int row = 0; row < height; row++) {
  2030.                 if (pixels[row] != null) {
  2031.                     for (int col = 0; col < width; col++) {
  2032.                         // note axis inversion, row/col => y/x
  2033.                         image.setRGB(col, row, pixels[row][col] | PIXEL_ALPHA);
  2034.                     }
  2035.                 }
  2036.             }
  2037.         }
  2038.     }
  2039.    
  2040.     /**
  2041.      * Sets the drawing panel's pixel size (width, height) to the given values.
  2042.      * After calling this method, the client must call getGraphics() again
  2043.      * to get the new graphics context of the newly enlarged image buffer.
  2044.      * @param width width, in pixels
  2045.      * @param height height, in pixels
  2046.      * @throws IllegalArgumentException if width/height is negative or exceeds MAX_SIZE
  2047.      */
  2048.     public void setSize(int width, int height) {
  2049.         ensureInRange("width", width, 0, MAX_SIZE);
  2050.         ensureInRange("height", height, 0, MAX_SIZE);
  2051.        
  2052.         // replace the image buffer for drawing
  2053.         BufferedImage newImage = new BufferedImage(width, height, image.getType());
  2054.         imagePanel.setImage(newImage);
  2055.         newImage.getGraphics().drawImage(image, 0, 0, imagePanel);
  2056.  
  2057.         this.width = width;
  2058.         this.height = height;
  2059.         image = newImage;
  2060.         g2 = (Graphics2D) newImage.getGraphics();
  2061.         g2.setColor(Color.BLACK);
  2062.         if (antialias) {
  2063.             g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  2064.         }
  2065.         zoom(currentZoom);
  2066.         if (isGraphical()) {
  2067.             frame.pack();
  2068.         }
  2069.     }
  2070.    
  2071.     /*
  2072.      * Sets the text that will appear in the drawing panel's bottom status bar.
  2073.      */
  2074.     private void setStatusBarText(String text) {
  2075.         if (currentZoom != 1) {
  2076.             text += " (current zoom: " + currentZoom + "x" + ")";
  2077.         }
  2078.         statusBar.setText(text);
  2079.     }
  2080.    
  2081.     /*
  2082.      * Initializes the drawing panel's menu bar items.
  2083.      */
  2084.     private void setupMenuBar() {
  2085.         // abort compare if we're running as an applet or in a secure environment
  2086.         boolean secure = (System.getSecurityManager() != null);
  2087.        
  2088.         JMenuItem saveAs = new JMenuItem("Save As...", 'A');
  2089.         saveAs.addActionListener(actionListener);
  2090.         saveAs.setAccelerator(KeyStroke.getKeyStroke("ctrl S"));
  2091.         saveAs.setEnabled(!secure);
  2092.        
  2093.         JMenuItem saveAnimated = new JMenuItem("Save Animated GIF...", 'G');
  2094.         saveAnimated.addActionListener(actionListener);
  2095.         saveAnimated.setAccelerator(KeyStroke.getKeyStroke("ctrl A"));
  2096.         saveAnimated.setEnabled(!secure);
  2097.        
  2098.         JMenuItem compare = new JMenuItem("Compare to File...", 'C');
  2099.         compare.addActionListener(actionListener);
  2100.         compare.setEnabled(!secure);
  2101.        
  2102.         JMenuItem compareURL = new JMenuItem("Compare to Web File...", 'U');
  2103.         compareURL.addActionListener(actionListener);
  2104.         compareURL.setAccelerator(KeyStroke.getKeyStroke("ctrl U"));
  2105.         compareURL.setEnabled(!secure);
  2106.        
  2107.         JMenuItem zoomIn = new JMenuItem("Zoom In", 'I');
  2108.         zoomIn.addActionListener(actionListener);
  2109.         zoomIn.setAccelerator(KeyStroke.getKeyStroke("ctrl EQUALS"));
  2110.        
  2111.         JMenuItem zoomOut = new JMenuItem("Zoom Out", 'O');
  2112.         zoomOut.addActionListener(actionListener);
  2113.         zoomOut.setAccelerator(KeyStroke.getKeyStroke("ctrl MINUS"));
  2114.        
  2115.         JMenuItem zoomNormal = new JMenuItem("Zoom Normal (100%)", 'N');
  2116.         zoomNormal.addActionListener(actionListener);
  2117.         zoomNormal.setAccelerator(KeyStroke.getKeyStroke("ctrl 0"));
  2118.        
  2119.         JCheckBoxMenuItem gridLinesItem = new JCheckBoxMenuItem("Grid Lines");
  2120.         gridLinesItem.setMnemonic('G');
  2121.         gridLinesItem.setSelected(gridLines);
  2122.         gridLinesItem.addActionListener(actionListener);
  2123.         gridLinesItem.setAccelerator(KeyStroke.getKeyStroke("ctrl G"));
  2124.        
  2125.         JMenuItem exit = new JMenuItem("Exit", 'x');
  2126.         exit.addActionListener(actionListener);
  2127.        
  2128.         JMenuItem about = new JMenuItem("About...", 'A');
  2129.         about.addActionListener(actionListener);
  2130.        
  2131.         JMenu file = new JMenu("File");
  2132.         file.setMnemonic('F');
  2133.         file.add(compareURL);
  2134.         file.add(compare);
  2135.         file.addSeparator();
  2136.         file.add(saveAs);
  2137.         file.add(saveAnimated);
  2138.         file.addSeparator();
  2139.         file.add(exit);
  2140.        
  2141.         JMenu view = new JMenu("View");
  2142.         view.setMnemonic('V');
  2143.         view.add(zoomIn);
  2144.         view.add(zoomOut);
  2145.         view.add(zoomNormal);
  2146.         view.addSeparator();
  2147.         view.add(gridLinesItem);
  2148.        
  2149.         JMenu help = new JMenu("Help");
  2150.         help.setMnemonic('H');
  2151.         help.add(about);
  2152.        
  2153.         JMenuBar bar = new JMenuBar();
  2154.         bar.add(file);
  2155.         bar.add(view);
  2156.         bar.add(help);
  2157.         frame.setJMenuBar(bar);
  2158.     }
  2159.    
  2160.     /**
  2161.      * Show or hide the drawing panel on the screen.
  2162.      * @param visible true to show, false to hide
  2163.      */
  2164.     public void setVisible(boolean visible) {
  2165.         if (isGraphical()) {
  2166.             frame.setVisible(visible);
  2167.         }
  2168.     }
  2169.    
  2170.     /**
  2171.      * Sets the drawing panel's width in pixels to the given value.
  2172.      * After calling this method, the client must call getGraphics() again
  2173.      * to get the new graphics context of the newly enlarged image buffer.
  2174.      * @param width width, in pixels
  2175.      * @throws IllegalArgumentException if height is negative or exceeds MAX_SIZE
  2176.      */
  2177.     public void setWidth(int width) {
  2178.         ensureInRange("width", width, 0, MAX_SIZE);
  2179.         setSize(width, getHeight());
  2180.     }
  2181.    
  2182.     /*
  2183.      * Returns whether the user wants to perform a 'diff' comparison of their
  2184.      * drawing panel with a given expected output image.
  2185.      */
  2186.     private boolean shouldDiff() {
  2187.         return hasProperty(DIFF_PROPERTY);
  2188.     }
  2189.    
  2190.     /*
  2191.      * Returns whether the user wants to save the drawing panel contents to
  2192.      * a file automatically.
  2193.      */
  2194.     private boolean shouldSave() {
  2195.         return hasProperty(SAVE_PROPERTY);
  2196.     }
  2197.    
  2198.     /*
  2199.      * Shows a dialog box with the given choices;
  2200.      * returns the index chosen (-1 == canceled).
  2201.      */
  2202.     private int showOptionDialog(Frame parent, String title,
  2203.             String message, final String[] names) {
  2204.         final JDialog dialog = new JDialog(parent, title, true);
  2205.         JPanel center = new JPanel(new GridLayout(0, 1));
  2206.        
  2207.         // just a hack to make the return value a mutable reference to an int
  2208.         final int[] hack = {-1};
  2209.        
  2210.         for (int i = 0; i < names.length; i++) {
  2211.             if (names[i].endsWith(":")) {
  2212.                 center.add(new JLabel("<html><b>" + names[i] + "</b></html>"));
  2213.             } else {
  2214.                 final JButton button = new JButton(names[i]);
  2215.                 button.setActionCommand(String.valueOf(i));
  2216.                 button.addActionListener(new ActionListener() {
  2217.                     public void actionPerformed(ActionEvent e) {
  2218.                         hack[0] = Integer.parseInt(button.getActionCommand());
  2219.                         dialog.setVisible(false);
  2220.                     }
  2221.                 });
  2222.                 center.add(button);
  2223.             }
  2224.         }
  2225.        
  2226.         JPanel south = new JPanel();
  2227.         JButton cancel = new JButton("Cancel");
  2228.         cancel.setMnemonic('C');
  2229.         cancel.requestFocus();
  2230.         cancel.addActionListener(new ActionListener() {
  2231.             public void actionPerformed(ActionEvent e) {
  2232.                 dialog.setVisible(false);
  2233.             }
  2234.         });
  2235.         south.add(cancel);
  2236.        
  2237.         dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
  2238.         dialog.getContentPane().setLayout(new BorderLayout(10, 5));
  2239.        
  2240.         if (message != null) {
  2241.             JLabel messageLabel = new JLabel(message);
  2242.             dialog.add(messageLabel, BorderLayout.NORTH);
  2243.         }
  2244.         dialog.add(center);
  2245.         dialog.add(south, BorderLayout.SOUTH);
  2246.         dialog.pack();
  2247.         dialog.setResizable(false);
  2248.         center(dialog);
  2249.         cancel.requestFocus();
  2250.         dialog.setVisible(true);
  2251.         cancel.requestFocus();
  2252.        
  2253.         return hack[0];
  2254.     }
  2255.  
  2256.     /**
  2257.      * Causes the program to pause for the given amount of time in milliseconds.
  2258.      * This allows for animation by calling pause in a loop.
  2259.      * If the DrawingPanel is not showing on the screen, has no effect.
  2260.      * @param millis number of milliseconds to sleep
  2261.      * @throws IllegalArgumentException if a negative number of ms is passed
  2262.      */
  2263.     public void sleep(int millis) {
  2264.         ensureInRange("millis", millis, 0, Integer.MAX_VALUE);
  2265.         if (isGraphical() && frame.isVisible()) {
  2266.             // if not even displaying, we don't actually need to sleep
  2267.             if (millis > 0) {
  2268.                 try {
  2269.                     Thread.sleep(millis);
  2270.                     panel.repaint();
  2271.                     // toFront(frame);
  2272.                 } catch (Exception e) {
  2273.                     // empty
  2274.                 }
  2275.             }
  2276.         }
  2277.        
  2278.         // manually enable animation if necessary
  2279.         if (!isAnimated() && !isMultiple() && autoEnableAnimationOnSleep()) {
  2280.             animated = true;
  2281.             initializeAnimation();
  2282.         }
  2283.        
  2284.         // capture a frame of animation
  2285.         if (isAnimated() && shouldSave() && !isMultiple()) {
  2286.             try {
  2287.                 if (frames.size() < MAX_FRAMES) {
  2288.                     frames.add(new ImageFrame(getImage(), millis));
  2289.                 }
  2290.                
  2291.                 // reset creation timer so that we won't save/close just yet
  2292.                 createTime = System.currentTimeMillis();
  2293.             } catch (OutOfMemoryError e) {
  2294.                 System.out.println("Out of memory after capturing " + frames.size() + " frames");
  2295.             }
  2296.         }
  2297.     }
  2298.    
  2299.     /**
  2300.      * Moves the drawing panel window on top of other windows so it can be seen.
  2301.      */
  2302.     public void toFront() {
  2303.         toFront(frame);
  2304.     }
  2305.    
  2306.     /*
  2307.      * Brings the given window to the front of the Z-ordering.
  2308.      */
  2309.     private void toFront(final Window window) {
  2310.         // TODO: remove anonymous inner class
  2311.         EventQueue.invokeLater(new Runnable() {
  2312.             public void run() {
  2313.                 if (window != null) {
  2314.                     window.toFront();
  2315.                     window.repaint();
  2316.                 }
  2317.             }
  2318.         });
  2319.     }
  2320.    
  2321.     /**
  2322.      * Zooms the drawing panel in/out to the given factor.
  2323.      * A zoom factor of 1, the default, indicates normal size.
  2324.      * A zoom factor of 2 would indicate 200% size, and so on.
  2325.      * The factor value passed should be at least 1; if not, 1 will be used.
  2326.      * @param zoomFactor the zoom factor to use (1 or greater)
  2327.      */
  2328.     public void zoom(int zoomFactor) {
  2329.         currentZoom = Math.max(1, zoomFactor);
  2330.         if (isGraphical()) {
  2331.             Dimension size = new Dimension(width * currentZoom, height * currentZoom);
  2332.             imagePanel.setPreferredSize(size);
  2333.             panel.setPreferredSize(size);
  2334.             imagePanel.validate();
  2335.             imagePanel.revalidate();
  2336.             panel.validate();
  2337.             panel.revalidate();
  2338.             // imagePanel.setSize(size);
  2339.             frame.getContentPane().validate();
  2340.             imagePanel.repaint();
  2341.             setStatusBarText(" ");
  2342.            
  2343.             // resize frame if any more space for it exists or it's the wrong size
  2344.             Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
  2345.             if (size.width <= screen.width || size.height <= screen.height) {
  2346.                 frame.pack();
  2347.             }
  2348.            
  2349.             if (currentZoom != 1) {
  2350.                 frame.setTitle(TITLE + " (" + currentZoom + "x zoom)");
  2351.             } else {
  2352.                 frame.setTitle(TITLE);
  2353.             }
  2354.         }
  2355.     }
  2356.    
  2357.     // INNER/NESTED CLASSES
  2358.    
  2359.     /*
  2360.      * Internal action listener for handling events on buttons and GUI components.
  2361.      */
  2362.     private class DPActionListener implements ActionListener {
  2363.         // used for an internal timer that keeps repainting
  2364.         public void actionPerformed(ActionEvent e) {
  2365.             if (e.getSource() instanceof Timer) {
  2366.                 // redraw the screen at regular intervals to catch all paint operations
  2367.                 panel.repaint();
  2368.                 if (shouldDiff() &&
  2369.                     System.currentTimeMillis() > createTime + 4 * DELAY) {
  2370.                     String expected = System.getProperty(DIFF_PROPERTY);
  2371.                     try {
  2372.                         String actual = saveToTempFile();
  2373.                         DiffImage diff = new DiffImage(expected, actual);
  2374.                         diff.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  2375.                     } catch (IOException ioe) {
  2376.                         System.err.println("Error diffing image: " + ioe);
  2377.                     }
  2378.                     timer.stop();
  2379.                 } else if (shouldSave() && readyToClose()) {
  2380.                     // auto-save-and-close if desired
  2381.                     try {
  2382.                         if (isAnimated()) {
  2383.                             saveAnimated(System.getProperty(SAVE_PROPERTY));
  2384.                         } else {
  2385.                             save(System.getProperty(SAVE_PROPERTY));
  2386.                         }
  2387.                     } catch (IOException ioe) {
  2388.                         System.err.println("Error saving image: " + ioe);
  2389.                     }
  2390.                     exit();
  2391.                 }
  2392.             } else if (e.getActionCommand().equals("Exit")) {
  2393.                 exit();
  2394.             } else if (e.getActionCommand().equals("Compare to File...")) {
  2395.                 compareToFile();
  2396.             } else if (e.getActionCommand().equals("Compare to Web File...")) {
  2397.                 new Thread(new Runnable() {
  2398.                     public void run() {
  2399.                         compareToURL();
  2400.                     }
  2401.                 }).start();
  2402.             } else if (e.getActionCommand().equals("Save As...")) {
  2403.                 saveAs();
  2404.             } else if (e.getActionCommand().equals("Save Animated GIF...")) {
  2405.                 saveAsAnimated();
  2406.             } else if (e.getActionCommand().equals("Zoom In")) {
  2407.                 zoom(currentZoom + 1);
  2408.             } else if (e.getActionCommand().equals("Zoom Out")) {
  2409.                 zoom(currentZoom - 1);
  2410.             } else if (e.getActionCommand().equals("Zoom Normal (100%)")) {
  2411.                 zoom(1);
  2412.             } else if (e.getActionCommand().equals("Grid Lines")) {
  2413.                 setGridLines(((JCheckBoxMenuItem) e.getSource()).isSelected());
  2414.             } else if (e.getActionCommand().equals("About...")) {
  2415.                 JOptionPane.showMessageDialog(frame,
  2416.                         ABOUT_MESSAGE,
  2417.                         ABOUT_MESSAGE_TITLE,
  2418.                         JOptionPane.INFORMATION_MESSAGE);
  2419.             }
  2420.         }
  2421.     }
  2422.    
  2423.     /*
  2424.      * Internal file filter class for showing image files in JFileChooser.
  2425.      */
  2426.     private class DPFileFilter extends FileFilter {
  2427.         public boolean accept(File file) {
  2428.             return file.isDirectory() ||
  2429.                 (file.getName().toLowerCase().endsWith(".png") ||
  2430.                  file.getName().toLowerCase().endsWith(".gif"));
  2431.         }
  2432.        
  2433.         public String getDescription() {
  2434.             return "Image files (*.png; *.gif)";
  2435.         }
  2436.     }
  2437.    
  2438.     // BEGIN EVENT ADAPTER CODE FOR JAVA 8 FUNCTIONAL INTERFACE CLIENTS
  2439.     // EXAMPLE:
  2440.     // panel.onClick( (x, y) -> System.out.println(x + " " + y) );
  2441.    
  2442.     /**
  2443.      * This functional interface is provided to allow Java 8 clients to write
  2444.      * lambda functions to handle mouse events that occur in a DrawingPanel.
  2445.      */
  2446.     @FunctionalInterface
  2447.     public static interface DPMouseEventHandler {
  2448.         /**
  2449.          * Called when a mouse event occurs at the given (x, y) position
  2450.          * in the drawing panel window.
  2451.          * @param x x-coordinate at which the event occurred
  2452.          * @param y y-coordinate at which the event occurred
  2453.          */
  2454.         public void onMouseEvent(int x, int y);
  2455.     }
  2456.    
  2457.     /**
  2458.      * This functional interface is provided to allow Java 8 clients to write
  2459.      * lambda functions to handle key events that occur in a DrawingPanel.
  2460.      */
  2461.     @FunctionalInterface
  2462.     public static interface DPKeyEventHandler {
  2463.         /**
  2464.          * Called when a key event occurs involving the given key character
  2465.          * in the drawing panel window.
  2466.          * @param keyCode char value that was typed
  2467.          */
  2468.         public void onKeyEvent(char keyCode);
  2469.     }
  2470.    
  2471.     // internal class to implement DPKeyEventHandler behavior.
  2472.     private class DPKeyEventHandlerAdapter implements KeyListener {
  2473.         private DPKeyEventHandler handler;
  2474.         private String eventType;
  2475.        
  2476.         /**
  2477.          * Constructs a new key handler adapter.
  2478.          * @param handler event handler function
  2479.          * @param eventType type of event to print
  2480.          */
  2481.         public DPKeyEventHandlerAdapter(DPKeyEventHandler handler, String eventType) {
  2482.             this.handler = handler;
  2483.             this.eventType = eventType.intern();
  2484.         }
  2485.        
  2486.         /**
  2487.          * Called when a key press occurs.
  2488.          * @param e event that occurred
  2489.          */
  2490.         @Override
  2491.         public void keyPressed(KeyEvent e) {
  2492.             // empty; see keyTyped
  2493.         }
  2494.        
  2495.         /**
  2496.          * Called when a key release occurs.
  2497.          * @param e event that occurred
  2498.          */
  2499.         @Override
  2500.         public void keyReleased(KeyEvent e) {
  2501.             if (eventType == "release") {
  2502.                 int keyCode = e.getKeyCode();
  2503.                 if (keyCode < ' ') {
  2504.                     return;
  2505.                 }
  2506.                 handler.onKeyEvent(e.getKeyChar());
  2507.             }
  2508.         }
  2509.        
  2510.         /**
  2511.          * Called when a key type event occurs.
  2512.          * @param e event that occurred
  2513.          */
  2514.         @Override
  2515.         public void keyTyped(KeyEvent e) {
  2516.             if (eventType == "press") {
  2517.                 handler.onKeyEvent(e.getKeyChar());
  2518.             }
  2519.         }
  2520.     }
  2521.    
  2522.     // internal class to implement DPMouseEventHandler behavior.
  2523.     private class DPMouseEventHandlerAdapter implements MouseInputListener {
  2524.         private DPMouseEventHandler handler;
  2525.         private String eventType;
  2526.        
  2527.         /**
  2528.          * Constructs a new mouse handler adapter.
  2529.          * @param handler event handler function
  2530.          * @param eventType type of event to print
  2531.          */
  2532.         public DPMouseEventHandlerAdapter(DPMouseEventHandler handler, String eventType) {
  2533.             this.handler = handler;
  2534.             this.eventType = eventType.intern();
  2535.         }
  2536.        
  2537.         /**
  2538.          * Called when a mouse press occurs.
  2539.          * @param e event that occurred
  2540.          */
  2541.         @Override
  2542.         public void mousePressed(MouseEvent e) {
  2543.             if (eventType == "press") {
  2544.                 handler.onMouseEvent(e.getX(), e.getY());
  2545.             }
  2546.         }
  2547.        
  2548.         /**
  2549.          * Called when a mouse release occurs.
  2550.          * @param e event that occurred
  2551.          */
  2552.         @Override
  2553.         public void mouseReleased(MouseEvent e) {
  2554.             if (eventType == "release") {
  2555.                 handler.onMouseEvent(e.getX(), e.getY());
  2556.             }
  2557.         }
  2558.        
  2559.         /**
  2560.          * Called when a mouse click occurs.
  2561.          * @param e event that occurred
  2562.          */
  2563.         @Override
  2564.         public void mouseClicked(MouseEvent e) {
  2565.             if (eventType == "click") {
  2566.                 handler.onMouseEvent(e.getX(), e.getY());
  2567.             }
  2568.         }
  2569.        
  2570.         /**
  2571.          * Called when a mouse enter occurs.
  2572.          * @param e event that occurred
  2573.          */
  2574.         @Override
  2575.         public void mouseEntered(MouseEvent e) {
  2576.             if (eventType == "enter") {
  2577.                 handler.onMouseEvent(e.getX(), e.getY());
  2578.             }
  2579.         }
  2580.        
  2581.         /**
  2582.          * Called when a mouse exit occurs.
  2583.          * @param e event that occurred
  2584.          */
  2585.         @Override
  2586.         public void mouseExited(MouseEvent e) {
  2587.             if (eventType == "exit") {
  2588.                 handler.onMouseEvent(e.getX(), e.getY());
  2589.             }
  2590.         }
  2591.        
  2592.         /**
  2593.          * Called when a mouse movement occurs.
  2594.          * @param e event that occurred
  2595.          */
  2596.         @Override
  2597.         public void mouseMoved(MouseEvent e) {
  2598.             if (eventType == "move") {
  2599.                 handler.onMouseEvent(e.getX(), e.getY());
  2600.             }
  2601.         }
  2602.        
  2603.         /**
  2604.          * Called when a mouse drag occurs.
  2605.          * @param e event that occurred
  2606.          */
  2607.         @Override
  2608.         public void mouseDragged(MouseEvent e) {
  2609.             if (eventType == "drag") {
  2610.                 handler.onMouseEvent(e.getX(), e.getY());
  2611.             }
  2612.         }
  2613.     }
  2614.    
  2615.     // END EVENT ADAPTER CODE FOR JAVA 8 FUNCTIONAL INTERFACE CLIENTS
  2616.    
  2617.     // Internal MouseListener class for handling mouse events in the panel.
  2618.     private class DPMouseListener extends MouseInputAdapter {
  2619.         // listens to mouse movement
  2620.         public void mouseMoved(MouseEvent e) {
  2621.             int x = e.getX() / currentZoom;
  2622.             int y = e.getY() / currentZoom;
  2623.             String status = "(x=" + x + ", y=" + y + ")";
  2624.             if (x >= 0 && x < width && y >= 0 && y < height) {
  2625.                 int rgb = getPixelRGB(x, y);
  2626.                 int r = getRed(rgb);
  2627.                 int g = getGreen(rgb);
  2628.                 int b = getBlue(rgb);
  2629.                 status += ", r=" + r + " g=" + g + " b=" + b;
  2630.             }
  2631.             setStatusBarText(status);
  2632.         }
  2633.     }
  2634.    
  2635.     // Internal WindowListener class for handling window events in the panel.
  2636.     private class DPWindowListener extends WindowAdapter {
  2637.         // called when DrawingPanel closes, to potentially exit the program
  2638.         public void windowClosing(WindowEvent event) {
  2639.             frame.setVisible(false);
  2640.             synchronized (LOCK) {
  2641.                 instances--;
  2642.             }
  2643.             frame.dispose();
  2644.         }
  2645.     }
  2646.    
  2647.     /*
  2648.      * This inner class passes through calls to the panel's Graphics object g2
  2649.      * but also records a count of how many times various basic drawing methods
  2650.      * are called. This is used for debugging purposes, so that a client can
  2651.      * compare their counts of various graphical method calls to those from an
  2652.      * expected output as a "sanity check" on their program's behavior.
  2653.      * Notice that it extends Graphics and not Graphics2D, so it is more limited
  2654.      * than g2.
  2655.      * @author Stuart Reges
  2656.      */
  2657.     private class DebuggingGraphics extends Graphics {
  2658.         public Graphics create() {
  2659.             return g2.create();
  2660.         }
  2661.  
  2662.         public void translate(int x, int y) {
  2663.             g2.translate(x, y);
  2664.         }
  2665.  
  2666.         public Color getColor() {
  2667.             return g2.getColor();
  2668.         }
  2669.  
  2670.         public void setPaintMode() {
  2671.             g2.setPaintMode();
  2672.         }
  2673.  
  2674.         public void setXORMode(Color c1) {
  2675.             g2.setXORMode(c1);
  2676.         }
  2677.  
  2678.         public Font getFont() {
  2679.             return g2.getFont();
  2680.         }
  2681.  
  2682.         public void setFont(Font font) {
  2683.             g2.setFont(font);
  2684.         }
  2685.  
  2686.         public FontMetrics getFontMetrics(Font f) {
  2687.             return g2.getFontMetrics();
  2688.         }
  2689.  
  2690.         public Rectangle getClipBounds() {
  2691.             return g2.getClipBounds();
  2692.         }
  2693.  
  2694.         public void clipRect(int x, int y, int width, int height) {
  2695.             g2.clipRect(x, y, width, height);
  2696.         }
  2697.  
  2698.         public void setClip(int x, int y, int width, int height) {
  2699.             g2.setClip(x, y, width, height);
  2700.         }
  2701.  
  2702.         public Shape getClip() {
  2703.             return g2.getClip();
  2704.         }
  2705.  
  2706.         public void setClip(Shape clip) {
  2707.             g2.setClip(clip);
  2708.         }
  2709.  
  2710.         public void copyArea(int x, int y, int width, int height, int dx, int dy) {
  2711.             g2.copyArea(x, y, width, height, dx, dy);
  2712.         }
  2713.  
  2714.         public void clearRect(int x, int y, int width, int height) {
  2715.             g2.clearRect(x, y, width, height);
  2716.         }
  2717.  
  2718.         public void drawRoundRect(int x, int y, int width, int height,
  2719.                 int arcWidth, int arcHeight) {
  2720.             g2.drawRoundRect(x, y, width, height, arcWidth, arcHeight);
  2721.         }
  2722.  
  2723.         public void fillRoundRect(int x, int y, int width, int height,
  2724.                 int arcWidth, int arcHeight) {
  2725.             g2.fillRoundRect(x, y, width, height, arcWidth, arcHeight);
  2726.         }
  2727.  
  2728.         public void drawArc(int x, int y, int width, int height,
  2729.                 int startAngle, int arcAngle) {
  2730.             g2.drawArc(x, y, width, height, startAngle, arcAngle);
  2731.         }
  2732.  
  2733.         public void fillArc(int x, int y, int width, int height,
  2734.                 int startAngle, int arcAngle) {
  2735.             g2.fillArc(x, y, width, height, startAngle, arcAngle);
  2736.         }
  2737.  
  2738.         public void drawPolyline(int xPoints[], int yPoints[], int nPoints) {
  2739.             g2.drawPolyline(xPoints, yPoints, nPoints);
  2740.         }
  2741.  
  2742.         public void drawPolygon(int xPoints[], int yPoints[], int nPoints) {
  2743.             g2.drawPolygon(xPoints, yPoints, nPoints);
  2744.         }
  2745.  
  2746.         public void fillPolygon(int xPoints[], int yPoints[], int nPoints) {
  2747.             g2.fillPolygon(xPoints, yPoints, nPoints);
  2748.         }
  2749.  
  2750.         public void drawString(AttributedCharacterIterator iterator, int x,
  2751.                 int y) {
  2752.             g2.drawString(iterator, x, y);
  2753.         }
  2754.  
  2755.         public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
  2756.             return g2.drawImage(img, x, y, observer);
  2757.         };
  2758.  
  2759.         public boolean drawImage(Image img, int x, int y, int width,
  2760.                 int height, ImageObserver observer) {
  2761.             return g2.drawImage(img, x, y, width, height, observer);
  2762.         };
  2763.  
  2764.         public boolean drawImage(Image img, int x, int y, Color bgcolor,
  2765.                 ImageObserver observer) {
  2766.             return g2.drawImage(img, x, y, bgcolor, observer);
  2767.         };
  2768.  
  2769.         public boolean drawImage(Image img, int x, int y, int width,
  2770.                 int height, Color bgcolor, ImageObserver observer) {
  2771.             return g2.drawImage(img, x, y, width, height, bgcolor, observer);
  2772.         }
  2773.  
  2774.         public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2,
  2775.                 int sx1, int sy1, int sx2, int sy2, ImageObserver observer) {
  2776.             return g2.drawImage(img, dx1, dy1, dx2, dy2, sx1, dy1, dx2, sy2,
  2777.                     observer);
  2778.         }
  2779.  
  2780.         public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2,
  2781.                 int sx1, int sy1, int sx2, int sy2, Color bgcolor,
  2782.                 ImageObserver observer) {
  2783.             return g2.drawImage(img, dx1, dy1, dx2, dy2, sx1, dy1, sx2, sy2,
  2784.                     bgcolor, observer);
  2785.         }
  2786.  
  2787.         public void dispose() {
  2788.             g2.dispose();
  2789.         }
  2790.  
  2791.         public void drawOval(int x, int y, int width, int height) {
  2792.             g2.drawOval(x, y, width, height);
  2793.             recordString("drawOval");
  2794.         }
  2795.  
  2796.         public void fillOval(int x, int y, int width, int height) {
  2797.             g2.fillOval(x, y, width, height);
  2798.             recordString("fillOval");
  2799.         }
  2800.  
  2801.         public void drawString(String str, int x, int y) {
  2802.             g2.drawString(str, x, y);
  2803.             recordString("drawString");
  2804.         }
  2805.  
  2806.         public void drawLine(int x1, int y1, int x2, int y2) {
  2807.             g2.drawLine(x1, y1, x2, y2);
  2808.             recordString("drawLine");
  2809.         }
  2810.  
  2811.         public void fillRect(int x, int y, int width, int height) {
  2812.             g2.fillRect(x, y, width, height);
  2813.             recordString("fillRect");
  2814.         }
  2815.  
  2816.         public void drawRect(int x, int y, int width, int height) {
  2817.             g2.drawRect(x, y, width, height);
  2818.             recordString("drawRect");
  2819.         }
  2820.  
  2821.         public void setColor(Color c) {
  2822.             g2.setColor(c);
  2823.             // recordString("setColor");
  2824.         }
  2825.  
  2826.         public void recordString(String s) {
  2827.             if (!counts.containsKey(s)) {
  2828.                 counts.put(s, 1);
  2829.             } else {
  2830.                 counts.put(s, counts.get(s) + 1);
  2831.             }
  2832.         }
  2833.     } // end class DebuggingGraphics
  2834.    
  2835.     /*
  2836.      * This internal class represents a graphical panel that can pop up on the
  2837.      * screen to report the differences between two images.
  2838.      * It is used to allow the client to compare their program's output against
  2839.      * a known correct output and view which pixels differ between the two.
  2840.      */
  2841.     private class DiffImage extends JPanel
  2842.             implements ActionListener, ChangeListener {
  2843.         private static final long serialVersionUID = 0;
  2844.        
  2845.         private BufferedImage image1;
  2846.         private BufferedImage image2;
  2847.         private String image1name;
  2848.         private int numDiffPixels;
  2849.         private int opacity = 50;
  2850.         private String label1Text = "Expected";
  2851.         private String label2Text = "Actual";
  2852.         private boolean highlightDiffs = false;
  2853.        
  2854.         private Color highlightColor = new Color(224, 0, 224);
  2855.         private JLabel image1Label;
  2856.         private JLabel image2Label;
  2857.         private JLabel diffPixelsLabel;
  2858.         private JSlider slider;
  2859.         private JCheckBox box;
  2860.         private JMenuItem saveAsItem;
  2861.         private JMenuItem setImage1Item;
  2862.         private JMenuItem setImage2Item;
  2863.         private JFrame frame;
  2864.         private JButton colorButton;
  2865.        
  2866.         public DiffImage(String file1, String file2) throws IOException {
  2867.             setImage1(file1);
  2868.             setImage2(file2);
  2869.             display();
  2870.         }
  2871.        
  2872.         public void actionPerformed(ActionEvent e) {
  2873.             Object source = e.getSource();
  2874.             if (source == box) {
  2875.                 highlightDiffs = box.isSelected();
  2876.                 repaint();
  2877.             } else if (source == colorButton) {
  2878.                 Color color = JColorChooser.showDialog(frame,
  2879.                                                        "Choose highlight color", highlightColor);
  2880.                 if (color != null) {
  2881.                     highlightColor = color;
  2882.                     colorButton.setBackground(color);
  2883.                     colorButton.setForeground(color);
  2884.                     repaint();
  2885.                 }
  2886.             } else if (source == saveAsItem) {
  2887.                 saveAs();
  2888.             } else if (source == setImage1Item) {
  2889.                 setImage1();
  2890.             } else if (source == setImage2Item) {
  2891.                 setImage2();
  2892.             }
  2893.         }
  2894.        
  2895.         // Counts number of pixels that differ between the two images.
  2896.         public void countDiffPixels() {
  2897.             if (image1 == null || image2 == null) {
  2898.                 return;
  2899.             }
  2900.            
  2901.             int w1 = image1.getWidth();
  2902.             int h1 = image1.getHeight();
  2903.             int w2 = image2.getWidth();
  2904.             int h2 = image2.getHeight();
  2905.             int wmax = Math.max(w1, w2);
  2906.             int hmax = Math.max(h1, h2);
  2907.            
  2908.             // check each pair of pixels
  2909.             numDiffPixels = 0;
  2910.             for (int y = 0; y < hmax; y++) {
  2911.                 for (int x = 0; x < wmax; x++) {
  2912.                     int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0;
  2913.                     int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0;
  2914.                     if (pixel1 != pixel2) {
  2915.                         numDiffPixels++;
  2916.                     }
  2917.                 }
  2918.             }
  2919.         }
  2920.        
  2921.         // initializes diffimage panel
  2922.         public void display() {
  2923.             countDiffPixels();
  2924.            
  2925.             setupComponents();
  2926.             setupEvents();
  2927.             setupLayout();
  2928.            
  2929.             frame.pack();
  2930.             center(frame);
  2931.            
  2932.             frame.setVisible(true);
  2933.             toFront(frame);
  2934.         }
  2935.        
  2936.         // draws the given image onto the given graphics context
  2937.         public void drawImageFull(Graphics2D g2, BufferedImage image) {
  2938.             int iw = image.getWidth();
  2939.             int ih = image.getHeight();
  2940.             int w = getWidth();
  2941.             int h = getHeight();
  2942.             int dw = w - iw;
  2943.             int dh = h - ih;
  2944.            
  2945.             if (dw > 0) {
  2946.                 g2.fillRect(iw, 0, dw, ih);
  2947.             }
  2948.             if (dh > 0) {
  2949.                 g2.fillRect(0, ih, iw, dh);
  2950.             }
  2951.             if (dw > 0 && dh > 0) {
  2952.                 g2.fillRect(iw, ih, dw, dh);
  2953.             }
  2954.             g2.drawImage(image, 0, 0, this);
  2955.         }
  2956.        
  2957.         // paints the DiffImage panel
  2958.         public void paintComponent(Graphics g) {
  2959.             super.paintComponent(g);
  2960.             Graphics2D g2 = (Graphics2D) g;
  2961.            
  2962.             // draw the expected output (image 1)
  2963.             if (image1 != null) {
  2964.                 drawImageFull(g2, image1);
  2965.             }
  2966.            
  2967.             // draw the actual output (image 2)
  2968.             if (image2 != null) {
  2969.                 Composite oldComposite = g2.getComposite();
  2970.                 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, ((float) opacity) / 100));
  2971.                 drawImageFull(g2, image2);
  2972.                 g2.setComposite(oldComposite);
  2973.             }
  2974.             g2.setColor(Color.BLACK);
  2975.            
  2976.             // draw the highlighted diffs (if so desired)
  2977.             if (highlightDiffs && image1 != null && image2 != null) {
  2978.                 int w1 = image1.getWidth();
  2979.                 int h1 = image1.getHeight();
  2980.                 int w2 = image2.getWidth();
  2981.                 int h2 = image2.getHeight();
  2982.                
  2983.                 int wmax = Math.max(w1, w2);
  2984.                 int hmax = Math.max(h1, h2);
  2985.                
  2986.                 // check each pair of pixels
  2987.                 g2.setColor(highlightColor);
  2988.                 for (int y = 0; y < hmax; y++) {
  2989.                     for (int x = 0; x < wmax; x++) {
  2990.                         int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0;
  2991.                         int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0;
  2992.                         if (pixel1 != pixel2) {
  2993.                             g2.fillRect(x, y, 1, 1);
  2994.                         }
  2995.                     }
  2996.                 }
  2997.             }
  2998.         }
  2999.        
  3000.         public void save(File file) throws IOException {
  3001.             // String extension = filename.substring(filename.lastIndexOf(".") + 1);
  3002.             // ImageIO.write(diffImage, extension, new File(filename));
  3003.             String filename = file.getName();
  3004.             String extension = filename.substring(filename.lastIndexOf(".") + 1);
  3005.             BufferedImage img = new BufferedImage(getPreferredSize().width, getPreferredSize().height, BufferedImage.TYPE_INT_ARGB);
  3006.             img.getGraphics().setColor(getBackground());
  3007.             img.getGraphics().fillRect(0, 0, img.getWidth(), img.getHeight());
  3008.             paintComponent(img.getGraphics());
  3009.             ImageIO.write(img, extension, file);
  3010.         }
  3011.        
  3012.         public void save(String filename) throws IOException {
  3013.             save(new File(filename));
  3014.         }
  3015.        
  3016.         // Called when "Save As" menu item is clicked
  3017.         public void saveAs() {
  3018.             checkChooser();
  3019.             if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
  3020.                 return;
  3021.             }
  3022.            
  3023.             File selectedFile = chooser.getSelectedFile();
  3024.             try {
  3025.                 save(selectedFile.toString());
  3026.             } catch (IOException ex) {
  3027.                 JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex);
  3028.             }
  3029.         }
  3030.        
  3031.         // called when "Set Image 1" menu item is clicked
  3032.         public void setImage1() {
  3033.             checkChooser();
  3034.             if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
  3035.                 return;
  3036.             }
  3037.            
  3038.             File selectedFile = chooser.getSelectedFile();
  3039.             try {
  3040.                 setImage1(selectedFile.toString());
  3041.                 countDiffPixels();
  3042.                 diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)");
  3043.                 image1Label.setText(selectedFile.getName());
  3044.                 frame.pack();
  3045.             } catch (IOException ex) {
  3046.                 JOptionPane.showMessageDialog(frame, "Unable to set image 1:\n" + ex);
  3047.             }
  3048.         }
  3049.        
  3050.         // sets image 1 to be the given image
  3051.         public void setImage1(BufferedImage image) {
  3052.             if (image == null) {
  3053.                 throw new NullPointerException();
  3054.             }
  3055.            
  3056.             image1 = image;
  3057.             setPreferredSize(new Dimension(
  3058.                                            Math.max(getPreferredSize().width, image.getWidth()),
  3059.                                            Math.max(getPreferredSize().height, image.getHeight()))
  3060.                                  );
  3061.             if (frame != null) {
  3062.                 frame.pack();
  3063.             }
  3064.             repaint();
  3065.         }
  3066.        
  3067.         // loads image 1 from the given filename or URL
  3068.         public void setImage1(String filename) throws IOException {
  3069.             image1name = new File(filename).getName();
  3070.             if (filename.startsWith("http")) {
  3071.                 setImage1(ImageIO.read(new URL(filename)));
  3072.             } else {
  3073.                 setImage1(ImageIO.read(new File(filename)));
  3074.             }
  3075.         }
  3076.        
  3077.         // called when "Set Image 2" menu item is clicked
  3078.         public void setImage2() {
  3079.             checkChooser();
  3080.             if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
  3081.                 return;
  3082.             }
  3083.            
  3084.             File selectedFile = chooser.getSelectedFile();
  3085.             try {
  3086.                 setImage2(selectedFile.toString());
  3087.                 countDiffPixels();
  3088.                 diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)");
  3089.                 image2Label.setText(selectedFile.getName());
  3090.                 frame.pack();
  3091.             } catch (IOException ex) {
  3092.                 JOptionPane.showMessageDialog(frame, "Unable to set image 2:\n" + ex);
  3093.             }
  3094.         }
  3095.        
  3096.         // sets image 2 to be the given image
  3097.         public void setImage2(BufferedImage image) {
  3098.             if (image == null) {
  3099.                 throw new NullPointerException();
  3100.             }
  3101.            
  3102.             image2 = image;
  3103.             setPreferredSize(new Dimension(
  3104.                                            Math.max(getPreferredSize().width, image.getWidth()),
  3105.                                            Math.max(getPreferredSize().height, image.getHeight()))
  3106.                                  );
  3107.             if (frame != null) {
  3108.                 frame.pack();
  3109.             }
  3110.             repaint();
  3111.         }
  3112.        
  3113.         // loads image 2 from the given filename
  3114.         public void setImage2(String filename) throws IOException {
  3115.             if (filename.startsWith("http")) {
  3116.                 setImage2(ImageIO.read(new URL(filename)));
  3117.             } else {
  3118.                 setImage2(ImageIO.read(new File(filename)));
  3119.             }
  3120.  
  3121.         }
  3122.        
  3123.         private void setupComponents() {
  3124.             String title = "DiffImage";
  3125.             if (image1name != null) {
  3126.                 title = "Compare to " + image1name;
  3127.             }
  3128.             frame = new JFrame(title);
  3129.             frame.setResizable(false);
  3130.             // frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  3131.            
  3132.             slider = new JSlider();
  3133.             slider.setPaintLabels(false);
  3134.             slider.setPaintTicks(true);
  3135.             slider.setSnapToTicks(true);
  3136.             slider.setMajorTickSpacing(25);
  3137.             slider.setMinorTickSpacing(5);
  3138.            
  3139.             box = new JCheckBox("Highlight diffs in color: ", highlightDiffs);
  3140.            
  3141.             colorButton = new JButton();
  3142.             colorButton.setBackground(highlightColor);
  3143.             colorButton.setForeground(highlightColor);
  3144.             colorButton.setPreferredSize(new Dimension(24, 24));
  3145.            
  3146.             diffPixelsLabel = new JLabel("(" + numDiffPixels + " pixels differ)");
  3147.             diffPixelsLabel.setFont(diffPixelsLabel.getFont().deriveFont(Font.BOLD));
  3148.             image1Label = new JLabel(label1Text);
  3149.             image2Label = new JLabel(label2Text);
  3150.            
  3151.             setupMenuBar();
  3152.         }
  3153.        
  3154.         // initializes layout of components
  3155.         private void setupLayout() {
  3156.             JPanel southPanel1 = new JPanel();
  3157.             southPanel1.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
  3158.             southPanel1.add(image1Label);
  3159.             southPanel1.add(slider);
  3160.             southPanel1.add(image2Label);
  3161.             southPanel1.add(Box.createHorizontalStrut(20));
  3162.            
  3163.             JPanel southPanel2 = new JPanel();
  3164.             southPanel2.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
  3165.             southPanel2.add(diffPixelsLabel);
  3166.             southPanel2.add(Box.createHorizontalStrut(20));
  3167.             southPanel2.add(box);
  3168.             southPanel2.add(colorButton);
  3169.            
  3170.             Container southPanel = javax.swing.Box.createVerticalBox();
  3171.             southPanel.add(southPanel1);
  3172.             southPanel.add(southPanel2);
  3173.            
  3174.             frame.add(this, BorderLayout.CENTER);
  3175.             frame.add(southPanel, BorderLayout.SOUTH);
  3176.         }
  3177.        
  3178.         // initializes main menu bar
  3179.         private void setupMenuBar() {
  3180.             saveAsItem = new JMenuItem("Save As...", 'A');
  3181.             saveAsItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S"));
  3182.             setImage1Item = new JMenuItem("Set Image 1...", '1');
  3183.             setImage1Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 1"));
  3184.             setImage2Item = new JMenuItem("Set Image 2...", '2');
  3185.             setImage2Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 2"));
  3186.            
  3187.             JMenu file = new JMenu("File");
  3188.             file.setMnemonic('F');
  3189.             file.add(setImage1Item);
  3190.             file.add(setImage2Item);
  3191.             file.addSeparator();
  3192.             file.add(saveAsItem);
  3193.            
  3194.             JMenuBar bar = new JMenuBar();
  3195.             bar.add(file);
  3196.            
  3197.             // disabling menu bar to simplify code
  3198.             // frame.setJMenuBar(bar);
  3199.         }
  3200.        
  3201.         // method of ChangeListener interface
  3202.         public void stateChanged(ChangeEvent e) {
  3203.             opacity = slider.getValue();
  3204.             repaint();
  3205.         }
  3206.        
  3207.         // adds event listeners to various components
  3208.         private void setupEvents() {
  3209.             slider.addChangeListener(this);
  3210.             box.addActionListener(this);
  3211.             colorButton.addActionListener(this);
  3212.             saveAsItem.addActionListener(this);
  3213.             this.setImage1Item.addActionListener(this);
  3214.             this.setImage2Item.addActionListener(this);
  3215.         }
  3216.     }
  3217.    
  3218.     // inner class to represent one frame of an animated GIF
  3219.     private static class ImageFrame {
  3220.         public Image image;
  3221.         public int delay;
  3222.        
  3223.         public ImageFrame(Image image, int delay) {
  3224.             this.image = image;
  3225.             this.delay = delay / 10;   // strangely, gif stores delay as sec/100
  3226.         }
  3227.     }
  3228.  
  3229.     // inner class to do the actual drawing onto the DrawingPanel
  3230.     private class ImagePanel extends JPanel {
  3231.         private static final long serialVersionUID = 0;
  3232.         private Image image;
  3233.        
  3234.         // constructs the image panel
  3235.         public ImagePanel(Image image) {
  3236.                 super(/* isDoubleBuffered */ true);
  3237.             setImage(image);
  3238.             setBackground(Color.WHITE);
  3239.             setPreferredSize(new Dimension(image.getWidth(this), image.getHeight(this)));
  3240.             setAlignmentX(0.0f);
  3241.         }
  3242.        
  3243.         // draws everything onto the panel
  3244.         public void paintComponent(Graphics g) {
  3245.             super.paintComponent(g);
  3246.             Graphics2D g2 = (Graphics2D) g;
  3247.             if (currentZoom != 1) {
  3248.                 g2.scale(currentZoom, currentZoom);
  3249.             }
  3250.             g2.drawImage(image, 0, 0, this);
  3251.            
  3252.             // possibly draw grid lines for debugging
  3253.             if (gridLines) {
  3254.                 g2.setPaint(GRID_LINE_COLOR);
  3255.                 for (int row = 1; row <= getHeight() / gridLinesPxGap; row++) {
  3256.                     g2.drawLine(0, row * gridLinesPxGap, getWidth(), row * gridLinesPxGap);
  3257.                 }
  3258.                 for (int col = 1; col <= getWidth() / gridLinesPxGap; col++) {
  3259.                     g2.drawLine(col * gridLinesPxGap, 0, col * gridLinesPxGap, getHeight());
  3260.                 }
  3261.             }
  3262.         }
  3263.        
  3264.         public void setImage(Image image) {
  3265.             this.image = image;
  3266.             repaint();
  3267.         }
  3268.     }
  3269.  
  3270.     // BEGIN GIF ENCODING CLASSES
  3271.  
  3272.     //******************************************************************************
  3273.     // DirectGif89Frame.java
  3274.     //******************************************************************************
  3275.  
  3276.     // ==============================================================================
  3277.     /**
  3278.      * Instances of this Gif89Frame subclass are constructed from RGB image
  3279.      * info, either in the form of an Image object or a pixel array.
  3280.      * <p>
  3281.      * There is an important restriction to note. It is only permissible to add
  3282.      * DirectGif89Frame objects to a Gif89Encoder constructed without an
  3283.      * explicit color map. The GIF color table will be automatically generated
  3284.      * from pixel information.
  3285.      *
  3286.      * @version 0.90 beta (15-Jul-2000)
  3287.      * @author J. M. G. Elliott (tep@jmge.net)
  3288.      * @see Gif89Encoder
  3289.      * @see Gif89Frame
  3290.      * @see IndexGif89Frame
  3291.      */
  3292.     class DirectGif89Frame extends Gif89Frame {
  3293.  
  3294.         private int[] argbPixels;
  3295.  
  3296.         // ----------------------------------------------------------------------------
  3297.         /**
  3298.          * Construct an DirectGif89Frame from a Java image.
  3299.          *
  3300.          * @param img
  3301.          *            A java.awt.Image object that supports pixel-grabbing.
  3302.          * @exception IOException
  3303.          *                If the image is unencodable due to failure of
  3304.          *                pixel-grabbing.
  3305.          */
  3306.         public DirectGif89Frame(Image img) throws IOException {
  3307.             PixelGrabber pg = new PixelGrabber(img, 0, 0, -1, -1, true);
  3308.  
  3309.             String errmsg = null;
  3310.             try {
  3311.                 if (!pg.grabPixels())
  3312.                     errmsg = "can't grab pixels from image";
  3313.             } catch (InterruptedException e) {
  3314.                 errmsg = "interrupted grabbing pixels from image";
  3315.             }
  3316.  
  3317.             if (errmsg != null)
  3318.                 throw new IOException(errmsg + " (" + getClass().getName()
  3319.                         + ")");
  3320.  
  3321.             theWidth = pg.getWidth();
  3322.             theHeight = pg.getHeight();
  3323.             argbPixels = (int[]) pg.getPixels();
  3324.             ciPixels = new byte[argbPixels.length];
  3325.  
  3326.             // flush to conserve resources
  3327.             img.flush();
  3328.         }
  3329.  
  3330.         // ----------------------------------------------------------------------------
  3331.         /**
  3332.          * Construct an DirectGif89Frame from ARGB pixel data.
  3333.          *
  3334.          * @param width
  3335.          *            Width of the bitmap.
  3336.          * @param height
  3337.          *            Height of the bitmap.
  3338.          * @param argb_pixels
  3339.          *            Array containing at least width*height pixels in the
  3340.          *            format returned by java.awt.Color.getRGB().
  3341.          */
  3342.         public DirectGif89Frame(int width, int height, int argb_pixels[]) {
  3343.             theWidth = width;
  3344.             theHeight = height;
  3345.             argbPixels = new int[theWidth * theHeight];
  3346.             System.arraycopy(argb_pixels, 0, argbPixels, 0, argbPixels.length);
  3347.             ciPixels = new byte[argbPixels.length];
  3348.         }
  3349.  
  3350.         // ----------------------------------------------------------------------------
  3351.         Object getPixelSource() {
  3352.             return argbPixels;
  3353.         }
  3354.     }
  3355.  
  3356.     // ******************************************************************************
  3357.     // Gif89Encoder.java
  3358.     // ******************************************************************************
  3359.  
  3360.     // ==============================================================================
  3361.     /**
  3362.      * This is the central class of a JDK 1.1 compatible GIF encoder that,
  3363.      * AFAIK, supports more features of the extended GIF spec than any other
  3364.      * Java open source encoder. Some sections of the source are lifted or
  3365.      * adapted from Jef Poskanzer's <cite>Acme GifEncoder</cite> (so please see
  3366.      * the <a href="../readme.txt">readme</a> containing his notice), but much
  3367.      * of it, including nearly all of the present class, is original code. My
  3368.      * main motivation for writing a new encoder was to support animated GIFs,
  3369.      * but the package also adds support for embedded textual comments.
  3370.      * <p>
  3371.      * There are still some limitations. For instance, animations are limited to
  3372.      * a single global color table. But that is usually what you want anyway, so
  3373.      * as to avoid irregularities on some displays. (So this is not really a
  3374.      * limitation, but a "disciplinary feature" :) Another rather more serious
  3375.      * restriction is that the total number of RGB colors in a given input-batch
  3376.      * mustn't exceed 256. Obviously, there is an opening here for someone who
  3377.      * would like to add a color-reducing preprocessor.
  3378.      * <p>
  3379.      * The encoder, though very usable in its present form, is at bottom only a
  3380.      * partial implementation skewed toward my own particular needs. Hence a
  3381.      * couple of caveats are in order. (1) During development it was in the back
  3382.      * of my mind that an encoder object should be reusable - i.e., you should
  3383.      * be able to make multiple calls to encode() on the same object, with or
  3384.      * without intervening frame additions or changes to options. But I haven't
  3385.      * reviewed the code with such usage in mind, much less tested it, so it's
  3386.      * likely I overlooked something. (2) The encoder classes aren't thread
  3387.      * safe, so use caution in a context where access is shared by multiple
  3388.      * threads. (Better yet, finish the library and re-release it :)
  3389.      * <p>
  3390.      * There follow a couple of simple examples illustrating the most common way
  3391.      * to use the encoder, i.e., to encode AWT Image objects created elsewhere
  3392.      * in the program. Use of some of the most popular format options is also
  3393.      * shown, though you will want to peruse the API for additional features.
  3394.      *
  3395.      * <p>
  3396.      * <strong>Animated GIF Example</strong>
  3397.      *
  3398.      * <pre>
  3399.      *  import net.jmge.gif.Gif89Encoder;
  3400.      *  // ...
  3401.      *  void writeAnimatedGIF(Image[] still_images,
  3402.      *                      String annotation,
  3403.      *                      boolean looped,
  3404.      *                      double frames_per_second,
  3405.      *                      OutputStream out) throws IOException
  3406.      *  {
  3407.      *  Gif89Encoder gifenc = new Gif89Encoder();
  3408.      *  for (int i = 0; i < still_images.length; ++i)
  3409.      *    gifenc.addFrame(still_images[i]);
  3410.      *  gifenc.setComments(annotation);
  3411.      *  gifenc.setLoopCount(looped ? 0 : 1);
  3412.      *  gifenc.setUniformDelay((int) Math.round(100 / frames_per_second));
  3413.      *  gifenc.encode(out);
  3414.      *  }
  3415.      * </pre>
  3416.      *
  3417.      * <strong>Static GIF Example</strong>
  3418.      *
  3419.      * <pre>
  3420.      *  import net.jmge.gif.Gif89Encoder;
  3421.      *  // ...
  3422.      *  void writeNormalGIF(Image img,
  3423.      *                    String annotation,
  3424.      *                    int transparent_index,  // pass -1 for none
  3425.      *                    boolean interlaced,
  3426.      *                    OutputStream out) throws IOException
  3427.      *  {
  3428.      *  Gif89Encoder gifenc = new Gif89Encoder(img);
  3429.      *  gifenc.setComments(annotation);
  3430.      *  gifenc.setTransparentIndex(transparent_index);
  3431.      *  gifenc.getFrameAt(0).setInterlaced(interlaced);
  3432.      *  gifenc.encode(out);
  3433.      *  }
  3434.      * </pre>
  3435.      *
  3436.      * @version 0.90 beta (15-Jul-2000)
  3437.      * @author J. M. G. Elliott (tep@jmge.net)
  3438.      * @see Gif89Frame
  3439.      * @see DirectGif89Frame
  3440.      * @see IndexGif89Frame
  3441.      */
  3442.     class Gif89Encoder {
  3443.         private static final boolean DEBUG = false;
  3444.         private Dimension dispDim = new Dimension(0, 0);
  3445.         private GifColorTable colorTable;
  3446.         private int bgIndex = 0;
  3447.         private int loopCount = 1;
  3448.         private String theComments;
  3449.         private Vector<Gif89Frame> vFrames = new Vector<Gif89Frame>();
  3450.  
  3451.         // ----------------------------------------------------------------------------
  3452.         /**
  3453.          * Use this default constructor if you'll be adding multiple frames
  3454.          * constructed from RGB data (i.e., AWT Image objects or ARGB-pixel
  3455.          * arrays).
  3456.          */
  3457.         public Gif89Encoder() {
  3458.             // empty color table puts us into "palette autodetect" mode
  3459.             colorTable = new GifColorTable();
  3460.         }
  3461.  
  3462.         // ----------------------------------------------------------------------------
  3463.         /**
  3464.          * Like the default except that it also adds a single frame, for
  3465.          * conveniently encoding a static GIF from an image.
  3466.          *
  3467.          * @param static_image
  3468.          *            Any Image object that supports pixel-grabbing.
  3469.          * @exception IOException
  3470.          *                See the addFrame() methods.
  3471.          */
  3472.         public Gif89Encoder(Image static_image) throws IOException {
  3473.             this();
  3474.             addFrame(static_image);
  3475.         }
  3476.  
  3477.         // ----------------------------------------------------------------------------
  3478.         /**
  3479.          * This constructor installs a user color table, overriding the
  3480.          * detection of of a palette from ARBG pixels.
  3481.          *
  3482.          * Use of this constructor imposes a couple of restrictions: (1) Frame
  3483.          * objects can't be of type DirectGif89Frame (2) Transparency, if
  3484.          * desired, must be set explicitly.
  3485.          *
  3486.          * @param colors
  3487.          *            Array of color values; no more than 256 colors will be
  3488.          *            read, since that's the limit for a GIF.
  3489.          */
  3490.         public Gif89Encoder(Color[] colors) {
  3491.             colorTable = new GifColorTable(colors);
  3492.         }
  3493.  
  3494.         // ----------------------------------------------------------------------------
  3495.         /**
  3496.          * Convenience constructor for encoding a static GIF from index-model
  3497.          * data. Adds a single frame as specified.
  3498.          *
  3499.          * @param colors
  3500.          *            Array of color values; no more than 256 colors will be
  3501.          *            read, since that's the limit for a GIF.
  3502.          * @param width
  3503.          *            Width of the GIF bitmap.
  3504.          * @param height
  3505.          *            Height of same.
  3506.          * @param ci_pixels
  3507.          *            Array of color-index pixels no less than width * height in
  3508.          *            length.
  3509.          * @exception IOException
  3510.          *                See the addFrame() methods.
  3511.          */
  3512.         public Gif89Encoder(Color[] colors, int width, int height,
  3513.                 byte ci_pixels[]) throws IOException {
  3514.             this(colors);
  3515.             addFrame(width, height, ci_pixels);
  3516.         }
  3517.  
  3518.         // ----------------------------------------------------------------------------
  3519.         /**
  3520.          * Get the number of frames that have been added so far.
  3521.          *
  3522.          * @return Number of frame items.
  3523.          */
  3524.         public int getFrameCount() {
  3525.             return vFrames.size();
  3526.         }
  3527.  
  3528.         // ----------------------------------------------------------------------------
  3529.         /**
  3530.          * Get a reference back to a Gif89Frame object by position.
  3531.          *
  3532.          * @param index
  3533.          *            Zero-based index of the frame in the sequence.
  3534.          * @return Gif89Frame object at the specified position (or null if no
  3535.          *         such frame).
  3536.          */
  3537.         public Gif89Frame getFrameAt(int index) {
  3538.             return isOk(index) ? vFrames.elementAt(index) : null;
  3539.         }
  3540.  
  3541.         // ----------------------------------------------------------------------------
  3542.         /**
  3543.          * Add a Gif89Frame frame to the end of the internal sequence. Note that
  3544.          * there are restrictions on the Gif89Frame type: if the encoder object
  3545.          * was constructed with an explicit color table, an attempt to add a
  3546.          * DirectGif89Frame will throw an exception.
  3547.          *
  3548.          * @param gf
  3549.          *            An externally constructed Gif89Frame.
  3550.          * @exception IOException
  3551.          *                If Gif89Frame can't be accommodated. This could happen
  3552.          *                if either (1) the aggregate cross-frame RGB color
  3553.          *                count exceeds 256, or (2) the Gif89Frame subclass is
  3554.          *                incompatible with the present encoder object.
  3555.          */
  3556.         public void addFrame(Gif89Frame gf) throws IOException {
  3557.             accommodateFrame(gf);
  3558.             vFrames.addElement(gf);
  3559.         }
  3560.  
  3561.         // ----------------------------------------------------------------------------
  3562.         /**
  3563.          * Convenience version of addFrame() that takes a Java Image, internally
  3564.          * constructing the requisite DirectGif89Frame.
  3565.          *
  3566.          * @param image
  3567.          *            Any Image object that supports pixel-grabbing.
  3568.          * @exception IOException
  3569.          *                If either (1) pixel-grabbing fails, (2) the aggregate
  3570.          *                cross-frame RGB color count exceeds 256, or (3) this
  3571.          *                encoder object was constructed with an explicit color
  3572.          *                table.
  3573.          */
  3574.         public void addFrame(Image image) throws IOException {
  3575.             DirectGif89Frame frame = new DirectGif89Frame(image);
  3576.             addFrame(frame);
  3577.         }
  3578.  
  3579.         // ----------------------------------------------------------------------------
  3580.         /**
  3581.          * The index-model convenience version of addFrame().
  3582.          *
  3583.          * @param width
  3584.          *            Width of the GIF bitmap.
  3585.          * @param height
  3586.          *            Height of same.
  3587.          * @param ci_pixels
  3588.          *            Array of color-index pixels no less than width * height in
  3589.          *            length.
  3590.          * @exception IOException
  3591.          *                Actually, in the present implementation, there aren't
  3592.          *                any unchecked exceptions that can be thrown when
  3593.          *                adding an IndexGif89Frame <i>per se</i>. But I might
  3594.          *                add some pedantic check later, to justify the
  3595.          *                generality :)
  3596.          */
  3597.         public void addFrame(int width, int height, byte ci_pixels[])
  3598.                 throws IOException {
  3599.             addFrame(new IndexGif89Frame(width, height, ci_pixels));
  3600.         }
  3601.  
  3602.         // ----------------------------------------------------------------------------
  3603.         /**
  3604.          * Like addFrame() except that the frame is inserted at a specific point
  3605.          * in the sequence rather than appended.
  3606.          *
  3607.          * @param index
  3608.          *            Zero-based index at which to insert frame.
  3609.          * @param gf
  3610.          *            An externally constructed Gif89Frame.
  3611.          * @exception IOException
  3612.          *                If Gif89Frame can't be accommodated. This could happen
  3613.          *                if either (1) the aggregate cross-frame RGB color
  3614.          *                count exceeds 256, or (2) the Gif89Frame subclass is
  3615.          *                incompatible with the present encoder object.
  3616.          */
  3617.         public void insertFrame(int index, Gif89Frame gf) throws IOException {
  3618.             accommodateFrame(gf);
  3619.             vFrames.insertElementAt(gf, index);
  3620.         }
  3621.  
  3622.         // ----------------------------------------------------------------------------
  3623.         /**
  3624.          * Set the color table index for the transparent color, if any.
  3625.          *
  3626.          * @param index
  3627.          *            Index of the color that should be rendered as transparent,
  3628.          *            if any. A value of -1 turns off transparency. (Default:
  3629.          *            -1)
  3630.          */
  3631.         public void setTransparentIndex(int index) {
  3632.             colorTable.setTransparent(index);
  3633.         }
  3634.  
  3635.         // ----------------------------------------------------------------------------
  3636.         /**
  3637.          * Sets attributes of the multi-image display area, if applicable.
  3638.          *
  3639.          * @param dim
  3640.          *            Width/height of display. (Default: largest detected frame
  3641.          *            size)
  3642.          * @param background
  3643.          *            Color table index of background color. (Default: 0)
  3644.          * @see Gif89Frame#setPosition
  3645.          */
  3646.         public void setLogicalDisplay(Dimension dim, int background) {
  3647.             dispDim = new Dimension(dim);
  3648.             bgIndex = background;
  3649.         }
  3650.  
  3651.         // ----------------------------------------------------------------------------
  3652.         /**
  3653.          * Set animation looping parameter, if applicable.
  3654.          *
  3655.          * @param count
  3656.          *            Number of times to play sequence. Special value of 0
  3657.          *            specifies indefinite looping. (Default: 1)
  3658.          */
  3659.         public void setLoopCount(int count) {
  3660.             loopCount = count;
  3661.         }
  3662.  
  3663.         // ----------------------------------------------------------------------------
  3664.         /**
  3665.          * Specify some textual comments to be embedded in GIF.
  3666.          *
  3667.          * @param comments
  3668.          *            String containing ASCII comments.
  3669.          */
  3670.         public void setComments(String comments) {
  3671.             theComments = comments;
  3672.         }
  3673.  
  3674.         // ----------------------------------------------------------------------------
  3675.         /**
  3676.          * A convenience method for setting the "animation speed". It simply
  3677.          * sets the delay parameter for each frame in the sequence to the
  3678.          * supplied value. Since this is actually frame-level rather than
  3679.          * animation-level data, take care to add your frames before calling
  3680.          * this method.
  3681.          *
  3682.          * @param interval
  3683.          *            Interframe interval in centiseconds.
  3684.          */
  3685.         public void setUniformDelay(int interval) {
  3686.             for (int i = 0; i < vFrames.size(); ++i)
  3687.                 vFrames.elementAt(i).setDelay(interval);
  3688.         }
  3689.  
  3690.         // ----------------------------------------------------------------------------
  3691.         /**
  3692.          * After adding your frame(s) and setting your options, simply call this
  3693.          * method to write the GIF to the passed stream. Multiple calls are
  3694.          * permissible if for some reason that is useful to your application.
  3695.          * (The method simply encodes the current state of the object with no
  3696.          * thought to previous calls.)
  3697.          *
  3698.          * @param out
  3699.          *            The stream you want the GIF written to.
  3700.          * @exception IOException
  3701.          *                If a write error is encountered.
  3702.          */
  3703.         public void encode(OutputStream out) throws IOException {
  3704.             int nframes = getFrameCount();
  3705.             boolean is_sequence = nframes > 1;
  3706.  
  3707.             // N.B. must be called before writing screen descriptor
  3708.             colorTable.closePixelProcessing();
  3709.  
  3710.             // write GIF HEADER
  3711.             putAscii("GIF89a", out);
  3712.  
  3713.             // write global blocks
  3714.             writeLogicalScreenDescriptor(out);
  3715.             colorTable.encode(out);
  3716.             if (is_sequence && loopCount != 1)
  3717.                 writeNetscapeExtension(out);
  3718.             if (theComments != null && theComments.length() > 0)
  3719.                 writeCommentExtension(out);
  3720.  
  3721.             // write out the control and rendering data for each frame
  3722.             for (int i = 0; i < nframes; ++i) {
  3723.                 DirectGif89Frame frame = (DirectGif89Frame) vFrames
  3724.                         .elementAt(i);
  3725.                 frame.encode(out, is_sequence, colorTable.getDepth(),
  3726.                         colorTable.getTransparent());
  3727.                 vFrames.set(i, null); // for GC's sake
  3728.                 System.gc();
  3729.             }
  3730.  
  3731.             // write GIF TRAILER
  3732.             out.write((int) ';');
  3733.  
  3734.             out.flush();
  3735.         }
  3736.  
  3737.         public boolean hasStarted = false;
  3738.  
  3739.         // ----------------------------------------------------------------------------
  3740.         /**
  3741.          * After adding your frame(s) and setting your options, simply call this
  3742.          * method to write the GIF to the passed stream. Multiple calls are
  3743.          * permissible if for some reason that is useful to your application.
  3744.          * (The method simply encodes the current state of the object with no
  3745.          * thought to previous calls.)
  3746.          *
  3747.          * @param out
  3748.          *            The stream you want the GIF written to.
  3749.          * @exception IOException
  3750.          *                If a write error is encountered.
  3751.          */
  3752.         public void startEncoding(OutputStream out, Image image, int delay)
  3753.                 throws IOException {
  3754.             hasStarted = true;
  3755.             boolean is_sequence = true;
  3756.             Gif89Frame gf = new DirectGif89Frame(image);
  3757.             accommodateFrame(gf);
  3758.  
  3759.             // N.B. must be called before writing screen descriptor
  3760.             colorTable.closePixelProcessing();
  3761.  
  3762.             // write GIF HEADER
  3763.             putAscii("GIF89a", out);
  3764.  
  3765.             // write global blocks
  3766.             writeLogicalScreenDescriptor(out);
  3767.             colorTable.encode(out);
  3768.             if (is_sequence && loopCount != 1)
  3769.                 writeNetscapeExtension(out);
  3770.             if (theComments != null && theComments.length() > 0)
  3771.                 writeCommentExtension(out);
  3772.         }
  3773.  
  3774.         public void continueEncoding(OutputStream out, Image image, int delay)
  3775.                 throws IOException {
  3776.             // write out the control and rendering data for each frame
  3777.             Gif89Frame gf = new DirectGif89Frame(image);
  3778.             accommodateFrame(gf);
  3779.             gf.encode(out, true, colorTable.getDepth(),
  3780.                     colorTable.getTransparent());
  3781.             out.flush();
  3782.             image.flush();
  3783.         }
  3784.  
  3785.         public void endEncoding(OutputStream out) throws IOException {
  3786.             // write GIF TRAILER
  3787.             out.write((int) ';');
  3788.  
  3789.             out.flush();
  3790.         }
  3791.  
  3792.         public void setBackground(Color color) {
  3793.             bgIndex = colorTable.indexOf(color);
  3794.             if (bgIndex < 0) {
  3795.                 try {
  3796.                     BufferedImage img = new BufferedImage(1, 1,
  3797.                             BufferedImage.TYPE_BYTE_INDEXED);
  3798.                     Graphics g = img.getGraphics();
  3799.                     g.setColor(color);
  3800.                     g.fillRect(0, 0, 2, 2);
  3801.                     DirectGif89Frame frame = new DirectGif89Frame(img);
  3802.                     accommodateFrame(frame);
  3803.                     bgIndex = colorTable.indexOf(color);
  3804.                 } catch (IOException e) {
  3805.                     if (DEBUG)
  3806.                         System.out
  3807.                                 .println("Error while setting background color: "
  3808.                                         + e);
  3809.                 }
  3810.             }
  3811.             if (DEBUG)
  3812.                 System.out.println("Setting bg index to " + bgIndex);
  3813.         }
  3814.  
  3815.         // ----------------------------------------------------------------------------
  3816.         private void accommodateFrame(Gif89Frame gf) throws IOException {
  3817.             dispDim.width = Math.max(dispDim.width, gf.getWidth());
  3818.             dispDim.height = Math.max(dispDim.height, gf.getHeight());
  3819.             colorTable.processPixels(gf);
  3820.         }
  3821.  
  3822.         // ----------------------------------------------------------------------------
  3823.         private void writeLogicalScreenDescriptor(OutputStream os)
  3824.                 throws IOException {
  3825.             putShort(dispDim.width, os);
  3826.             putShort(dispDim.height, os);
  3827.  
  3828.             // write 4 fields, packed into a byte (bitfieldsize:value)
  3829.             // global color map present? (1:1)
  3830.             // bits per primary color less 1 (3:7)
  3831.             // sorted color table? (1:0)
  3832.             // bits per pixel less 1 (3:varies)
  3833.             os.write(0xf0 | colorTable.getDepth() - 1);
  3834.  
  3835.             // write background color index
  3836.             os.write(bgIndex);
  3837.  
  3838.             // Jef Poskanzer's notes on the next field, for our possible
  3839.             // edification:
  3840.             // Pixel aspect ratio - 1:1.
  3841.             // Putbyte( (byte) 49, outs );
  3842.             // Java's GIF reader currently has a bug, if the aspect ratio byte
  3843.             // is
  3844.             // not zero it throws an ImageFormatException. It doesn't know that
  3845.             // 49 means a 1:1 aspect ratio. Well, whatever, zero works with all
  3846.             // the other decoders I've tried so it probably doesn't hurt.
  3847.  
  3848.             // OK, if it's good enough for Jef, it's definitely good enough for
  3849.             // us:
  3850.             os.write(0);
  3851.         }
  3852.  
  3853.         // ----------------------------------------------------------------------------
  3854.         private void writeNetscapeExtension(OutputStream os) throws IOException {
  3855.             // n.b. most software seems to interpret the count as a repeat count
  3856.             // (i.e., interations beyond 1) rather than as an iteration count
  3857.             // (thus, to avoid repeating we have to omit the whole extension)
  3858.  
  3859.             os.write((int) '!'); // GIF Extension Introducer
  3860.             os.write(0xff); // Application Extension Label
  3861.  
  3862.             os.write(11); // application ID block size
  3863.             putAscii("NETSCAPE2.0", os); // application ID data
  3864.  
  3865.             os.write(3); // data sub-block size
  3866.             os.write(1); // a looping flag? dunno
  3867.  
  3868.             // we finally write the relevent data
  3869.             putShort(loopCount > 1 ? loopCount - 1 : 0, os);
  3870.  
  3871.             os.write(0); // block terminator
  3872.         }
  3873.  
  3874.         // ----------------------------------------------------------------------------
  3875.         private void writeCommentExtension(OutputStream os) throws IOException {
  3876.             os.write((int) '!'); // GIF Extension Introducer
  3877.             os.write(0xfe); // Comment Extension Label
  3878.  
  3879.             int remainder = theComments.length() % 255;
  3880.             int nsubblocks_full = theComments.length() / 255;
  3881.             int nsubblocks = nsubblocks_full + (remainder > 0 ? 1 : 0);
  3882.             int ibyte = 0;
  3883.             for (int isb = 0; isb < nsubblocks; ++isb) {
  3884.                 int size = isb < nsubblocks_full ? 255 : remainder;
  3885.  
  3886.                 os.write(size);
  3887.                 putAscii(theComments.substring(ibyte, ibyte + size), os);
  3888.                 ibyte += size;
  3889.             }
  3890.  
  3891.             os.write(0); // block terminator
  3892.         }
  3893.  
  3894.         // ----------------------------------------------------------------------------
  3895.         private boolean isOk(int frame_index) {
  3896.             return frame_index >= 0 && frame_index < vFrames.size();
  3897.         }
  3898.     }
  3899.  
  3900.     // ==============================================================================
  3901.     class GifColorTable {
  3902.  
  3903.         // the palette of ARGB colors, packed as returned by Color.getRGB()
  3904.         private int[] theColors = new int[256];
  3905.  
  3906.         // other basic attributes
  3907.         private int colorDepth;
  3908.         private int transparentIndex = -1;
  3909.  
  3910.         // these fields track color-index info across frames
  3911.         private int ciCount = 0; // count of distinct color indices
  3912.         private ReverseColorMap ciLookup; // cumulative rgb-to-ci lookup table
  3913.  
  3914.         // ----------------------------------------------------------------------------
  3915.         GifColorTable() {
  3916.             ciLookup = new ReverseColorMap(); // puts us into "auto-detect mode"
  3917.         }
  3918.  
  3919.         // ----------------------------------------------------------------------------
  3920.         GifColorTable(Color[] colors) {
  3921.             int n2copy = Math.min(theColors.length, colors.length);
  3922.             for (int i = 0; i < n2copy; ++i)
  3923.                 theColors[i] = colors[i].getRGB();
  3924.         }
  3925.  
  3926.         int indexOf(Color color) {
  3927.             int rgb = color.getRGB();
  3928.             for (int i = 0; i < theColors.length; i++) {
  3929.                 if (rgb == theColors[i]) {
  3930.                     return i;
  3931.                 }
  3932.             }
  3933.             return -1;
  3934.         }
  3935.  
  3936.         // ----------------------------------------------------------------------------
  3937.         int getDepth() {
  3938.             return colorDepth;
  3939.         }
  3940.  
  3941.         // ----------------------------------------------------------------------------
  3942.         int getTransparent() {
  3943.             return transparentIndex;
  3944.         }
  3945.  
  3946.         // ----------------------------------------------------------------------------
  3947.         // default: -1 (no transparency)
  3948.         void setTransparent(int color_index) {
  3949.             transparentIndex = color_index;
  3950.         }
  3951.  
  3952.         // ----------------------------------------------------------------------------
  3953.         void processPixels(Gif89Frame gf) throws IOException {
  3954.             if (gf instanceof DirectGif89Frame)
  3955.                 filterPixels((DirectGif89Frame) gf);
  3956.             else
  3957.                 trackPixelUsage((IndexGif89Frame) gf);
  3958.         }
  3959.  
  3960.         // ----------------------------------------------------------------------------
  3961.         void closePixelProcessing() // must be called before encode()
  3962.         {
  3963.             colorDepth = computeColorDepth(ciCount);
  3964.         }
  3965.  
  3966.         // ----------------------------------------------------------------------------
  3967.         void encode(OutputStream os) throws IOException {
  3968.             // size of palette written is the smallest power of 2 that can
  3969.             // accomdate
  3970.             // the number of RGB colors detected (or largest color index, in
  3971.             // case of
  3972.             // index pixels)
  3973.             int palette_size = 1 << colorDepth;
  3974.             for (int i = 0; i < palette_size; ++i) {
  3975.                 os.write(theColors[i] >> 16 & 0xff);
  3976.                 os.write(theColors[i] >> 8 & 0xff);
  3977.                 os.write(theColors[i] & 0xff);
  3978.             }
  3979.         }
  3980.  
  3981.         // ----------------------------------------------------------------------------
  3982.         // This method accomplishes three things:
  3983.         // (1) converts the passed rgb pixels to indexes into our rgb lookup
  3984.         // table
  3985.         // (2) fills the rgb table as new colors are encountered
  3986.         // (3) looks for transparent pixels so as to set the transparent index
  3987.         // The information is cumulative across multiple calls.
  3988.         //
  3989.         // (Note: some of the logic is borrowed from Jef Poskanzer's code.)
  3990.         // ----------------------------------------------------------------------------
  3991.         private void filterPixels(DirectGif89Frame dgf) throws IOException {
  3992.             if (ciLookup == null)
  3993.                 throw new IOException(
  3994.                         "RGB frames require palette autodetection");
  3995.  
  3996.             int[] argb_pixels = (int[]) dgf.getPixelSource();
  3997.             byte[] ci_pixels = dgf.getPixelSink();
  3998.             int npixels = argb_pixels.length;
  3999.             for (int i = 0; i < npixels; ++i) {
  4000.                 int argb = argb_pixels[i];
  4001.  
  4002.                 // handle transparency
  4003.                 if ((argb >>> 24) < 0x80) // transparent pixel?
  4004.                     if (transparentIndex == -1) // first transparent color
  4005.                                                 // encountered?
  4006.                         transparentIndex = ciCount; // record its index
  4007.                     else if (argb != theColors[transparentIndex]) // different
  4008.                                                                     // pixel
  4009.                                                                     // value?
  4010.                     {
  4011.                         // collapse all transparent pixels into one color index
  4012.                         ci_pixels[i] = (byte) transparentIndex;
  4013.                         continue; // CONTINUE - index already in table
  4014.                     }
  4015.  
  4016.                 // try to look up the index in our "reverse" color table
  4017.                 int color_index = ciLookup.getPaletteIndex(argb & 0xffffff);
  4018.  
  4019.                 if (color_index == -1) // if it isn't in there yet
  4020.                 {
  4021.                     if (ciCount == 256)
  4022.                         throw new IOException(
  4023.                                 "can't encode as GIF (> 256 colors)");
  4024.  
  4025.                     // store color in our accumulating palette
  4026.                     theColors[ciCount] = argb;
  4027.  
  4028.                     // store index in reverse color table
  4029.                     ciLookup.put(argb & 0xffffff, ciCount);
  4030.  
  4031.                     // send color index to our output array
  4032.                     ci_pixels[i] = (byte) ciCount;
  4033.  
  4034.                     // increment count of distinct color indices
  4035.                     ++ciCount;
  4036.                 } else
  4037.                     // we've already snagged color into our palette
  4038.                     ci_pixels[i] = (byte) color_index; // just send filtered
  4039.                                                         // pixel
  4040.             }
  4041.         }
  4042.  
  4043.         // ----------------------------------------------------------------------------
  4044.         private void trackPixelUsage(IndexGif89Frame igf) throws IOException {
  4045.             byte[] ci_pixels = (byte[]) igf.getPixelSource();
  4046.             int npixels = ci_pixels.length;
  4047.             for (int i = 0; i < npixels; ++i)
  4048.                 if (ci_pixels[i] >= ciCount)
  4049.                     ciCount = ci_pixels[i] + 1;
  4050.         }
  4051.  
  4052.         // ----------------------------------------------------------------------------
  4053.         private int computeColorDepth(int colorcount) {
  4054.             // color depth = log-base-2 of maximum number of simultaneous
  4055.             // colors, i.e.
  4056.             // bits per color-index pixel
  4057.             if (colorcount <= 2)
  4058.                 return 1;
  4059.             if (colorcount <= 4)
  4060.                 return 2;
  4061.             if (colorcount <= 16)
  4062.                 return 4;
  4063.             return 8;
  4064.         }
  4065.     }
  4066.  
  4067.     // ==============================================================================
  4068.     // We're doing a very simple linear hashing thing here, which seems
  4069.     // sufficient
  4070.     // for our needs. I make no claims for this approach other than that it
  4071.     // seems
  4072.     // an improvement over doing a brute linear search for each pixel on the one
  4073.     // hand, and creating a Java object for each pixel (if we were to use a Java
  4074.     // Hashtable) on the other. Doubtless my little hash could be improved by
  4075.     // tuning the capacity (at the very least). Suggestions are welcome.
  4076.     // ==============================================================================
  4077.     class ReverseColorMap {
  4078.  
  4079.         private class ColorRecord {
  4080.             int rgb;
  4081.             int ipalette;
  4082.  
  4083.             ColorRecord(int rgb, int ipalette) {
  4084.                 this.rgb = rgb;
  4085.                 this.ipalette = ipalette;
  4086.             }
  4087.         }
  4088.  
  4089.         // I wouldn't really know what a good hashing capacity is, having missed
  4090.         // out
  4091.         // on data structures and algorithms class :) Alls I know is, we've got
  4092.         // a lot
  4093.         // more space than we have time. So let's try a sparse table with a
  4094.         // maximum
  4095.         // load of about 1/8 capacity.
  4096.         private static final int HCAPACITY = 2053; // a nice prime number
  4097.  
  4098.         // our hash table proper
  4099.         private ColorRecord[] hTable = new ColorRecord[HCAPACITY];
  4100.  
  4101.         // ----------------------------------------------------------------------------
  4102.         // Assert: rgb is not negative (which is the same as saying, be sure the
  4103.         // alpha transparency byte - i.e., the high byte - has been masked out).
  4104.         // ----------------------------------------------------------------------------
  4105.         int getPaletteIndex(int rgb) {
  4106.             ColorRecord rec;
  4107.  
  4108.             for (int itable = rgb % hTable.length; (rec = hTable[itable]) != null
  4109.                     && rec.rgb != rgb; itable = ++itable % hTable.length)
  4110.                 ;
  4111.  
  4112.             if (rec != null)
  4113.                 return rec.ipalette;
  4114.  
  4115.             return -1;
  4116.         }
  4117.  
  4118.         // ----------------------------------------------------------------------------
  4119.         // Assert: (1) same as above; (2) rgb key not already present
  4120.         // ----------------------------------------------------------------------------
  4121.         void put(int rgb, int ipalette) {
  4122.             int itable;
  4123.  
  4124.             for (itable = rgb % hTable.length; hTable[itable] != null; itable = ++itable
  4125.                     % hTable.length)
  4126.                 ;
  4127.  
  4128.             hTable[itable] = new ColorRecord(rgb, ipalette);
  4129.         }
  4130.     }
  4131.  
  4132.     // ******************************************************************************
  4133.     // Gif89Frame.java
  4134.     // ******************************************************************************
  4135.  
  4136.     // ==============================================================================
  4137.     /**
  4138.      * First off, just to dispel any doubt, this class and its subclasses have
  4139.      * nothing to do with GUI "frames" such as java.awt.Frame. We merely use the
  4140.      * term in its very common sense of a still picture in an animation
  4141.      * sequence. It's hoped that the restricted context will prevent any
  4142.      * confusion.
  4143.      * <p>
  4144.      * An instance of this class is used in conjunction with a Gif89Encoder
  4145.      * object to represent and encode a single static image and its associated
  4146.      * "control" data. A Gif89Frame doesn't know or care whether it is encoding
  4147.      * one of the many animation frames in a GIF movie, or the single bitmap in
  4148.      * a "normal" GIF. (FYI, this design mirrors the encoded GIF structure.)
  4149.      * <p>
  4150.      * Since Gif89Frame is an abstract class we don't instantiate it directly,
  4151.      * but instead create instances of its concrete subclasses, IndexGif89Frame
  4152.      * and DirectGif89Frame. From the API standpoint, these subclasses differ
  4153.      * only in the sort of data their instances are constructed from. Most folks
  4154.      * will probably work with DirectGif89Frame, since it can be constructed
  4155.      * from a java.awt.Image object, but the lower-level IndexGif89Frame class
  4156.      * offers advantages in specialized circumstances. (Of course, in routine
  4157.      * situations you might not explicitly instantiate any frames at all,
  4158.      * instead letting Gif89Encoder's convenience methods do the honors.)
  4159.      * <p>
  4160.      * As far as the public API is concerned, objects in the Gif89Frame
  4161.      * hierarchy interact with a Gif89Encoder only via the latter's methods for
  4162.      * adding and querying frames. (As a side note, you should know that while
  4163.      * Gif89Encoder objects are permanently modified by the addition of
  4164.      * Gif89Frames, the reverse is NOT true. That is, even though the ultimate
  4165.      * encoding of a Gif89Frame may be affected by the context its parent
  4166.      * encoder object provides, it retains its original condition and can be
  4167.      * reused in a different context.)
  4168.      * <p>
  4169.      * The core pixel-encoding code in this class was essentially lifted from
  4170.      * Jef Poskanzer's well-known <cite>Acme GifEncoder</cite>, so please see
  4171.      * the <a href="../readme.txt">readme</a> containing his notice.
  4172.      *
  4173.      * @version 0.90 beta (15-Jul-2000)
  4174.      * @author J. M. G. Elliott (tep@jmge.net)
  4175.      * @see Gif89Encoder
  4176.      * @see DirectGif89Frame
  4177.      * @see IndexGif89Frame
  4178.      */
  4179.     abstract class Gif89Frame {
  4180.  
  4181.         // // Public "Disposal Mode" constants ////
  4182.  
  4183.         /**
  4184.          * The animated GIF renderer shall decide how to dispose of this
  4185.          * Gif89Frame's display area.
  4186.          *
  4187.          * @see Gif89Frame#setDisposalMode
  4188.          */
  4189.         public static final int DM_UNDEFINED = 0;
  4190.  
  4191.         /**
  4192.          * The animated GIF renderer shall take no display-disposal action.
  4193.          *
  4194.          * @see Gif89Frame#setDisposalMode
  4195.          */
  4196.         public static final int DM_LEAVE = 1;
  4197.  
  4198.         /**
  4199.          * The animated GIF renderer shall replace this Gif89Frame's area with
  4200.          * the background color.
  4201.          *
  4202.          * @see Gif89Frame#setDisposalMode
  4203.          */
  4204.         public static final int DM_BGCOLOR = 2;
  4205.  
  4206.         /**
  4207.          * The animated GIF renderer shall replace this Gif89Frame's area with
  4208.          * the previous frame's bitmap.
  4209.          *
  4210.          * @see Gif89Frame#setDisposalMode
  4211.          */
  4212.         public static final int DM_REVERT = 3;
  4213.  
  4214.         // // Bitmap variables set in package subclass constructors ////
  4215.         int theWidth = -1;
  4216.         int theHeight = -1;
  4217.         byte[] ciPixels;
  4218.  
  4219.         // // GIF graphic frame control options ////
  4220.         private Point thePosition = new Point(0, 0);
  4221.         private boolean isInterlaced;
  4222.         private int csecsDelay;
  4223.         private int disposalCode = DM_LEAVE;
  4224.  
  4225.         // ----------------------------------------------------------------------------
  4226.         /**
  4227.          * Set the position of this frame within a larger animation display
  4228.          * space.
  4229.          *
  4230.          * @param p
  4231.          *            Coordinates of the frame's upper left corner in the
  4232.          *            display space. (Default: The logical display's origin [0,
  4233.          *            0])
  4234.          * @see Gif89Encoder#setLogicalDisplay
  4235.          */
  4236.         public void setPosition(Point p) {
  4237.             thePosition = new Point(p);
  4238.         }
  4239.  
  4240.         // ----------------------------------------------------------------------------
  4241.         /**
  4242.          * Set or clear the interlace flag.
  4243.          *
  4244.          * @param b
  4245.          *            true if you want interlacing. (Default: false)
  4246.          */
  4247.         public void setInterlaced(boolean b) {
  4248.             isInterlaced = b;
  4249.         }
  4250.  
  4251.         // ----------------------------------------------------------------------------
  4252.         /**
  4253.          * Set the between-frame interval.
  4254.          *
  4255.          * @param interval
  4256.          *            Centiseconds to wait before displaying the subsequent
  4257.          *            frame. (Default: 0)
  4258.          */
  4259.         public void setDelay(int interval) {
  4260.             csecsDelay = interval;
  4261.         }
  4262.  
  4263.         // ----------------------------------------------------------------------------
  4264.         /**
  4265.          * Setting this option determines (in a cooperative GIF-viewer) what
  4266.          * will be done with this frame's display area before the subsequent
  4267.          * frame is displayed. For instance, a setting of DM_BGCOLOR can be used
  4268.          * for erasure when redrawing with displacement.
  4269.          *
  4270.          * @param code
  4271.          *            One of the four int constants of the Gif89Frame.DM_*
  4272.          *            series. (Default: DM_LEAVE)
  4273.          */
  4274.         public void setDisposalMode(int code) {
  4275.             disposalCode = code;
  4276.         }
  4277.  
  4278.         // ----------------------------------------------------------------------------
  4279.         Gif89Frame() {
  4280.         } // package-visible default constructor
  4281.  
  4282.         // ----------------------------------------------------------------------------
  4283.         abstract Object getPixelSource();
  4284.  
  4285.         // ----------------------------------------------------------------------------
  4286.         int getWidth() {
  4287.             return theWidth;
  4288.         }
  4289.  
  4290.         // ----------------------------------------------------------------------------
  4291.         int getHeight() {
  4292.             return theHeight;
  4293.         }
  4294.  
  4295.         // ----------------------------------------------------------------------------
  4296.         byte[] getPixelSink() {
  4297.             return ciPixels;
  4298.         }
  4299.  
  4300.         // ----------------------------------------------------------------------------
  4301.         void encode(OutputStream os, boolean epluribus, int color_depth,
  4302.                 int transparent_index) throws IOException {
  4303.             writeGraphicControlExtension(os, epluribus, transparent_index);
  4304.             writeImageDescriptor(os);
  4305.             new GifPixelsEncoder(theWidth, theHeight, ciPixels, isInterlaced,
  4306.                     color_depth).encode(os);
  4307.         }
  4308.  
  4309.         // ----------------------------------------------------------------------------
  4310.         private void writeGraphicControlExtension(OutputStream os,
  4311.                 boolean epluribus, int itransparent) throws IOException {
  4312.             int transflag = itransparent == -1 ? 0 : 1;
  4313.             if (transflag == 1 || epluribus) // using transparency or animating
  4314.                                                 // ?
  4315.             {
  4316.                 os.write((int) '!'); // GIF Extension Introducer
  4317.                 os.write(0xf9); // Graphic Control Label
  4318.                 os.write(4); // subsequent data block size
  4319.                 os.write((disposalCode << 2) | transflag); // packed fields (1
  4320.                                                             // byte)
  4321.                 putShort(csecsDelay, os); // delay field (2 bytes)
  4322.                 os.write(itransparent); // transparent index field
  4323.                 os.write(0); // block terminator
  4324.             }
  4325.         }
  4326.  
  4327.         // ----------------------------------------------------------------------------
  4328.         private void writeImageDescriptor(OutputStream os) throws IOException {
  4329.             os.write((int) ','); // Image Separator
  4330.             putShort(thePosition.x, os);
  4331.             putShort(thePosition.y, os);
  4332.             putShort(theWidth, os);
  4333.             putShort(theHeight, os);
  4334.             os.write(isInterlaced ? 0x40 : 0); // packed fields (1 byte)
  4335.         }
  4336.     }
  4337.  
  4338.     // ==============================================================================
  4339.     class GifPixelsEncoder {
  4340.  
  4341.         private static final int EOF = -1;
  4342.  
  4343.         private int imgW, imgH;
  4344.         private byte[] pixAry;
  4345.         private boolean wantInterlaced;
  4346.         private int initCodeSize;
  4347.  
  4348.         // raster data navigators
  4349.         private int countDown;
  4350.         private int xCur, yCur;
  4351.         private int curPass;
  4352.  
  4353.         // ----------------------------------------------------------------------------
  4354.         GifPixelsEncoder(int width, int height, byte[] pixels,
  4355.                 boolean interlaced, int color_depth) {
  4356.             imgW = width;
  4357.             imgH = height;
  4358.             pixAry = pixels;
  4359.             wantInterlaced = interlaced;
  4360.             initCodeSize = Math.max(2, color_depth);
  4361.         }
  4362.  
  4363.         // ----------------------------------------------------------------------------
  4364.         void encode(OutputStream os) throws IOException {
  4365.             os.write(initCodeSize); // write "initial code size" byte
  4366.  
  4367.             countDown = imgW * imgH; // reset navigation variables
  4368.             xCur = yCur = curPass = 0;
  4369.  
  4370.             compress(initCodeSize + 1, os); // compress and write the pixel data
  4371.  
  4372.             os.write(0); // write block terminator
  4373.         }
  4374.  
  4375.         // ****************************************************************************
  4376.         // (J.E.) The logic of the next two methods is largely intact from
  4377.         // Jef Poskanzer. Some stylistic changes were made for consistency sake,
  4378.         // plus the second method accesses the pixel value from a prefiltered
  4379.         // linear
  4380.         // array. That's about it.
  4381.         // ****************************************************************************
  4382.  
  4383.         // ----------------------------------------------------------------------------
  4384.         // Bump the 'xCur' and 'yCur' to point to the next pixel.
  4385.         // ----------------------------------------------------------------------------
  4386.         private void bumpPosition() {
  4387.             // Bump the current X position
  4388.             ++xCur;
  4389.  
  4390.             // If we are at the end of a scan line, set xCur back to the
  4391.             // beginning
  4392.             // If we are interlaced, bump the yCur to the appropriate spot,
  4393.             // otherwise, just increment it.
  4394.             if (xCur == imgW) {
  4395.                 xCur = 0;
  4396.  
  4397.                 if (!wantInterlaced)
  4398.                     ++yCur;
  4399.                 else
  4400.                     switch (curPass) {
  4401.                     case 0:
  4402.                         yCur += 8;
  4403.                         if (yCur >= imgH) {
  4404.                             ++curPass;
  4405.                             yCur = 4;
  4406.                         }
  4407.                         break;
  4408.                     case 1:
  4409.                         yCur += 8;
  4410.                         if (yCur >= imgH) {
  4411.                             ++curPass;
  4412.                             yCur = 2;
  4413.                         }
  4414.                         break;
  4415.                     case 2:
  4416.                         yCur += 4;
  4417.                         if (yCur >= imgH) {
  4418.                             ++curPass;
  4419.                             yCur = 1;
  4420.                         }
  4421.                         break;
  4422.                     case 3:
  4423.                         yCur += 2;
  4424.                         break;
  4425.                     }
  4426.             }
  4427.         }
  4428.  
  4429.         // ----------------------------------------------------------------------------
  4430.         // Return the next pixel from the image
  4431.         // ----------------------------------------------------------------------------
  4432.         private int nextPixel() {
  4433.             if (countDown == 0)
  4434.                 return EOF;
  4435.  
  4436.             --countDown;
  4437.  
  4438.             byte pix = pixAry[yCur * imgW + xCur];
  4439.  
  4440.             bumpPosition();
  4441.  
  4442.             return pix & 0xff;
  4443.         }
  4444.  
  4445.         // ****************************************************************************
  4446.         // (J.E.) I didn't touch Jef Poskanzer's code from this point on. (Well,
  4447.         // OK,
  4448.         // I changed the name of the sole outside method it accesses.) I figure
  4449.         // if I have no idea how something works, I shouldn't play with it :)
  4450.         //
  4451.         // Despite its unencapsulated structure, this section is actually highly
  4452.         // self-contained. The calling code merely calls compress(), and the
  4453.         // present
  4454.         // code calls nextPixel() in the caller. That's the sum total of their
  4455.         // communication. I could have dumped it in a separate class with a
  4456.         // callback
  4457.         // via an interface, but it didn't seem worth messing with.
  4458.         // ****************************************************************************
  4459.  
  4460.         // GIFCOMPR.C - GIF Image compression routines
  4461.         //
  4462.         // Lempel-Ziv compression based on 'compress'. GIF modifications by
  4463.         // David Rowley (mgardi@watdcsu.waterloo.edu)
  4464.  
  4465.         // General DEFINEs
  4466.  
  4467.         static final int BITS = 12;
  4468.  
  4469.         static final int HSIZE = 5003; // 80% occupancy
  4470.  
  4471.         // GIF Image compression - modified 'compress'
  4472.         //
  4473.         // Based on: compress.c - File compression ala IEEE Computer, June 1984.
  4474.         //
  4475.         // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
  4476.         // Jim McKie (decvax!mcvax!jim)
  4477.         // Steve Davies (decvax!vax135!petsd!peora!srd)
  4478.         // Ken Turkowski (decvax!decwrl!turtlevax!ken)
  4479.         // James A. Woods (decvax!ihnp4!ames!jaw)
  4480.         // Joe Orost (decvax!vax135!petsd!joe)
  4481.  
  4482.         int n_bits; // number of bits/code
  4483.         int maxbits = BITS; // user settable max # bits/code
  4484.         int maxcode; // maximum code, given n_bits
  4485.         int maxmaxcode = 1 << BITS; // should NEVER generate this code
  4486.  
  4487.         final int MAXCODE(int n_bits) {
  4488.             return (1 << n_bits) - 1;
  4489.         }
  4490.  
  4491.         int[] htab = new int[HSIZE];
  4492.         int[] codetab = new int[HSIZE];
  4493.  
  4494.         int hsize = HSIZE; // for dynamic table sizing
  4495.  
  4496.         int free_ent = 0; // first unused entry
  4497.  
  4498.         // block compression parameters -- after all codes are used up,
  4499.         // and compression rate changes, start over.
  4500.         boolean clear_flg = false;
  4501.  
  4502.         // Algorithm: use open addressing double hashing (no chaining) on the
  4503.         // prefix code / next character combination. We do a variant of Knuth's
  4504.         // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
  4505.         // secondary probe. Here, the modular division first probe is gives way
  4506.         // to a faster exclusive-or manipulation. Also do block compression with
  4507.         // an adaptive reset, whereby the code table is cleared when the
  4508.         // compression
  4509.         // ratio decreases, but after the table fills. The variable-length
  4510.         // output
  4511.         // codes are re-sized at this point, and a special CLEAR code is
  4512.         // generated
  4513.         // for the decompressor. Late addition: construct the table according to
  4514.         // file size for noticeable speed improvement on small files. Please
  4515.         // direct
  4516.         // questions about this implementation to ames!jaw.
  4517.  
  4518.         int g_init_bits;
  4519.  
  4520.         int ClearCode;
  4521.         int EOFCode;
  4522.  
  4523.         void compress(int init_bits, OutputStream outs) throws IOException {
  4524.             int fcode;
  4525.             int i /* = 0 */;
  4526.             int c;
  4527.             int ent;
  4528.             int disp;
  4529.             int hsize_reg;
  4530.             int hshift;
  4531.  
  4532.             // Set up the globals: g_init_bits - initial number of bits
  4533.             g_init_bits = init_bits;
  4534.  
  4535.             // Set up the necessary values
  4536.             clear_flg = false;
  4537.             n_bits = g_init_bits;
  4538.             maxcode = MAXCODE(n_bits);
  4539.  
  4540.             ClearCode = 1 << (init_bits - 1);
  4541.             EOFCode = ClearCode + 1;
  4542.             free_ent = ClearCode + 2;
  4543.  
  4544.             char_init();
  4545.  
  4546.             ent = nextPixel();
  4547.  
  4548.             hshift = 0;
  4549.             for (fcode = hsize; fcode < 65536; fcode *= 2)
  4550.                 ++hshift;
  4551.             hshift = 8 - hshift; // set hash code range bound
  4552.  
  4553.             hsize_reg = hsize;
  4554.             cl_hash(hsize_reg); // clear hash table
  4555.  
  4556.             output(ClearCode, outs);
  4557.  
  4558.             outer_loop: while ((c = nextPixel()) != EOF) {
  4559.                 fcode = (c << maxbits) + ent;
  4560.                 i = (c << hshift) ^ ent; // xor hashing
  4561.  
  4562.                 if (htab[i] == fcode) {
  4563.                     ent = codetab[i];
  4564.                     continue;
  4565.                 } else if (htab[i] >= 0) // non-empty slot
  4566.                 {
  4567.                     disp = hsize_reg - i; // secondary hash (after G. Knott)
  4568.                     if (i == 0)
  4569.                         disp = 1;
  4570.                     do {
  4571.                         if ((i -= disp) < 0)
  4572.                             i += hsize_reg;
  4573.  
  4574.                         if (htab[i] == fcode) {
  4575.                             ent = codetab[i];
  4576.                             continue outer_loop;
  4577.                         }
  4578.                     } while (htab[i] >= 0);
  4579.                 }
  4580.                 output(ent, outs);
  4581.                 ent = c;
  4582.                 if (free_ent < maxmaxcode) {
  4583.                     codetab[i] = free_ent++; // code -> hashtable
  4584.                     htab[i] = fcode;
  4585.                 } else
  4586.                     cl_block(outs);
  4587.             }
  4588.             // Put out the final code.
  4589.             output(ent, outs);
  4590.             output(EOFCode, outs);
  4591.         }
  4592.  
  4593.         // output
  4594.         //
  4595.         // Output the given code.
  4596.         // Inputs:
  4597.         // code: A n_bits-bit integer. If == -1, then EOF. This assumes
  4598.         // that n_bits =< wordsize - 1.
  4599.         // Outputs:
  4600.         // Outputs code to the file.
  4601.         // Assumptions:
  4602.         // Chars are 8 bits long.
  4603.         // Algorithm:
  4604.         // Maintain a BITS character long buffer (so that 8 codes will
  4605.         // fit in it exactly). Use the VAX insv instruction to insert each
  4606.         // code in turn. When the buffer fills up empty it and start over.
  4607.  
  4608.         int cur_accum = 0;
  4609.         int cur_bits = 0;
  4610.  
  4611.         int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F,
  4612.                 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF,
  4613.                 0x7FFF, 0xFFFF };
  4614.  
  4615.         void output(int code, OutputStream outs) throws IOException {
  4616.             cur_accum &= masks[cur_bits];
  4617.  
  4618.             if (cur_bits > 0)
  4619.                 cur_accum |= (code << cur_bits);
  4620.             else
  4621.                 cur_accum = code;
  4622.  
  4623.             cur_bits += n_bits;
  4624.  
  4625.             while (cur_bits >= 8) {
  4626.                 char_out((byte) (cur_accum & 0xff), outs);
  4627.                 cur_accum >>= 8;
  4628.                 cur_bits -= 8;
  4629.             }
  4630.  
  4631.             // If the next entry is going to be too big for the code size,
  4632.             // then increase it, if possible.
  4633.             if (free_ent > maxcode || clear_flg) {
  4634.                 if (clear_flg) {
  4635.                     maxcode = MAXCODE(n_bits = g_init_bits);
  4636.                     clear_flg = false;
  4637.                 } else {
  4638.                     ++n_bits;
  4639.                     if (n_bits == maxbits)
  4640.                         maxcode = maxmaxcode;
  4641.                     else
  4642.                         maxcode = MAXCODE(n_bits);
  4643.                 }
  4644.             }
  4645.  
  4646.             if (code == EOFCode) {
  4647.                 // At EOF, write the rest of the buffer.
  4648.                 while (cur_bits > 0) {
  4649.                     char_out((byte) (cur_accum & 0xff), outs);
  4650.                     cur_accum >>= 8;
  4651.                     cur_bits -= 8;
  4652.                 }
  4653.  
  4654.                 flush_char(outs);
  4655.             }
  4656.         }
  4657.  
  4658.         // Clear out the hash table
  4659.  
  4660.         // table clear for block compress
  4661.         void cl_block(OutputStream outs) throws IOException {
  4662.             cl_hash(hsize);
  4663.             free_ent = ClearCode + 2;
  4664.             clear_flg = true;
  4665.  
  4666.             output(ClearCode, outs);
  4667.         }
  4668.  
  4669.         // reset code table
  4670.         void cl_hash(int hsize) {
  4671.             for (int i = 0; i < hsize; ++i)
  4672.                 htab[i] = -1;
  4673.         }
  4674.  
  4675.         // GIF Specific routines
  4676.  
  4677.         // Number of characters so far in this 'packet'
  4678.         int a_count;
  4679.  
  4680.         // Set up the 'byte output' routine
  4681.         void char_init() {
  4682.             a_count = 0;
  4683.         }
  4684.  
  4685.         // Define the storage for the packet accumulator
  4686.         byte[] accum = new byte[256];
  4687.  
  4688.         // Add a character to the end of the current packet, and if it is 254
  4689.         // characters, flush the packet to disk.
  4690.         void char_out(byte c, OutputStream outs) throws IOException {
  4691.             accum[a_count++] = c;
  4692.             if (a_count >= 254)
  4693.                 flush_char(outs);
  4694.         }
  4695.  
  4696.         // Flush the packet to disk, and reset the accumulator
  4697.         void flush_char(OutputStream outs) throws IOException {
  4698.             if (a_count > 0) {
  4699.                 outs.write(a_count);
  4700.                 outs.write(accum, 0, a_count);
  4701.                 a_count = 0;
  4702.             }
  4703.         }
  4704.     }
  4705.  
  4706.     // ******************************************************************************
  4707.     // IndexGif89Frame.java
  4708.     // ******************************************************************************
  4709.  
  4710.     // ==============================================================================
  4711.     /**
  4712.      * Instances of this Gif89Frame subclass are constructed from bitmaps in the
  4713.      * form of color-index pixels, which accords with a GIF's native palettized
  4714.      * color model. The class is useful when complete control over a GIF's color
  4715.      * palette is desired. It is also much more efficient when one is using an
  4716.      * algorithmic frame generator that isn't interested in RGB values (such as
  4717.      * a cellular automaton).
  4718.      * <p>
  4719.      * Objects of this class are normally added to a Gif89Encoder object that
  4720.      * has been provided with an explicit color table at construction. While you
  4721.      * may also add them to "auto-map" encoders without an exception being
  4722.      * thrown, there obviously must be at least one DirectGif89Frame object in
  4723.      * the sequence so that a color table may be detected.
  4724.      *
  4725.      * @version 0.90 beta (15-Jul-2000)
  4726.      * @author J. M. G. Elliott (tep@jmge.net)
  4727.      * @see Gif89Encoder
  4728.      * @see Gif89Frame
  4729.      * @see DirectGif89Frame
  4730.      */
  4731.     class IndexGif89Frame extends Gif89Frame {
  4732.  
  4733.         // ----------------------------------------------------------------------------
  4734.         /**
  4735.          * Construct a IndexGif89Frame from color-index pixel data.
  4736.          *
  4737.          * @param width
  4738.          *            Width of the bitmap.
  4739.          * @param height
  4740.          *            Height of the bitmap.
  4741.          * @param ci_pixels
  4742.          *            Array containing at least width*height color-index pixels.
  4743.          */
  4744.         public IndexGif89Frame(int width, int height, byte ci_pixels[]) {
  4745.             theWidth = width;
  4746.             theHeight = height;
  4747.             ciPixels = new byte[theWidth * theHeight];
  4748.             System.arraycopy(ci_pixels, 0, ciPixels, 0, ciPixels.length);
  4749.         }
  4750.  
  4751.         // ----------------------------------------------------------------------------
  4752.         Object getPixelSource() {
  4753.             return ciPixels;
  4754.         }
  4755.     }
  4756.  
  4757.     // ----------------------------------------------------------------------------
  4758.     /**
  4759.      * Internal method;
  4760.      * write just the low bytes of a String. (This sucks, but the concept of an
  4761.      * encoding seems inapplicable to a binary file ID string. I would think
  4762.      * flexibility is just what we don't want - but then again, maybe I'm slow.)
  4763.      * This is an internal method not meant to be called by clients.
  4764.      */
  4765.     private static void putAscii(String s, OutputStream os) throws IOException {
  4766.         byte[] bytes = new byte[s.length()];
  4767.         for (int i = 0; i < bytes.length; ++i) {
  4768.             bytes[i] = (byte) s.charAt(i); // discard the high byte
  4769.         }
  4770.         os.write(bytes);
  4771.     }
  4772.  
  4773.     // ----------------------------------------------------------------------------
  4774.     /**
  4775.      * Internal method;
  4776.      * write a 16-bit integer in little endian byte order.
  4777.      * This is an internal method not meant to be called by clients.
  4778.      */
  4779.     private static void putShort(int i16, OutputStream os) throws IOException {
  4780.         os.write(i16 & 0xff);
  4781.         os.write(i16 >> 8 & 0xff);
  4782.     }
  4783. }

Raw Paste