/* 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
* 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
*
* 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);
}
}
* 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