/* Modified by Stuart Reges 4/9/15 to include a DebuggingGraphics inner class that keeps track of how many times various drawing methods are called. It includes a showCounts method for the DrawingPanel itself that allows a client to examine this. The panel will record basic drawing methods performed by a version of the Graphics class obtained by calling getDebugging Graphics: Graphics g = panel.getDebuggingGraphics(); Novices will be encouraged to simply print it at the end of main, as in: System.out.println(panel.getCounts()); All additions are commented (search for "DebuggingGraphics") */ /** The DrawingPanel class provides a simple interface for drawing persistent images using a Graphics object. An internal BufferedImage object is used to keep track of what has been drawn. A client of the class simply constructs a DrawingPanel of a particular size and then draws on it with the Graphics object, setting the background color if they so choose.

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

This version of DrawingPanel also saves animated GIFs, though this is kind of hit-and-miss because animated GIFs are pretty sucky (256 color limit, large file size, etc).

Recent features: - save zoomed images (2011/10/25) - window no longer moves when zoom changes (2011/10/25) - grid lines (2011/10/11) @author Marty Stepp @version October 21, 2011 */ // new imports to support DebuggingGraphics class import java.awt.FontMetrics; import java.awt.Rectangle; import java.awt.Shape; import java.awt.image.ImageObserver; import java.text.AttributedCharacterIterator; import java.util.Collections; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Composite; import java.awt.Container; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.Image; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.image.BufferedImage; import java.awt.image.PixelGrabber; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.lang.Exception; import java.lang.Integer; import java.lang.InterruptedException; import java.lang.Math; import java.lang.Object; import java.lang.OutOfMemoryError; import java.lang.SecurityException; import java.lang.String; import java.lang.System; import java.lang.Thread; import java.net.URL; import java.net.NoRouteToHostException; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.TreeMap; import java.util.Vector; import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JCheckBoxMenuItem; import javax.swing.JColorChooser; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MouseInputListener; import javax.swing.filechooser.FileFilter; public final class DrawingPanel extends FileFilter implements ActionListener, MouseMotionListener, Runnable, WindowListener { // inner class to represent one frame of an animated GIF private static class ImageFrame { public Image image; public int delay; public ImageFrame(Image image, int delay) { this.image = image; this.delay = delay / 10; // strangely, gif stores delay as sec/100 } } // inner class that passes through calls to the panel's Graphics object g2 // but that also records a count of how many times various basic drawing // methods are called. Notice that it extends Graphics and not Graphics2D, // so it is more limited than g2. private class DebuggingGraphics extends Graphics { public Graphics create() {return g2.create();} public void translate(int x, int y) { g2.translate(x, y); } public Color getColor() {return g2.getColor();} public void setPaintMode() { g2.setPaintMode(); } public void setXORMode(Color c1) { g2.setXORMode(c1); } public Font getFont() {return g2.getFont();} public void setFont(Font font) { g2.setFont(font); } public FontMetrics getFontMetrics(Font f) {return g2.getFontMetrics();} public Rectangle getClipBounds() {return g2.getClipBounds();} public void clipRect(int x, int y, int width, int height) { g2.clipRect(x, y, width, height); } public void setClip(int x, int y, int width, int height) { g2.setClip(x, y, width, height); } public Shape getClip() {return g2.getClip();} public void setClip(Shape clip) { g2.setClip(clip); } public void copyArea(int x, int y, int width, int height, int dx, int dy) { g2.copyArea(x, y, width, height, dx, dy); } public void clearRect(int x, int y, int width, int height) { g2.clearRect(x, y, width, height); } public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { g2.drawRoundRect(x, y, width, height, arcWidth, arcHeight); } public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { g2.fillRoundRect(x, y, width, height, arcWidth, arcHeight); } public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { g2.drawArc(x, y, width, height, startAngle, arcAngle); } public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { g2.fillArc(x, y, width, height, startAngle, arcAngle); } public void drawPolyline(int xPoints[], int yPoints[], int nPoints) { g2.drawPolyline(xPoints, yPoints, nPoints); } public void drawPolygon(int xPoints[], int yPoints[], int nPoints) { g2.drawPolygon(xPoints, yPoints, nPoints); } public void fillPolygon(int xPoints[], int yPoints[], int nPoints) { g2.fillPolygon(xPoints, yPoints, nPoints); } public void drawString(AttributedCharacterIterator iterator, int x, int y) { g2.drawString(iterator, x, y); } public boolean drawImage(Image img, int x, int y, ImageObserver observer) { return g2.drawImage(img, x, y, observer);}; public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { return g2.drawImage(img, x, y, width, height, observer);}; public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { return g2.drawImage(img, x, y, bgcolor, observer);}; public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) { return g2.drawImage(img, x, y, width, height, bgcolor, observer);} public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { return g2.drawImage(img, dx1, dy1, dx2, dy2, sx1, dy1, dx2, sy2, observer);} public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) { return g2.drawImage(img, dx1, dy1, dx2, dy2, sx1, dy1, sx2, sy2, bgcolor, observer);} public void dispose() { g2.dispose(); } public void drawOval(int x, int y, int width, int height) { g2.drawOval(x, y, width, height); recordString("drawOval"); } public void fillOval(int x, int y, int width, int height) { g2.fillOval(x, y, width, height); recordString("fillOval"); } public void drawString(String str, int x, int y) { g2.drawString(str, x, y); recordString("drawString"); } public void drawLine(int x1, int y1, int x2, int y2) { g2.drawLine(x1, y1, x2, y2); recordString("drawLine"); } public void fillRect(int x, int y, int width, int height) { g2.fillRect(x, y, width, height); recordString("fillRect"); } public void drawRect(int x, int y, int width, int height) { g2.drawRect(x, y, width, height); recordString("drawRect"); } public void setColor(Color c) { g2.setColor(c); // recordString("setColor"); } public void recordString(String s) { if (!counts.containsKey(s)) { counts.put(s, 1); } else { counts.put(s, counts.get(s) + 1); } } } // class constants public static final String ANIMATED_PROPERTY = "drawingpanel.animated"; public static final String AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY = "drawingpanel.animateonsleep"; public static final String DIFF_PROPERTY = "drawingpanel.diff"; public static final String HEADLESS_PROPERTY = "drawingpanel.headless"; public static final String MULTIPLE_PROPERTY = "drawingpanel.multiple"; public static final String SAVE_PROPERTY = "drawingpanel.save"; public static final String ANIMATION_FILE_NAME = "_drawingpanel_animation_save.txt"; private static final String TITLE = "Drawing Panel"; private static final String COURSE_WEB_SITE = "http://courses.cs.washington.edu/courses/cse142/CurrentQtr/drawingpanel.txt"; private static final Color GRID_LINE_COLOR = new Color(64, 64, 64, 128); private static final int GRID_SIZE = 10; // 10px between grid lines private static final int DELAY = 100; // delay between repaints in millis private static final int MAX_FRAMES = 100; // max animation frames private static final int MAX_SIZE = 10000; // max width/height private static final boolean DEBUG = false; private static final boolean SAVE_SCALED_IMAGES = true; // if true, when panel is zoomed, saves images at that zoom factor private static int instances = 0; private static Thread shutdownThread = null; private static void checkAnimationSettings() { try { File settingsFile = new File(ANIMATION_FILE_NAME); if (settingsFile.exists()) { Scanner input = new Scanner(settingsFile); String animationSaveFileName = input.nextLine(); input.close(); // *** TODO: delete the file System.out.println("***"); System.out.println("*** DrawingPanel saving animated GIF: " + new File(animationSaveFileName).getName()); System.out.println("***"); settingsFile.delete(); System.setProperty(ANIMATED_PROPERTY, "1"); System.setProperty(SAVE_PROPERTY, animationSaveFileName); } } catch (Exception e) { if (DEBUG) { System.out.println("error checking animation settings: " + e); } } } private static boolean hasProperty(String name) { try { return System.getProperty(name) != null; } catch (SecurityException e) { if (DEBUG) System.out.println("Security exception when trying to read " + name); return false; } } private static boolean propertyIsTrue(String name) { try { String prop = System.getProperty(name); return prop != null && (prop.equalsIgnoreCase("true") || prop.equalsIgnoreCase("yes") || prop.equalsIgnoreCase("1")); } catch (SecurityException e) { if (DEBUG) System.out.println("Security exception when trying to read " + name); return false; } } /* private static boolean propertyIsFalse(String name) { try { String prop = System.getProperty(name); return prop != null && (prop.equalsIgnoreCase("false") || prop.equalsIgnoreCase("no") || prop.equalsIgnoreCase("0")); } catch (SecurityException e) { if (DEBUG) System.out.println("Security exception when trying to read " + name); return false; } } */ // Returns whether the 'main' thread is still running. private static boolean mainIsActive() { ThreadGroup group = Thread.currentThread().getThreadGroup(); int activeCount = group.activeCount(); // look for the main thread in the current thread group Thread[] threads = new Thread[activeCount]; group.enumerate(threads); for (int i = 0; i < threads.length; i++) { Thread thread = threads[i]; String name = ("" + thread.getName()).toLowerCase(); if (name.indexOf("main") >= 0 || name.indexOf("testrunner-assignmentrunner") >= 0) { // found main thread! // (TestRunnerApplet's main runner also counts as "main" thread) return thread.isAlive(); } } // didn't find a running main thread; guess that main is done running return false; } private static boolean usingDrJava() { try { return System.getProperty("drjava.debug.port") != null || System.getProperty("java.class.path").toLowerCase().indexOf("drjava") >= 0; } catch (SecurityException e) { // running as an applet, or something return false; } } private class ImagePanel extends JPanel { private static final long serialVersionUID = 0; private Image image; public ImagePanel(Image image) { setImage(image); setBackground(Color.WHITE); setPreferredSize(new Dimension(image.getWidth(this), image.getHeight(this))); setAlignmentX(0.0f); } public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; if (currentZoom != 1) { g2.scale(currentZoom, currentZoom); } g2.drawImage(image, 0, 0, this); // possibly draw grid lines for debugging if (gridLines) { g2.setPaint(GRID_LINE_COLOR); for (int row = 1; row <= getHeight() / GRID_SIZE; row++) { g2.drawLine(0, row * GRID_SIZE, getWidth(), row * GRID_SIZE); } for (int col = 1; col <= getWidth() / GRID_SIZE; col++) { g2.drawLine(col * GRID_SIZE, 0, col * GRID_SIZE, getHeight()); } } } public void setImage(Image image) { this.image = image; repaint(); } } // fields private int width, height; // dimensions of window frame private JFrame frame; // overall window frame private JPanel panel; // overall drawing surface private ImagePanel imagePanel; // real drawing surface private BufferedImage image; // remembers drawing commands private Graphics2D g2; // graphics context for painting private JLabel statusBar; // status bar showing mouse position private JFileChooser chooser; // file chooser to save files private long createTime; // time at which DrawingPanel was constructed private Timer timer; // animation timer private ArrayList frames; // stores frames of animation to save private Gif89Encoder encoder; // private FileOutputStream stream; private Color backgroundColor = Color.WHITE; private String callingClassName; // name of class that constructed this panel private boolean animated = false; // changes to true if sleep() is called private boolean PRETTY = true; // true to anti-alias private boolean gridLines = false; private int instanceNumber; private int currentZoom = 1; private int initialPixel; // initial value in each pixel, for clear() // new fields to support DebuggingGraphics private Map counts; private Graphics g3; // construct a drawing panel of given width and height enclosed in a window public DrawingPanel(int width, int height) { if (width < 0 || width > MAX_SIZE || height < 0 || height > MAX_SIZE) { throw new IllegalArgumentException("Illegal width/height: " + width + " x " + height); } checkAnimationSettings(); synchronized (getClass()) { instances++; instanceNumber = instances; // each DrawingPanel stores its own int number if (shutdownThread == null && !usingDrJava()) { shutdownThread = new Thread(new Runnable() { // Runnable implementation; used for shutdown thread. public void run() { try { while (true) { // maybe shut down the program, if no more DrawingPanels are onscreen // and main has finished executing if ((instances == 0 || shouldSave()) && !mainIsActive()) { try { System.exit(0); } catch (SecurityException sex) {} } Thread.sleep(250); } } catch (Exception e) {} } }); shutdownThread.setPriority(Thread.MIN_PRIORITY); shutdownThread.start(); } } this.width = width; this.height = height; if (DEBUG) System.out.println("w=" + width + ",h=" + height + ",anim=" + isAnimated() + ",graph=" + isGraphical() + ",save=" + shouldSave()); if (isAnimated() && shouldSave()) { // image must be no more than 256 colors image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); // image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); PRETTY = false; // turn off anti-aliasing to save palette colors // initially fill the entire frame with the background color, // because it won't show through via transparency like with a full ARGB image Graphics g = image.getGraphics(); g.setColor(backgroundColor); g.fillRect(0, 0, width + 1, height + 1); } else { image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } initialPixel = image.getRGB(0, 0); g2 = (Graphics2D) image.getGraphics(); // new field assignments for DebuggingGraphics g3 = new DebuggingGraphics(); counts = new TreeMap(); g2.setColor(Color.BLACK); if (PRETTY) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } if (isAnimated()) { initializeAnimation(); } if (isGraphical()) { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) {} statusBar = new JLabel(" "); statusBar.setBorder(BorderFactory.createLineBorder(Color.BLACK)); panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); panel.setBackground(backgroundColor); panel.setPreferredSize(new Dimension(width, height)); imagePanel = new ImagePanel(image); imagePanel.setBackground(backgroundColor); panel.add(imagePanel); // listen to mouse movement panel.addMouseMotionListener(this); // main window frame frame = new JFrame(TITLE); // frame.setResizable(false); frame.addWindowListener(this); // JPanel center = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); JScrollPane center = new JScrollPane(panel); // center.add(panel); frame.getContentPane().add(center); frame.getContentPane().add(statusBar, "South"); frame.setBackground(Color.DARK_GRAY); // menu bar setupMenuBar(); frame.pack(); center(frame); frame.setVisible(true); if (!shouldSave()) { toFront(frame); } // repaint timer so that the screen will update createTime = System.currentTimeMillis(); timer = new Timer(DELAY, this); timer.start(); } else if (shouldSave()) { // headless mode; just set a hook on shutdown to save the image callingClassName = getCallingClassName(); try { Runtime.getRuntime().addShutdownHook(new Thread(this)); } catch (Exception e) { if (DEBUG) System.out.println("unable to add shutdown hook: " + e); } } } // method of FileFilter interface public boolean accept(File file) { return file.isDirectory() || (file.getName().toLowerCase().endsWith(".png") || file.getName().toLowerCase().endsWith(".gif")); } // used for an internal timer that keeps repainting public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Timer) { // redraw the screen at regular intervals to catch all paint operations panel.repaint(); if (shouldDiff() && System.currentTimeMillis() > createTime + 4 * DELAY) { String expected = System.getProperty(DIFF_PROPERTY); try { String actual = saveToTempFile(); DiffImage diff = new DiffImage(expected, actual); diff.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } catch (IOException ioe) { System.err.println("Error diffing image: " + ioe); } timer.stop(); } else if (shouldSave() && readyToClose()) { // auto-save-and-close if desired try { if (isAnimated()) { saveAnimated(System.getProperty(SAVE_PROPERTY)); } else { save(System.getProperty(SAVE_PROPERTY)); } } catch (IOException ioe) { System.err.println("Error saving image: " + ioe); } exit(); } } else if (e.getActionCommand().equals("Exit")) { exit(); } else if (e.getActionCommand().equals("Compare to File...")) { compareToFile(); } else if (e.getActionCommand().equals("Compare to Web File...")) { new Thread(new Runnable() { public void run() { compareToURL(); } }).start(); } else if (e.getActionCommand().equals("Save As...")) { saveAs(); } else if (e.getActionCommand().equals("Save Animated GIF...")) { saveAsAnimated(); } else if (e.getActionCommand().equals("Zoom In")) { zoom(currentZoom + 1); } else if (e.getActionCommand().equals("Zoom Out")) { zoom(currentZoom - 1); } else if (e.getActionCommand().equals("Zoom Normal (100%)")) { zoom(1); } else if (e.getActionCommand().equals("Grid Lines")) { setGridLines(((JCheckBoxMenuItem) e.getSource()).isSelected()); } else if (e.getActionCommand().equals("About...")) { JOptionPane.showMessageDialog(frame, "DrawingPanel\n" + "Graphical library class to support Building Java Programs textbook\n" + "written by Marty Stepp and Stuart Reges\n" + "University of Washington\n\n" + "please visit our web site at:\n" + "http://www.buildingjavaprograms.com/", "About DrawingPanel", JOptionPane.INFORMATION_MESSAGE); } } public void addMouseListener(MouseInputListener listener) { panel.addMouseListener(listener); if (listener instanceof MouseMotionListener) { panel.addMouseMotionListener((MouseMotionListener) listener); } } // erases all drawn shapes/lines/colors from the panel public void clear() { int[] pixels = new int[width * height]; for (int i = 0; i < pixels.length; i++) { pixels[i] = initialPixel; } image.setRGB(0, 0, width, height, pixels, 0, 1); } // method of FileFilter interface public String getDescription() { return "Image files (*.png; *.gif)"; } // obtain the Graphics object to draw on the panel public Graphics2D getGraphics() { return g2; } // variation of getGraphics that returns an object that records a count for // various drawing methods...see also getCounts public Graphics getDebuggingGraphics() { if (g3 == null) { g3 = new DebuggingGraphics(); } return g3; } // returns an immutable version of the internal map used for counting // occurrences calls on various drawing methods. Experts can manipulate // the map, but novices will be encouraged to simply print it public Map getCounts() { return Collections.unmodifiableMap(counts); } // returns the drawing panel's width in pixels public int getHeight() { return height; } // returns the drawing panel's pixel size (width, height) as a Dimension object public Dimension getSize() { return new Dimension(width, height); } // returns the drawing panel's width in pixels public int getWidth() { return width; } // returns panel's current zoom factor public int getZoom() { return currentZoom; } // listens to mouse dragging public void mouseDragged(MouseEvent e) {} // listens to mouse movement public void mouseMoved(MouseEvent e) { int x = e.getX() / currentZoom; int y = e.getY() / currentZoom; setStatusBarText("(" + x + ", " + y + ")"); } // run on shutdown to save the image public void run() { if (DEBUG) System.out.println("Running shutdown hook"); try { String filename = System.getProperty(SAVE_PROPERTY); if (filename == null) { filename = callingClassName + ".png"; } if (isAnimated()) { saveAnimated(filename); } else { save(filename); } } catch (SecurityException e) { } catch (IOException e) { System.err.println("Error saving image: " + e); } } // take the current contents of the panel and write them to a file public void save(String filename) throws IOException { BufferedImage image2 = getImage(); // if zoomed, scale image before saving it if (SAVE_SCALED_IMAGES && currentZoom != 1) { BufferedImage zoomedImage = new BufferedImage(width * currentZoom, height * currentZoom, image.getType()); Graphics2D g = (Graphics2D) zoomedImage.getGraphics(); g.setColor(Color.BLACK); if (PRETTY) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } g.scale(currentZoom, currentZoom); g.drawImage(image2, 0, 0, imagePanel); image2 = zoomedImage; } // if saving multiple panels, append number // (e.g. output_*.png becomes output_1.png, output_2.png, etc.) if (isMultiple()) { filename = filename.replaceAll("\\*", String.valueOf(instanceNumber)); } int lastDot = filename.lastIndexOf("."); String extension = filename.substring(lastDot + 1); // write file // TODO: doesn't save background color I don't think ImageIO.write(image2, extension, new File(filename)); } // take the current contents of the panel and write them to a file public void saveAnimated(String filename) throws IOException { // add one more final frame if (DEBUG) System.out.println("saveAnimated(" + filename + ")"); frames.add(new ImageFrame(getImage(), 5000)); // encoder.continueEncoding(stream, getImage(), 5000); // Gif89Encoder gifenc = new Gif89Encoder(); // add each frame of animation to the encoder try { for (int i = 0; i < frames.size(); i++) { ImageFrame imageFrame = frames.get(i); encoder.addFrame(imageFrame.image); encoder.getFrameAt(i).setDelay(imageFrame.delay); imageFrame.image.flush(); frames.set(i, null); } } catch (OutOfMemoryError e) { System.out.println("Out of memory when saving"); } // gifenc.setComments(annotation); // gifenc.setUniformDelay((int) Math.round(100 / frames_per_second)); // gifenc.setUniformDelay(DELAY); // encoder.setBackground(backgroundColor); encoder.setLoopCount(0); encoder.encode(new FileOutputStream(filename)); } // set the background color of the drawing panel public void setBackground(Color c) { Color oldBackgroundColor = backgroundColor; backgroundColor = c; if (isGraphical()) { panel.setBackground(c); imagePanel.setBackground(c); } // with animated images, need to palette-swap the old bg color for the new // because there's no notion of transparency in a palettized 8-bit image if (isAnimated()) { replaceColor(image, oldBackgroundColor, c); } } // Enables or disables the drawing of grid lines on top of the image to help // with debugging sizes and coordinates. public void setGridLines(boolean gridLines) { this.gridLines = gridLines; imagePanel.repaint(); } // sets the drawing panel's height in pixels to the given value // After calling this method, the client must call getGraphics() again // to get the new graphics context of the newly enlarged image buffer. public void setHeight(int height) { setSize(getWidth(), height); } // sets the drawing panel's pixel size (width, height) to the given values // After calling this method, the client must call getGraphics() again // to get the new graphics context of the newly enlarged image buffer. public void setSize(int width, int height) { // replace the image buffer for drawing BufferedImage newImage = new BufferedImage(width, height, image.getType()); imagePanel.setImage(newImage); newImage.getGraphics().drawImage(image, 0, 0, imagePanel); this.width = width; this.height = height; image = newImage; g2 = (Graphics2D) newImage.getGraphics(); g2.setColor(Color.BLACK); if (PRETTY) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } zoom(currentZoom); if (isGraphical()) { frame.pack(); } } // show or hide the drawing panel on the screen public void setVisible(boolean visible) { if (isGraphical()) { frame.setVisible(visible); } } // sets the drawing panel's width in pixels to the given value // After calling this method, the client must call getGraphics() again // to get the new graphics context of the newly enlarged image buffer. public void setWidth(int width) { setSize(width, getHeight()); } // makes the program pause for the given amount of time, // allowing for animation public void sleep(int millis) { if (isGraphical() && frame.isVisible()) { // if not even displaying, we don't actually need to sleep if (millis > 0) { try { Thread.sleep(millis); panel.repaint(); toFront(frame); } catch (Exception e) {} } } // manually enable animation if necessary if (!isAnimated() && !isMultiple() && autoEnableAnimationOnSleep()) { animated = true; initializeAnimation(); } // capture a frame of animation if (isAnimated() && shouldSave() && !isMultiple()) { try { if (frames.size() < MAX_FRAMES) { frames.add(new ImageFrame(getImage(), millis)); } // reset creation timer so that we won't save/close just yet createTime = System.currentTimeMillis(); } catch (OutOfMemoryError e) { System.out.println("Out of memory after capturing " + frames.size() + " frames"); } } } // moves window on top of other windows public void toFront() { toFront(frame); } // called when DrawingPanel closes, to potentially exit the program public void windowClosing(WindowEvent event) { frame.setVisible(false); synchronized (getClass()) { instances--; } frame.dispose(); } // methods required by WindowListener interface public void windowActivated(WindowEvent event) {} public void windowClosed(WindowEvent event) {} public void windowDeactivated(WindowEvent event) {} public void windowDeiconified(WindowEvent event) {} public void windowIconified(WindowEvent event) {} public void windowOpened(WindowEvent event) {} // zooms the drawing panel in/out to the given factor // factor should be >= 1 public void zoom(int zoomFactor) { currentZoom = Math.max(1, zoomFactor); if (isGraphical()) { Dimension size = new Dimension(width * currentZoom, height * currentZoom); imagePanel.setPreferredSize(size); panel.setPreferredSize(size); imagePanel.validate(); imagePanel.revalidate(); panel.validate(); panel.revalidate(); // imagePanel.setSize(size); frame.getContentPane().validate(); imagePanel.repaint(); setStatusBarText(" "); // resize frame if any more space for it exists or it's the wrong size Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); if (size.width <= screen.width || size.height <= screen.height) { frame.pack(); } // if (size.width <= screen.width && size.height <= screen.height) { // frame.pack(); // center(frame); // } } } // moves given jframe to center of screen private void center(Window frame) { Toolkit tk = Toolkit.getDefaultToolkit(); Dimension screen = tk.getScreenSize(); int x = Math.max(0, (screen.width - frame.getWidth()) / 2); int y = Math.max(0, (screen.height - frame.getHeight()) / 2); frame.setLocation(x, y); } // constructs and initializes JFileChooser object if necessary private void checkChooser() { if (chooser == null) { // TODO: fix security on applet mode chooser = new JFileChooser(System.getProperty("user.dir")); chooser.setMultiSelectionEnabled(false); chooser.setFileFilter(this); } } // compares current DrawingPanel image to an image file on disk private void compareToFile() { // save current image to a temp file try { String tempFile = saveToTempFile(); // use file chooser dialog to find image to compare against checkChooser(); if (chooser.showOpenDialog(frame) != JFileChooser.APPROVE_OPTION) { return; } // user chose a file; let's diff it new DiffImage(chooser.getSelectedFile().toString(), tempFile); } catch (IOException ioe) { JOptionPane.showMessageDialog(frame, "Unable to compare images: \n" + ioe); } } // compares current DrawingPanel image to an image file on the web private void compareToURL() { // save current image to a temp file try { String tempFile = saveToTempFile(); // get list of images to compare against from web site URL url = new URL(COURSE_WEB_SITE); Scanner input = new Scanner(url.openStream()); List lines = new ArrayList(); List filenames = new ArrayList(); while (input.hasNextLine()) { String line = input.nextLine().trim(); if (line.length() == 0) { continue; } if (line.startsWith("#")) { // a comment if (line.endsWith(":")) { // category label lines.add(line); line = line.replaceAll("#\\s*", ""); filenames.add(line); } } else { lines.add(line); // get filename int lastSlash = line.lastIndexOf('/'); if (lastSlash >= 0) { line = line.substring(lastSlash + 1); } // remove extension int dot = line.lastIndexOf('.'); if (dot >= 0) { line = line.substring(0, dot); } filenames.add(line); } } if (filenames.isEmpty()) { JOptionPane.showMessageDialog(frame, "No valid web files found to compare against.", "Error: no web files found", JOptionPane.ERROR_MESSAGE); return; } else { String fileURL = null; if (filenames.size() == 1) { // only one choice; take it fileURL = lines.get(0); } else { // user chooses file to compare against int choice = showOptionDialog(frame, "File to compare against?", "Choose File", filenames.toArray(new String[0])); if (choice < 0) { return; } // user chose a file; let's diff it fileURL = lines.get(choice); } if (DEBUG) System.out.println(fileURL); new DiffImage(fileURL, tempFile); } } catch (NoRouteToHostException nrthe) { JOptionPane.showMessageDialog(frame, "You do not appear to have a working internet connection.\nPlease check your internet settings and try again.\n\n" + nrthe); } catch (UnknownHostException uhe) { JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + uhe); } catch (SocketException se) { JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + se); } catch (IOException ioe) { JOptionPane.showMessageDialog(frame, "Unable to compare images: \n" + ioe); } } // closes the frame and exits the program private void exit() { if (isGraphical()) { frame.setVisible(false); frame.dispose(); } try { System.exit(0); } catch (SecurityException e) { // if we're running in an applet or something, can't do System.exit } } // returns a best guess about the name of the class that constructed this panel private String getCallingClassName() { StackTraceElement[] stack = new RuntimeException().getStackTrace(); String className = this.getClass().getName(); for (StackTraceElement element : stack) { String cl = element.getClassName(); if (!className.equals(cl)) { className = cl; break; } } return className; } private BufferedImage getImage() { // create second image so we get the background color BufferedImage image2; if (isAnimated()) { image2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED); } else { image2 = new BufferedImage(width, height, image.getType()); } Graphics g = image2.getGraphics(); if (DEBUG) System.out.println("getImage setting background to " + backgroundColor); g.setColor(backgroundColor); g.fillRect(0, 0, width, height); g.drawImage(image, 0, 0, panel); return image2; } private void initializeAnimation() { frames = new ArrayList(); encoder = new Gif89Encoder(); /* try { if (hasProperty(SAVE_PROPERTY)) { stream = new FileOutputStream(System.getProperty(SAVE_PROPERTY)); } // encoder.startEncoding(stream); } catch (IOException e) { System.out.println(e); } */ } private boolean autoEnableAnimationOnSleep() { return propertyIsTrue(AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY); } private boolean isAnimated() { return animated || propertyIsTrue(ANIMATED_PROPERTY); } private boolean isGraphical() { return !hasProperty(SAVE_PROPERTY) && !hasProperty(HEADLESS_PROPERTY); } private boolean isMultiple() { return propertyIsTrue(MULTIPLE_PROPERTY); } private boolean readyToClose() { /* if (isAnimated()) { // wait a little longer, in case animation is sleeping return System.currentTimeMillis() > createTime + 5 * DELAY; } else { return System.currentTimeMillis() > createTime + 4 * DELAY; } */ return (instances == 0 || shouldSave()) && !mainIsActive(); } private void replaceColor(BufferedImage image, Color oldColor, Color newColor) { int oldRGB = oldColor.getRGB(); int newRGB = newColor.getRGB(); for (int y = 0; y < image.getHeight(); y++) { for (int x = 0; x < image.getWidth(); x++) { if (image.getRGB(x, y) == oldRGB) { image.setRGB(x, y, newRGB); } } } } // called when user presses "Save As" menu item private void saveAs() { String filename = saveAsHelper("png"); if (filename != null) { try { save(filename); // save the file } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex); } } } private void saveAsAnimated() { String filename = saveAsHelper("gif"); if (filename != null) { try { // record that the file should be saved next time PrintStream out = new PrintStream(new File(ANIMATION_FILE_NAME)); out.println(filename); out.close(); JOptionPane.showMessageDialog(frame, "Due to constraints about how DrawingPanel works, you'll need to\n" + "re-run your program. When you run it the next time, DrawingPanel will \n" + "automatically save your animated image as: " + new File(filename).getName() ); } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to store animation settings:\n" + ex); } } } private String saveAsHelper(String extension) { // use file chooser dialog to get filename to save into checkChooser(); if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) { return null; } File selectedFile = chooser.getSelectedFile(); String filename = selectedFile.toString(); if (!filename.toLowerCase().endsWith(extension)) { // Windows is dumb about extensions with file choosers filename += "." + extension; } // confirm overwrite of file if (new File(filename).exists() && JOptionPane.showConfirmDialog( frame, "File exists. Overwrite?", "Overwrite?", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { return null; } return filename; } // saves DrawingPanel image to a temporary file and returns file's name private String saveToTempFile() throws IOException { File currentImageFile = File.createTempFile("current_image", ".png"); save(currentImageFile.toString()); return currentImageFile.toString(); } // sets the text that will appear in the bottom status bar private void setStatusBarText(String text) { if (currentZoom != 1) { text += " (current zoom: " + currentZoom + "x" + ")"; } statusBar.setText(text); } // initializes DrawingPanel's menu bar items private void setupMenuBar() { // abort compare if we're running as an applet or in a secure environment boolean secure = (System.getSecurityManager() != null); JMenuItem saveAs = new JMenuItem("Save As...", 'A'); saveAs.addActionListener(this); saveAs.setAccelerator(KeyStroke.getKeyStroke("ctrl S")); saveAs.setEnabled(!secure); JMenuItem saveAnimated = new JMenuItem("Save Animated GIF...", 'G'); saveAnimated.addActionListener(this); saveAnimated.setAccelerator(KeyStroke.getKeyStroke("ctrl A")); saveAnimated.setEnabled(!secure); JMenuItem compare = new JMenuItem("Compare to File...", 'C'); compare.addActionListener(this); // compare.setAccelerator(KeyStroke.getKeyStroke("ctrl C")); compare.setEnabled(!secure); JMenuItem compareURL = new JMenuItem("Compare to Web File...", 'U'); compareURL.addActionListener(this); compareURL.setAccelerator(KeyStroke.getKeyStroke("ctrl U")); compareURL.setEnabled(!secure); JMenuItem zoomIn = new JMenuItem("Zoom In", 'I'); zoomIn.addActionListener(this); zoomIn.setAccelerator(KeyStroke.getKeyStroke("ctrl EQUALS")); JMenuItem zoomOut = new JMenuItem("Zoom Out", 'O'); zoomOut.addActionListener(this); zoomOut.setAccelerator(KeyStroke.getKeyStroke("ctrl MINUS")); JMenuItem zoomNormal = new JMenuItem("Zoom Normal (100%)", 'N'); zoomNormal.addActionListener(this); zoomNormal.setAccelerator(KeyStroke.getKeyStroke("ctrl 0")); JMenuItem gridLinesItem = new JCheckBoxMenuItem("Grid Lines"); gridLinesItem.setMnemonic('G'); gridLinesItem.addActionListener(this); gridLinesItem.setAccelerator(KeyStroke.getKeyStroke("ctrl G")); JMenuItem exit = new JMenuItem("Exit", 'x'); exit.addActionListener(this); JMenuItem about = new JMenuItem("About...", 'A'); about.addActionListener(this); JMenu file = new JMenu("File"); file.setMnemonic('F'); file.add(compareURL); file.add(compare); file.addSeparator(); file.add(saveAs); file.add(saveAnimated); file.addSeparator(); file.add(exit); JMenu view = new JMenu("View"); view.setMnemonic('V'); view.add(zoomIn); view.add(zoomOut); view.add(zoomNormal); view.addSeparator(); view.add(gridLinesItem); JMenu help = new JMenu("Help"); help.setMnemonic('H'); help.add(about); JMenuBar bar = new JMenuBar(); bar.add(file); bar.add(view); bar.add(help); frame.setJMenuBar(bar); } private boolean shouldDiff() { return hasProperty(DIFF_PROPERTY); } private boolean shouldSave() { return hasProperty(SAVE_PROPERTY); } // show dialog box with given choices; return index chosen (-1 == cancel) private int showOptionDialog(Frame parent, String title, String message, final String[] names) { final JDialog dialog = new JDialog(parent, title, true); JPanel center = new JPanel(new GridLayout(0, 1)); // just a hack to make the return value a mutable reference to an int final int[] hack = {-1}; for (int i = 0; i < names.length; i++) { if (names[i].endsWith(":")) { center.add(new JLabel("" + names[i] + "")); } else { final JButton button = new JButton(names[i]); button.setActionCommand(String.valueOf(i)); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { hack[0] = Integer.parseInt(button.getActionCommand()); dialog.setVisible(false); } }); center.add(button); } } JPanel south = new JPanel(); JButton cancel = new JButton("Cancel"); cancel.setMnemonic('C'); cancel.requestFocus(); cancel.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { dialog.setVisible(false); } }); south.add(cancel); dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); dialog.getContentPane().setLayout(new BorderLayout(10, 5)); // ((JComponent) dialog.getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); if (message != null) { JLabel messageLabel = new JLabel(message); dialog.add(messageLabel, BorderLayout.NORTH); } dialog.add(center); dialog.add(south, BorderLayout.SOUTH); dialog.pack(); dialog.setResizable(false); center(dialog); cancel.requestFocus(); dialog.setVisible(true); cancel.requestFocus(); return hack[0]; } // brings the given window to the front of the z-ordering private void toFront(final Window window) { EventQueue.invokeLater(new Runnable() { public void run() { if (window != null) { window.toFront(); window.repaint(); } } }); } // Reports the differences between two images. private class DiffImage extends JPanel implements ActionListener, ChangeListener { private static final long serialVersionUID = 0; private BufferedImage image1; private BufferedImage image2; private String image1name; private int numDiffPixels; private int opacity = 50; private String label1Text = "Expected"; private String label2Text = "Actual"; private boolean highlightDiffs = false; private Color highlightColor = new Color(224, 0, 224); private JLabel image1Label; private JLabel image2Label; private JLabel diffPixelsLabel; private JSlider slider; private JCheckBox box; private JMenuItem saveAsItem; private JMenuItem setImage1Item; private JMenuItem setImage2Item; private JFrame frame; private JButton colorButton; public DiffImage(String file1, String file2) throws IOException { setImage1(file1); setImage2(file2); display(); } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == box) { highlightDiffs = box.isSelected(); repaint(); } else if (source == colorButton) { Color color = JColorChooser.showDialog(frame, "Choose highlight color", highlightColor); if (color != null) { highlightColor = color; colorButton.setBackground(color); colorButton.setForeground(color); repaint(); } } else if (source == saveAsItem) { saveAs(); } else if (source == setImage1Item) { setImage1(); } else if (source == setImage2Item) { setImage2(); } } // Counts number of pixels that differ between the two images. public void countDiffPixels() { if (image1 == null || image2 == null) { return; } int w1 = image1.getWidth(); int h1 = image1.getHeight(); int w2 = image2.getWidth(); int h2 = image2.getHeight(); int wmax = Math.max(w1, w2); int hmax = Math.max(h1, h2); // check each pair of pixels numDiffPixels = 0; for (int y = 0; y < hmax; y++) { for (int x = 0; x < wmax; x++) { int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0; int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0; if (pixel1 != pixel2) { numDiffPixels++; } } } } // initializes diffimage panel public void display() { countDiffPixels(); setupComponents(); setupEvents(); setupLayout(); frame.pack(); center(frame); frame.setVisible(true); toFront(frame); } // draws the given image onto the given graphics context public void drawImageFull(Graphics2D g2, BufferedImage image) { int iw = image.getWidth(); int ih = image.getHeight(); int w = getWidth(); int h = getHeight(); int dw = w - iw; int dh = h - ih; if (dw > 0) { g2.fillRect(iw, 0, dw, ih); } if (dh > 0) { g2.fillRect(0, ih, iw, dh); } if (dw > 0 && dh > 0) { g2.fillRect(iw, ih, dw, dh); } g2.drawImage(image, 0, 0, this); } // paints the DiffImage panel public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; // draw the expected output (image 1) if (image1 != null) { drawImageFull(g2, image1); } // draw the actual output (image 2) if (image2 != null) { Composite oldComposite = g2.getComposite(); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, ((float) opacity) / 100)); drawImageFull(g2, image2); g2.setComposite(oldComposite); } g2.setColor(Color.BLACK); // draw the highlighted diffs (if so desired) if (highlightDiffs && image1 != null && image2 != null) { int w1 = image1.getWidth(); int h1 = image1.getHeight(); int w2 = image2.getWidth(); int h2 = image2.getHeight(); int wmax = Math.max(w1, w2); int hmax = Math.max(h1, h2); // check each pair of pixels g2.setColor(highlightColor); for (int y = 0; y < hmax; y++) { for (int x = 0; x < wmax; x++) { int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0; int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0; if (pixel1 != pixel2) { g2.fillRect(x, y, 1, 1); } } } } } public void save(File file) throws IOException { // String extension = filename.substring(filename.lastIndexOf(".") + 1); // ImageIO.write(diffImage, extension, new File(filename)); String filename = file.getName(); String extension = filename.substring(filename.lastIndexOf(".") + 1); BufferedImage img = new BufferedImage(getPreferredSize().width, getPreferredSize().height, BufferedImage.TYPE_INT_ARGB); img.getGraphics().setColor(getBackground()); img.getGraphics().fillRect(0, 0, img.getWidth(), img.getHeight()); paintComponent(img.getGraphics()); ImageIO.write(img, extension, file); } public void save(String filename) throws IOException { save(new File(filename)); } // Called when "Save As" menu item is clicked public void saveAs() { checkChooser(); if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) { return; } File selectedFile = chooser.getSelectedFile(); try { save(selectedFile.toString()); } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex); } } // called when "Set Image 1" menu item is clicked public void setImage1() { checkChooser(); if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) { return; } File selectedFile = chooser.getSelectedFile(); try { setImage1(selectedFile.toString()); countDiffPixels(); diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)"); image1Label.setText(selectedFile.getName()); frame.pack(); } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to set image 1:\n" + ex); } } // sets image 1 to be the given image public void setImage1(BufferedImage image) { if (image == null) { throw new NullPointerException(); } image1 = image; setPreferredSize(new Dimension( Math.max(getPreferredSize().width, image.getWidth()), Math.max(getPreferredSize().height, image.getHeight())) ); if (frame != null) { frame.pack(); } repaint(); } // loads image 1 from the given filename or URL public void setImage1(String filename) throws IOException { image1name = new File(filename).getName(); if (filename.startsWith("http")) { setImage1(ImageIO.read(new URL(filename))); } else { setImage1(ImageIO.read(new File(filename))); } } // called when "Set Image 2" menu item is clicked public void setImage2() { checkChooser(); if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) { return; } File selectedFile = chooser.getSelectedFile(); try { setImage2(selectedFile.toString()); countDiffPixels(); diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)"); image2Label.setText(selectedFile.getName()); frame.pack(); } catch (IOException ex) { JOptionPane.showMessageDialog(frame, "Unable to set image 2:\n" + ex); } } // sets image 2 to be the given image public void setImage2(BufferedImage image) { if (image == null) { throw new NullPointerException(); } image2 = image; setPreferredSize(new Dimension( Math.max(getPreferredSize().width, image.getWidth()), Math.max(getPreferredSize().height, image.getHeight())) ); if (frame != null) { frame.pack(); } repaint(); } // loads image 2 from the given filename public void setImage2(String filename) throws IOException { if (filename.startsWith("http")) { setImage2(ImageIO.read(new URL(filename))); } else { setImage2(ImageIO.read(new File(filename))); } } private void setupComponents() { String title = "DiffImage"; if (image1name != null) { title = "Compare to " + image1name; } frame = new JFrame(title); frame.setResizable(false); // frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); slider = new JSlider(); slider.setPaintLabels(false); slider.setPaintTicks(true); slider.setSnapToTicks(true); slider.setMajorTickSpacing(25); slider.setMinorTickSpacing(5); box = new JCheckBox("Highlight diffs in color: ", highlightDiffs); colorButton = new JButton(); colorButton.setBackground(highlightColor); colorButton.setForeground(highlightColor); colorButton.setPreferredSize(new Dimension(24, 24)); diffPixelsLabel = new JLabel("(" + numDiffPixels + " pixels differ)"); diffPixelsLabel.setFont(diffPixelsLabel.getFont().deriveFont(Font.BOLD)); image1Label = new JLabel(label1Text); image2Label = new JLabel(label2Text); setupMenuBar(); } // initializes layout of components private void setupLayout() { JPanel southPanel1 = new JPanel(); southPanel1.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY)); southPanel1.add(image1Label); southPanel1.add(slider); southPanel1.add(image2Label); southPanel1.add(Box.createHorizontalStrut(20)); JPanel southPanel2 = new JPanel(); southPanel2.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY)); southPanel2.add(diffPixelsLabel); southPanel2.add(Box.createHorizontalStrut(20)); southPanel2.add(box); southPanel2.add(colorButton); Container southPanel = javax.swing.Box.createVerticalBox(); southPanel.add(southPanel1); southPanel.add(southPanel2); frame.add(this, BorderLayout.CENTER); frame.add(southPanel, BorderLayout.SOUTH); } // initializes main menu bar private void setupMenuBar() { saveAsItem = new JMenuItem("Save As...", 'A'); saveAsItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S")); setImage1Item = new JMenuItem("Set Image 1...", '1'); setImage1Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 1")); setImage2Item = new JMenuItem("Set Image 2...", '2'); setImage2Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 2")); JMenu file = new JMenu("File"); file.setMnemonic('F'); file.add(setImage1Item); file.add(setImage2Item); file.addSeparator(); file.add(saveAsItem); JMenuBar bar = new JMenuBar(); bar.add(file); // disabling menu bar to simplify code // frame.setJMenuBar(bar); } // method of ChangeListener interface public void stateChanged(ChangeEvent e) { opacity = slider.getValue(); repaint(); } // adds event listeners to various components private void setupEvents() { slider.addChangeListener(this); box.addActionListener(this); colorButton.addActionListener(this); saveAsItem.addActionListener(this); this.setImage1Item.addActionListener(this); this.setImage2Item.addActionListener(this); } } //****************************************************************************** // DirectGif89Frame.java //****************************************************************************** //============================================================================== /** Instances of this Gif89Frame subclass are constructed from RGB image info, * either in the form of an Image object or a pixel array. *

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

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

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

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

* Animated GIF Example *

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

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

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

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

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

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