ORIGINAL DRAFT

One of the best ways you can add value to your application is by supporting a high degree of customizability. Swing gives you an immediate edge, with a plugable look-and-feel model that lets you offer flexibility in only a few lines of code. An interesting enhancement is to allow users to customize visual elements like icons. Depending on your customers, this may be more than you really want to offer, but in some applications, such as integrated development environments or graphic applications, this is a nice feature. This month’s visual component is a full-function icon editor.

Figure 1: JIconEditor in action.

Figure 1: JIconEditor in action.

Figure 1 shows JIconEditor in action. As you can see, we support common tools like a pencil for single pixel editing, an eye dropper for sampling colors, a paint fill, erase, move and various shape drawing tools, along with vertical and horizontal flipping and rotation.

We’ll be using a number of classes in this component and you can see how they relate to each other in Figure

  1. JIconEditor uses three toolbars, derived from the ToolPanel class, each of which uses a specific type of button. The PixelEditor is an extention of a PixelView and uses an ImageOps class to delegate image operations. The Action and Draw buttons and toolbars, along with the PixelEditor make use of some common constants declared in ActionConstants and DrawConstants interfaces for convenience.
Figure 2: Class relationships.

Figure 2: Class relationships.

Not all these classes can be listed in print, so be sure to check www.java-pro.com to get the full listings.

Toolbars

We’ll be using a number of tools in our implementation, along with related toolbars. Rather than use dockable JToolbar implementations, I chose to use a base class called ToolPanel which is primarily a JPanel intended to support action event dispatching. ToolPanel implements addActionListener, removeActionListener and fireActionEvent methods, which allow us to send notification to listeners whenever something happens. If you want to make these dockable, you can always drop them into a JToolBar without making any sacrifices.

There are three toolbars in our implementation, which you can see separately in Figure 3: The DrawToolbar, responsible for selecting drawing tools, the ActionToolbar, which triggers actions like flip or move, and the PaletteToolbar, which lets us select the active color. If you read last month’s column, you know I was distracted into developing a JPalette component for use in this context. This article intentionally implements a simplified palette toolbar so that there are no code dependencies between columns. If you download the code from both columns, you can substitute the PaletteToolbar with the JPalette to produce an even more sophisticated icon editor.

Figure 3.1: Action toolbar.

Figure 3.1: Action toolbar.

Figure 3.2: Draw toolbar.

Figure 3.2: Draw toolbar.

Figure 3.3: Palette toolbar.

Figure 3.3: Palette toolbar.

The PaletteToolbar uses a PaletteButton, shown in Listing 1, which extends JToggleButton so that we can make these members of a ButtonGroup. This allows us to create a mutually exclusive selection group without much of an effort. The PaletteButton class stores a color, which me make accessible from a getColor method. We also set up a few parameters like the preferred and maximum size, image positioning and focus handling (which we effectively disable). The icon is built in the constructor with a BufferedImage, filled with the selected color.

Since we want each of the buttons in the toolbar to be added to the panel, registered with an action listener and be a member of a ButtonGroup, we’ll construct them in advance in an array and iterate through the array, operating on each in the same way. The only other thing our constructor does is to set up a GridLayout and a suitable Border.

The PaletteToolbar class on Listing 2 implements the ActionListener interface and receives notification when a button is pressed. From that, we store the active color and fire an action event of our own to any ActionListener registered with the PaletteToolbar. This is easy, since we’re inheriting from the ToolPanel and need only call the fireActionEvent method. You’ll see this pattern repeated in the rest of our toolbars.

The DrawToolbar class implements another set of mutually exclusive buttons. These DrawButton instances determine the drawing context we are in, such as erasing or drawing pixels or shapes. To make it easier to reference resources, we use a DrawConstants interface that lets us include the same constants in other classes. If you’ve never seen this technique before, its convenient primarily because you don’t have to keep qualifying each constant with the class it was declared in, since the interface can be included anywhere.

The DrawConstants serve a dual purpose. The constants remove any tight coupling to specific values and let us change the values without affecting behavior. They also store the prefix names for the icon files we intend to use. These are loaded by the DrawButton class, along with a suitable cursor image.

If you’ve never defined custom cursors, it’s easy. The trick under Windows is to use the upper left 16x16 pixels in a 32x32 pixel image. Just use a GIF file and make the background transparent. This may work differently on other platforms according to the API documentation but the technique I just mentioned is not obvious and required some experimentation. You can create custom cursors with the createCustomCursor method in the AWT Toolbox class.

We use an ImageIcon to store both the Icon and Cursor images in DrawButton, whcih you can see in Listing 3. The preferred size and icons are set up in the constructor, along with a command name and a reference to the cursor for later use. We can get the cursor with a call to getToolCursor. Be careful not to rename this to getCursor or you’ll override the component-assigned cursor accessor and end up with confusing results. The only other thing to keep in mind with cursors is the hot spot, which is defined as specific pixel position for the pointer, typically the tip of an arrow or the bottom of an eye dropper, for example.

We define the hot spot as a point argument in DrawButton and set things up in the DrawToolbar class. Like PaletteToolbar, we use an array of constructed DrawButton elements and iterate through them to add, group and register each button. DrawToolbar does some minor set up work, like setting the border and layout manager, and watches for button action events. When a button is pressed, we fire off an action event through the fireActionEvent method inherited from ToolPalette.

The final toolbar is the ActionTollbar class, which uses the ActionConstants interface and ActionButton elements. These buttons take immediate action and are not mutually exclusive, so they are not members of a ButtonGroup. In every other way, however, ActionToolbar is like DrawToolbar. The ActionButton is identical to a DrawButton, except for the need to store a cursor. Like the DrawConstants, the ActionConstants define file prefixes for the icons, but we don’t expect any files with the Cursor suffix to be loaded.

Pixel Space

One key element of an icon editor is the pixel editing space. Our implementation uses two classes in the form of a PixelView, which lets us render pixels representations in a larger visible area, and a PixelEditor, which extends the PixelView by adding editing capabilities. This supports a nice separation of responsibilities and provides additional flexibility by promoting reuse should we decide we want a lightweight view, without the editing features, at some point in the future.

Listing 4 shows the PixelView source code. The constructor expects a BufferedImage and a cell size value. The BufferedImage is our source image, which will be drawn by mapping each pixel to a larger square in a grid. Each pixel is drawn based on the specified cell size parameter and will either be the color of the pixel or a singe small dot if the pixel is transparent. We store the image and cell size as instance variables to be used in our paint method.

To make sure we can use our PixelView with larger images, probably in a scroll view, we provide a getPreferredSize method which does a simple calculation based on the cellsize and possible insets. It’s important to take insets into account if we want to respond correctly when a border is used.

Drawing pixels is a simple matter of iterating through the BufferedImage using the getRGB method for each pixel position. Before we draw the pixels, our paintComponent method draws vertical and horizontal grid lines. We delegate the pixel drawing to a method called drawPixel, which decides whether to draw a filled-in square the color of the pixel or a small dot on a flat background if the alpha value is zero.

The PixelEditor in Listing 5 extends the PixelView class and adds a number of useful behaviors. Some of these are context-dependent and based on the active tool, driven by selections in the DrawToolbar. Whenever tools are selected, we receive an action event and, after determining which toolbar the event came from, either set the context or take immediate action. In the case of color selections or drawing tools, we set the color or the active tool. Drawing tools, as you may recall, also set the cursor.

The PixelEditor class processes mouse events. The mousePressed events are triggers for a drawing operations, such as drawing or erasing a single pixel or setting an anchor for an operation that relies on the mouse release position, such as drawing a line, oval or rectangle. The first cases are easy and effectively complete by the time the mouse is released. Other cases involve mouse dragging and drawing a rubber band-style tracking shape.

The paintComponent method calls the superclass to draw the grid and calls the drawSelection method if the drag variable is not null. We always make sure it gets set to null intentionally when the mouse is released and the drawing complete. The drawSelection method calculates the drawing rectangle and draws the outline of the current selection based on the current color and the drawing tool that was selected.

The mouseDrag events has to handle selection and movement operations. Selection is simply a matter of moving the non-anchored corner of our selection and repainting. Movement requires making a copy of the image and drawing into it at the new position. We handle this, in part, with the mousePressed event, to copy the original image to be dragged, and partly in the mouseDrag event to position the image at the new location. You can see this behavior when you use the hand icon to drag the image around.

After the mouse is released, a drawing operation need only draw the line, oval or rectangle outline, and possibly filled region, into the image buffer and the repaint takes care of the rest. While the selection drawing is done on the larger PixelView canvas, releasing the mouse causes painting to be transferred to the original image and the repaint causes the larger view to be updated to reflect the changes. We use the Java 2D antialiasing hints to make the drawing smoother.

The ActionToolbar events use a subclass called ImageOps to move, flip, rotate or fill portions of an image. This helps keep the coupling looser and allows us to reuse image operations in other contexts. The only tricky operation is the floodFill algorithm, which required a lot of tweaking. I started with my own version, researched it on the web to find something better and found myself repairing the best version I’d found.

This algorithm is recursive and works by moving out from a center point in every direction. The center point moves around through recursive calls and works its way around corners to fill everything that touches the original pixel in the same color, except for diagonals. It may not be optimally efficient on very large images, but it works reliably in all cases and acts plenty fast for an icon editor.

All the operations in the ImageOps class, in Listing 6, operate on a BufferedImage and may be used in other contexts, making the code as reusable as possible. The PixelEditor is more tightly coupled to the operations it performs and could be improved by developing a model that delegates operations to other classes. I found the compromise between the number of classes, overall organization and loose coupling fairly reasonable at this level, though I might use keystroke mappings and Action objects in a future version.

Top of the Class

Listing 7 shows the JIconEditor class, which ties all this together, implementing the editor in a JPanel for convenience. The ActionToolbar and DrawToolbar are placed on the left and the PatelleToolbar on the right of a PixelView area. The PixelView is in a JScrollPane in case the image is too large to fit in the viewing area. With the real work delegated to subcomponents, there’s nothing more than a constructor that positions the elements in a BorderLayout.

You’ll find a JIconEditorTest class online when you pull the source code down. The main method simply creates a JFrame and puts a JIconEditor in the center. I found loading time to be slightly longer than I like because so many small images were being loaded for the toolbar icons and cursors. An optimization that could address this is to put all the images together in single image/grid and to reconstruct the individual icons in memory.

Overall JIconEditor is a useful tool for editing small images. The tool set is relatively comprehensive and provides considerable power to a user. This component is more of a small application that those we’ve visited in past installments, yet it still provides a drop-in set of functionality that can help enhance your user experience. Don’t just drop an icon editor into an end-user application that doesn’t really need it, but if the moment is right, you know where to look for the tools.

Listing 1

import java.awt.*;
import java.awt.image.*;
import javax.swing.*;

public class PaletteButton extends JToggleButton
{
  protected Color color;

  public PaletteButton(Color color)
  {
    setPreferredSize(new Dimension(22, 22));
    setMaximumSize(new Dimension(22, 22));
    this.color = color;
    BufferedImage image = new BufferedImage(
      16, 16, BufferedImage.TYPE_INT_RGB);
    Graphics g = image.getGraphics();
    g.setColor(color);
    g.fillRect(0, 0, 16, 16);
    setIcon(new ImageIcon(image));
    setFocusPainted(false);
    setHorizontalAlignment(CENTER);
    setVerticalAlignment(CENTER);
  }
  
  public boolean isFocusTraversable()
  {
    return false;
  }
  
  public boolean isDefaultButton()
  {
    return false;
  }
  
  public Color getColor()
  {
    return color;
  }
}

Listing 2

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;

public class PaletteToolbar extends ToolPanel
  implements ActionListener
{
  protected Color color;
  protected ArrayList listeners = new ArrayList();
  
  private PaletteButton[] buttons =
  {
    new PaletteButton(Color.red),
    new PaletteButton(Color.cyan),
    new PaletteButton(Color.black),
    new PaletteButton(Color.orange),
    new PaletteButton(Color.green),
    new PaletteButton(Color.magenta),
    new PaletteButton(Color.gray),
    new PaletteButton(Color.pink),
    new PaletteButton(Color.blue),
    new PaletteButton(Color.yellow),
    new PaletteButton(Color.white),
    new PaletteButton(Color.lightGray)
  };
    
  public PaletteToolbar()
  {
    setBorder(new CompoundBorder(
      new EtchedBorder(),
      new EmptyBorder(4, 4, 4, 4)));
    setLayout(new GridLayout(4, 3, 2, 2));
    ButtonGroup group = new ButtonGroup();
    for (int i = 0; i < buttons.length; i++)
    {
      add(buttons[i]);
      group.add(buttons[i]);
      buttons[i].addActionListener(this);
    }
  }

  public void actionPerformed(ActionEvent event)
  {
    PaletteButton button = (PaletteButton)event.getSource();
    color = button.getColor();
    fireActionEvent("Select");
  }
  
  public Color getColor()
  {
    return color;
  }
}

Listing 3

import java.io.*;
import java.awt.*;
import javax.swing.*;

public class DrawButton extends JToggleButton
  implements DrawConstants
{
  protected String command;
  protected Cursor cursor;

  public DrawButton(String name, Point point)
  {
    String iconfile = "icons/" + name + "Icon.GIF";
    String cursfile = "icons/" + name + "Cursor.GIF";
    ImageIcon icon = new ImageIcon(iconfile);
    setIcon(icon);
    setPreferredSize(new Dimension(22, 22));
    setMaximumSize(new Dimension(22, 22));
    command = name;
    if ((new File(cursfile)).exists())
    {
      ImageIcon curs = new ImageIcon(cursfile);
      cursor = Toolkit.getDefaultToolkit().createCustomCursor(
        curs.getImage(), point, command);
    }
    else
      cursor = null;
    setFocusPainted(false);
  }
  
  public boolean isFocusTraversable()
  {
    return false;
  }
  
  public boolean isDefaultButton()
  {
    return false;
  }
  
  public String getCommand()
  {
    return command;
  }

  public Cursor getToolCursor()
  {
    if (cursor == null)
      return Cursor.getDefaultCursor();
    return cursor;
  }
}

Listing 4

import java.awt.*;
import java.awt.image.*;
import javax.swing.*;

public class PixelView extends JPanel
{
  protected int cellSize;
  protected BufferedImage image;

  public PixelView(BufferedImage image, int cellSize)
  {
    this.image = image;
    this.cellSize = cellSize;
  }
  
  public Dimension getPreferredSize()
  {
    Insets insets = getInsets();
    int width = image.getWidth();
    int height = image.getHeight();
    return new Dimension(
      insets.left + insets.right + width * cellSize + 1,
      insets.top + insets.bottom + height * cellSize + 1);
  }
 
  public void paintComponent(Graphics g)
  {
    int w = getSize().width;
    int h = getSize().height;
    g.setColor(getBackground());
    g.fillRect(0, 0, w, h);

    Insets insets = getInsets();
    int width = image.getWidth();
    int height = image.getHeight();
    
    // Draw grid
    g.setColor(Color.lightGray);
    for (int x = 0; x <= width; x++)
    {
      int offset = insets.left + x * cellSize;
      g.drawLine(offset, insets.top,
        offset, insets.top + height * cellSize);
    }
    for (int y = 0; y <= height; y++)
    {
      int offset = insets.top + y * cellSize;
      g.drawLine(insets.left, offset,
        insets.left + width * cellSize, offset);
    }
    // Draw cells
    for (int x = 0; x < width; x++)
    {
      for (int y = 0; y < height; y++)
      {
        drawCell(g, insets, x, y, image.getRGB(x, y));
      }
    }
  }
  
  protected void drawCell(Graphics g,
    Insets insets, int x, int y, int rgb)
  {
    int alpha = (rgb >> 24) & 0xff;
    if (alpha == 0)
    {
      g.setColor(getBackground());
      g.fillRect(
        insets.left + x * cellSize + 1,
        insets.top + y * cellSize + 1,
        cellSize - 1, cellSize - 1);
      g.setColor(Color.black);
      g.drawLine(
        insets.left + x * cellSize + (cellSize / 2),
        insets.top + y * cellSize + (cellSize / 2),
        insets.left + x * cellSize + (cellSize / 2),
        insets.top + y * cellSize + (cellSize / 2));
    }
    else
    {
      g.setColor(new Color(rgb));
      g.fillRect(
        insets.left + x * cellSize + 1,
        insets.top + y * cellSize + 1,
        cellSize - 1, cellSize - 1);
    }
  }
}

Listing 5

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.*;

public class PixelEditor extends PixelView
  implements DrawConstants, ActionConstants,
    MouseListener, MouseMotionListener,
    ActionListener
{
  protected String activeTool = PENCIL;
  protected Color color = Color.blue;
  protected BufferedImage move;
  protected Rectangle drag;
  protected JLabel preview;
  protected ImageOps imageOps;
  protected JFileChooser fileChooser;

  public PixelEditor(BufferedImage image,
    int cellSize, JLabel preview)
  {
    super(image, cellSize);
    this.preview = preview;
    imageOps = new ImageOps();
    addMouseListener(this);
    setBorder(new EmptyBorder(4, 4, 4, 4));
    fileChooser = new JFileChooser();
    fileChooser.addChoosableFileFilter(new GIFFileFilter());
    fileChooser.setCurrentDirectory(new File("."));
  }
  
  protected void refresh()
  {
    repaint();
    preview.repaint();
  }
  
  public void paintComponent(Graphics g)
  {
    super.paintComponent(g);
    if (drag != null)
    {
      drawSelection(g, getInsets(), activeTool);
    }
  }
  
  protected void drawSelection(Graphics g,
    Insets insets, String selected)
  {
    g.setColor(color);
    int halfCell = cellSize / 2;
    int corner = cellSize * 3;
    Rectangle scaled = new Rectangle(
      insets.left + drag.x * cellSize + halfCell,
      insets.top + drag.y * cellSize + halfCell,
      drag.width * cellSize, drag.height * cellSize);
    if (selected == LINE)
    {
      g.drawLine(scaled.x, scaled.y,
        scaled.x + scaled.width,
        scaled.y + scaled.height);
    }
    if (selected == FILL_OVAL)
    {
      g.fillOval(scaled.x, scaled.y,
        scaled.width, scaled.height);
    }
    if (selected == OVAL ||
        selected == FILL_OVAL)
    {
      g.drawOval(scaled.x, scaled.y,
        scaled.width, scaled.height);
    }
    if (selected == FILL_RECT)
    {
      g.fillRect(scaled.x, scaled.y,
        scaled.width, scaled.height);
    }
    if (selected == RECT ||
        selected == FILL_RECT)
    {
      g.drawRect(scaled.x, scaled.y,
        scaled.width, scaled.height);
    }
    if (selected == FILL_ROUND_RECT)
    {
      g.fillRoundRect(scaled.x, scaled.y,
        scaled.width, scaled.height, corner, corner);
    }
    if (selected == ROUND_RECT ||
        selected == FILL_ROUND_RECT)
    {
      g.drawRoundRect(scaled.x, scaled.y,
        scaled.width, scaled.height, corner, corner);
    }
  }
  
  public void mouseClicked(MouseEvent event) {}
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}

  public void mouseReleased(MouseEvent event)
  {
    removeMouseMotionListener(this);
    if (drag != null)
    {
      Graphics2D g = (Graphics2D)image.getGraphics();
      g.setColor(color);
      g.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
      if (activeTool == LINE)
      {
        g.drawLine(drag.x, drag.y,
          drag.x + drag.width, drag.y + drag.height);
      }
      if (activeTool == FILL_OVAL)
      {
        g.fillOval(drag.x, drag.y, drag.width, drag.height);
      }
      if (activeTool == OVAL ||
          activeTool == FILL_OVAL)
      {
        g.drawOval(drag.x, drag.y, drag.width, drag.height);
      }
      if (activeTool == FILL_RECT)
      {
        g.fillRect(drag.x, drag.y, drag.width, drag.height);
      }
      if (activeTool == RECT ||
          activeTool == FILL_RECT)
      {
        g.drawRect(drag.x, drag.y, drag.width, drag.height);
      }
      if (activeTool == FILL_ROUND_RECT)
      {
        g.fillRoundRect(drag.x, drag.y, drag.width, drag.height, 3, 3);
      }
      if (activeTool == ROUND_RECT ||
          activeTool == FILL_ROUND_RECT)
      {
        g.drawRoundRect(drag.x, drag.y, drag.width, drag.height, 3, 3);
      }
    }
    move = null;
    drag = null;
    refresh();
  }
  
  public void mousePressed(MouseEvent event)
  {
    Insets insets = getInsets();
    int width = image.getWidth();
    int height = image.getHeight();
    int x = (event.getX() - insets.left) / width;
    int y = (event.getY() - insets.top) / height;
    if (activeTool.equals(PENCIL))
    {
      image.setRGB(x, y, color.getRGB());
    }
    if (activeTool.equals(ERASER))
    {
      image.setRGB(x, y, 0);
    }
    if (activeTool.equals(DROPPER))
    {
      color = new Color(image.getRGB(x, y));
    }
    if (activeTool.equals(PAINT))
    {
      imageOps.floodFill(image, x, y,
        image.getRGB(x, y), color.getRGB());
    }
    if (activeTool.equals(LINE) ||
        activeTool.equals(OVAL) ||
        activeTool.equals(FILL_OVAL) ||
        activeTool.equals(RECT) ||
        activeTool.equals(FILL_RECT) ||
        activeTool.equals(ROUND_RECT) ||
        activeTool.equals(FILL_ROUND_RECT))
    {
      int xa = (event.getX() - insets.left) / cellSize;
      int ya = (event.getY() - insets.top) / cellSize;
      drag = new Rectangle(xa, ya, 0, 0);
      addMouseMotionListener(this);
    }
    if (activeTool.equals(HAND))
    {
      int w = image.getWidth();
      int h = image.getHeight();
      move = new BufferedImage(
        w, h, BufferedImage.TYPE_INT_ARGB);
      Graphics g = move.getGraphics();
      g.drawImage(image, 0, 0, this);
      int xa = (event.getX() - insets.left) / cellSize;
      int ya = (event.getY() - insets.top) / cellSize;
      drag = new Rectangle(xa, ya, 0, 0);
      addMouseMotionListener(this);
    }
    refresh();
  }
  
  public void mouseMoved(MouseEvent event) {}
  public void mouseDragged(MouseEvent event)
  {
    Insets insets = getInsets();
    if (move != null)
    {
      int x = (event.getX() - insets.left) /
        cellSize - drag.x;
      int y = (event.getY() - insets.top) /
        cellSize - drag.y;
      int w = image.getWidth();
      int h = image.getHeight();
      image = new BufferedImage(
        w, h, BufferedImage.TYPE_INT_ARGB);
      Graphics g = image.getGraphics();
      g.drawImage(move, x, y, this);
      refresh();
    }
    if (drag != null)
    {
      int x = (event.getX() - insets.left) / cellSize;
      int y = (event.getY() - insets.top) / cellSize;			
      drag.setSize(x - drag.x, y - drag.y);
      refresh();
    }
  }

  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    if (source instanceof PaletteToolbar)
    {
      PaletteToolbar palette = (PaletteToolbar)source;
      color = palette.getColor();
    }
    if (source instanceof DrawToolbar)
    {
      DrawToolbar tools = (DrawToolbar)source;
      activeTool = event.getActionCommand();
      setCursor(tools.getToolCursor());
    }
    if (source instanceof ActionToolbar)
    {
      ActionToolbar toolbar = (ActionToolbar)source;
      String command = event.getActionCommand();
      if (command.equals(MOVE_NORTH_WEST))
        image = imageOps.moveImage(image, -1, -1);
      if (command.equals(MOVE_NORTH))
        image = imageOps.moveImage(image, 0, -1);
      if (command.equals(MOVE_NORTH_EAST))
        image = imageOps.moveImage(image, 1, -1);
      if (command.equals(MOVE_WEST))
        image = imageOps.moveImage(image, -1, 0);
      if (command.equals(MOVE_EAST))
        image = imageOps.moveImage(image, 1, 0);
      if (command.equals(MOVE_SOUTH_WEST))
        image = imageOps.moveImage(image, -1, 1);
      if (command.equals(MOVE_SOUTH))
        image = imageOps.moveImage(image, 0, 1);
      if (command.equals(MOVE_SOUTH_EAST))
        image = imageOps.moveImage(image, 1, 1);
      if (command.equals(FLIP_VERT))
        image = imageOps.flipImage(image, ImageOps.VERTICAL);
      if (command.equals(FLIP_HORZ))
        image = imageOps.flipImage(image, ImageOps.HORIZONTAL);
      if (command.equals(ROTATE_RIGHT))
        image = imageOps.rotateImage(image, ImageOps.RIGHT);
      if (command.equals(ROTATE_LEFT))
        image = imageOps.rotateImage(image, ImageOps.LEFT);

      if (command.equals(NEW_FILE))
        newFile();
      if (command.equals(OPEN_FILE))
        openFile();
      if (command.equals(SAVE_FILE))
        saveFile();
      
      refresh();
    }
  }
  
  protected void newFile()
  {
    int w = image.getWidth();
    int h = image.getHeight();
    image = new BufferedImage(
      w, h, BufferedImage.TYPE_INT_ARGB);
    preview.setIcon(new ImageIcon(image));
    refresh();
  }
  
  protected void openFile()
  {
    int result = fileChooser.showOpenDialog(this);
    if (result == JFileChooser.APPROVE_OPTION)
    {
      File file = fileChooser.getSelectedFile();
      image = readBufferedImage(file.toString());
      preview.setIcon(new ImageIcon(image));
      refresh();
    }
  }
  
  protected void saveFile()
  {
    int result = fileChooser.showSaveDialog(this);
    if (result == JFileChooser.APPROVE_OPTION)
    {
      File file = fileChooser.getSelectedFile();
      //GifEncoder encoder = new GifEncoder(image, stream);
      System.out.println("Save " + file.toString());
    }
  }

  public BufferedImage readBufferedImage(String filename)
  {
    ImageIcon icon = new ImageIcon(filename);
    Image image = icon.getImage();
    BufferedImage buffer = new BufferedImage(
      image.getWidth(null), image.getHeight(null),
      BufferedImage.TYPE_INT_ARGB);
    Graphics g = buffer.getGraphics();
    g.drawImage(image, 0, 0, null);
    return buffer;
  }
}

Listing 6

import java.awt.*;
import java.awt.image.*;
import javax.swing.*;

public class ImageOps implements SwingConstants
{
  protected BufferedImage moveImage(
    BufferedImage image, int x, int y)
  {
    int w = image.getWidth();
    int h = image.getHeight();
    BufferedImage buffer = new BufferedImage(
      w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics gc = buffer.getGraphics();
    gc.drawImage(image, 0, 0, null);
    image = new BufferedImage(
      w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics g = image.getGraphics();
    g.drawImage(buffer, x, y, null);
    return image;
  }
  
  protected BufferedImage flipImage(
    BufferedImage image, int direction)
  {
    int w = image.getWidth();
    int h = image.getHeight();
    BufferedImage buffer = new BufferedImage(
      w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics gc = buffer.getGraphics();
    if (direction == HORIZONTAL)
      gc.drawImage(image, w, 0, 0, h, 0, 0, w, h, null);
    else
      gc.drawImage(image, 0, h, w, 0, 0, 0, w, h, null);
    image = new BufferedImage(
      w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics g = image.getGraphics();
    g.drawImage(buffer, 0, 0, null);
    return image;
  }

  protected BufferedImage rotateImage(
    BufferedImage image, int direction)
  {
    int w = image.getWidth();
    int h = image.getHeight();
    BufferedImage buffer = new BufferedImage(
      w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics2D gc = (Graphics2D)buffer.getGraphics();
    if (direction == RIGHT)
    {
      gc.translate(w, 0);
      gc.rotate(Math.PI / 2);
    }
    if (direction == LEFT)
    {
      gc.translate(0, h);
      gc.rotate(-(Math.PI / 2));
    }
    gc.drawImage(image, 0, 0, null);
    
    image = new BufferedImage(
      w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics g = image.getGraphics();
    g.drawImage(buffer, 0, 0, null);
    return image;
  }

  protected void floodFill(BufferedImage buffer,
    int x, int y, int oldColor, int newColor)
  {
    int w = buffer.getWidth();
    int h = buffer.getHeight();
    if (y < 0 || y >= h) return;
    if (x < 0 || x >= w) return;
    int pos = x;
    while (buffer.getRGB(pos, y) == oldColor)
    {
      buffer.setRGB(pos, y, newColor);
      if (y > 0 && buffer.getRGB(pos, y - 1) == oldColor)
        floodFill(buffer, pos, y - 1, oldColor, newColor);
      if ((y + 1) < h && buffer.getRGB(pos, y + 1) == oldColor)
        floodFill(buffer, pos, y + 1, oldColor, newColor);
      pos--;
      if (pos < 0) break;
    }
    pos = x + 1;
    if (pos >= w) return;
    while (buffer.getRGB(pos, y) == oldColor)
    {
      buffer.setRGB(pos, y, newColor);
      if (y > 0 && buffer.getRGB(pos, y - 1) == oldColor)
        floodFill(buffer, pos, y - 1, oldColor, newColor);
      if ((y + 1) < h && buffer.getRGB(pos, y + 1) == oldColor)
        floodFill(buffer, pos, y + 1, oldColor, newColor);
      pos++;
      if (pos >= w) break;
    }
  }
}

Listing 7

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.*;

public class JIconEditor extends JPanel
{
  public JIconEditor()
  {
    BufferedImage image = readBufferedImage("icons/Computer.gif");
    
    setLayout(new BorderLayout());
    
    JPanel west = new JPanel(new BorderLayout());
    JPanel east = new JPanel(new BorderLayout());
    
    ToolPanel action = new ActionToolbar();
    ToolPanel draw = new DrawToolbar();
    JPanel westPanel = new JPanel(new BorderLayout());
    westPanel.add(BorderLayout.NORTH, action);
    westPanel.add(BorderLayout.SOUTH, draw);

    west.add(BorderLayout.NORTH, westPanel);
    add(BorderLayout.WEST, west);

    JLabel preview = new JLabel(new ImageIcon(image));
    preview.setPreferredSize(new Dimension(26, 26));
    preview.setBorder(BorderFactory.createEtchedBorder());
    
    ToolPanel palette = new PaletteToolbar();
    
    JPanel eastPanel = new JPanel(new BorderLayout());
    eastPanel.add(BorderLayout.NORTH, preview);
    eastPanel.add(BorderLayout.SOUTH, palette);

    east.add(BorderLayout.NORTH, eastPanel);
    add(BorderLayout.EAST, east);

    PixelEditor editor = new PixelEditor(image, 16, preview);
    add(BorderLayout.CENTER, new JScrollPane(editor));

    palette.addActionListener(editor);
    draw.addActionListener(editor);
    action.addActionListener(editor);
  }
  
  public BufferedImage readBufferedImage(String filename)
  {
    ImageIcon icon = new ImageIcon(filename);
    Image image = icon.getImage();
    BufferedImage buffer = new BufferedImage(
      image.getWidth(null), image.getHeight(null),
      BufferedImage.TYPE_INT_ARGB);
    Graphics g = buffer.getGraphics();
    g.drawImage(image, 0, 0, null);
    return buffer;
  }
}