/**
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
*/
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.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Insets;
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.Scanner;
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
}
}
// 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://www.cs.washington.edu/education/courses/cse142/12sp/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()
// 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();
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;
}
// 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 panel = 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(":")) {
panel.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);
}
});
panel.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);
}
JScrollPane center = new JScrollPane(panel);
dialog.add(center);
dialog.add(south, BorderLayout.SOUTH);
dialog.pack();
Dimension dialogDimensions = dialog.getSize();
Insets dialogInsets = dialog.getInsets();
int maxHeight = (int)GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds().getHeight();
maxHeight = Math.min(maxHeight, (int)dialogDimensions.getHeight());
int newWidth = (int)Math.ceil(dialogDimensions.getWidth()) + dialogInsets.left + dialogInsets.right;
int newHeight = maxHeight - dialogInsets.top - dialogInsets.bottom;
Dimension newDialogDimensions = new Dimension(newWidth, newHeight);
dialog.setSize(newDialogDimensions);
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);
}
}