Chapter 6. Advanced AWT

If all you are doing is scanning in images or downloading existing images from the net, you rarely need to get under the hood of an image. Ultimately, however, an image is an array of bits describing the pixels; Java, like most languages with sophisticated image handling, has methods both for converting an image to an array of pixels and for converting an array of pixels back to an image. This capability lets you:

  • Build up images via a mathematical formula. (It is certainly quicker to send a formula for building an image across the net than it is to send a large image. Fractals like the ubiquitous Mandelbrot set are good examples of this.)

  • Write conversion routines that let you move images from one Java program to another, or even to programs written in other languages.

  • Easily modify the image pixel by pixel. (For example, you might want to make it brighter or to sharpen it in some way.)

This chapter shows you the techniques that you need in order to do this kind of pixel-by-pixel manipulation. We first show you the image filtering classes built into Java. You can use one of these image filters to modify an existing image fairly effortlessly, and you can build your own image filters for more sophisticated image editing. Then, we move on to the methods for building up an image from an array of pixels, including a nifty new way to do animations that was added to Java 1.1.

We end this chapter by taking up Java’s new Cut and Paste feature. Note that although “Cut and Paste” is prominently mentioned as a major addition to Java 1.1, in practice the implementation is yet one more reminder that you are dealing with the “Java development kit”—some assembly is definitely required. The off-the-shelf JDK implements only the simplest case of transferring strings, curiously omitting the capability to transfer Java objects between different Java programs. In the last section of this book, we show you how to remedy this omission for serializable Java objects—while we all await JavaSoft’s official solution for the general case in the next version of Java.

NOTE

NOTE

This (relatively) short chapter can in no way substitute for such specialized books as David Geary’s Graphic Java 1.1(also from Sunsoft/Prentice Hall) or the very comprehensive and useful book by John Zukowski, Java AWT Reference(O’Reilly, 1997). We advise people needing more specialized techniques than those covered here to consult one of these two books. Also note that we do not cover the various Java class libraries that extend the basic AWT and that are still in development. Both the Java Foundation classes (JFC—sometimes called the “Swing Set” from the Sun/IBM/Netscape alliance) and the “Application Foundation Classes” (AFC from Microsoft) promise to revolutionize the GUI end of the AWT by adding the kind of components that users have grown to expect. In either case, using them will not be much different than using the existing AWT components, as described in Volume 1.

An Overview of Java's Image Manipulation

As you saw in Chapter 7 of Volume 1, the basic AWT toolkit can load graphics files in the standard GIF and JPEG format. (Java 1.1 can also transparently handle animated GIF files, the so-called GIF89a specification—calling drawImage will start the animation.) To manipulate these images or to write the code that will allow images of other types to be used in Java programs, you need to understand the basic Java imaging classes and interfaces, as shown in Figure 6-1.

The image manipulation classes and interfaces

Figure 6-1. The image manipulation classes and interfaces

From the figure, you can deduce the following:

  • Any object that implements Java’s ImageProducer interface can produce the data for an image.

  • Any object that implements the ImageConsumer interface can take the data from an image producer.

  • You can place any object that implements the ImageFilter interface between an image producer and an image consumer in order to modify the image. Use FilteredImageSource objects for the communication between producer, filter, and consumer.

Of course, as you will soon see, Java comes with filter classes, and you can also design your own. For example, a specialized image filter might try to “sharpen” or “bleach” the image produced by the image producer.

NOTE

NOTE

For monitoring the asynchronous loading of images, an object that implements ImageObserver must be involved. See Chapter 7 of Volume 1 for more on image observers.

The Java Color Models

Before you can go deeper with the image manipulation process, you need to understand the various ways Java can handle color for individual pixels. Java uses subclasses of an abstract class called ColorModel to hold color information. The default color model is encapsulated in a subclass called DirectColorModel. This model uses a single integer to describe each pixel, thus allowing 32 bits for the color model. The bits are divided into four bytes: three for the familiar red, green, blue and the fourth byte for the “alpha,” which defines the transparency of the pixel. If the alpha byte is 0, then the pixel is transparent and the background shines clearly through. If the alpha byte is 255, then the pixel is opaque. Using 24 bits for the color information allows the standard “true” color model (16,777,216 colors) to be used. Keep in mind that while the AWT will make its best approximation to the color of a pixel if the target system doesn’t support high color, it does require at least 256-color support.

The other common situation that you may find yourself in is one where video memory is at a premium and high color is not needed. For this situation Java allows you to use an indexed color model, which gives you a palette of a small number of colors to work with—16 or 256 colors are typical. With an indexed color model, the colors are not themselves limited—only the number of colors that you can display on the screen at any one time is limited. As the API notes indicate, most of the constructors that build images can use an indexed color model in addition to the default color model. (For those who are familiar with the internals of video cards, standard VGA on a PC uses an indexed color model.) The constructors of the IndexColorModel class take arrays of bytes to specify the red, green, and blue values for each of the colors that you want to use.

Image Filters

Suppose you have an image object, for example, as the return value of the getImage method of the Toolkit class.

Image img = Toolkit.getDefaultToolkit().getImage("myfile.gif");

As you saw in Chapter 7 of Volume 1, an Image object does not contain the actual pixels of the image; it just knows where to find these pixels. The image is acquired only when the image pixels are actually needed. You can display an image object with the drawImage method of the Graphics class, but if you want to modify the image, you need an ImageProducer. Actually, ImageProducer is an interface, so you need an object of a class implementing that interface.

However, the class Image does not implement the ImageProducer interface. To obtain an object that can produce the pixels of the image (which is of some unknown class that implements the ImageProducer interface), you call the getSource method of the Image class:

ImageProducer prod = img.getSource();

Now, you are ready to apply an image filter. Image filters are objects of classes that extend ImageFilter. The base class ImageFilter implements the ImageConsumer interface. Typical filtering operations are to crop, blur, sharpen, or brighten the image. Each filtering operation requires a filter object of a particular ImageFilter subclass. You pass the image producer and the filter object to the constructor of FilteredImageSource, which is yet another class implementing ImageProducer.

ImageProducer filteredProd = new FilteredImageSource(prod, 
   filter);

Of course, you can now apply a second filter.

ImageProducer filteredProd2 = new 
   FilteredImageSource(filteredProd, filter2);

When you are done, you need to turn the result back into an image. The Toolkit class has a method createImage that turns an image producer back into an image.

Image filtered = 
   Toolkit.getDefaultToolkit().createImage(filteredProd2);

There is also a createImage method in the Component class. If the call to createImage is contained in a method of a class derived from a component such as Frame or Canvas, you can simply call

Image filtered = createImage(filteredProd2); 
// inside Component subclass

CropImageFilter

Java 1.1 supplies a few image filters, and it is usually a simple, albeit tedious, task to roll your own. Let’s first take up the supplied filter for getting a rectangular piece of an existing image (“cropping” in publishers’ jargon). It is, therefore, called the CropImageFilter.

You do not need to subclass this class. Its constructor is:

CropImageFilter(int x, int y, int width, int height)

This constructor specifies the (rectangular) part of the old image to use for the new image. The x, y parameters specify the top left corner for the cropped image, and the width and height parameters specify how large and image to crop. If the (x, y) coordinates are outside the original image area, you end up with nothing in the cropped image. If you ask for too large a width and height relative to the starting coordinates x and y, you get a black region in the excess area. Example 6-1 puts the crop filter to use. The getFilteredImage method has as parameters the image file name and the percentage of the image to be retained. It then loads and filters the image. You can select a different file name from the File menu option. Use the scrollbar at the bottom of the screen to change the crop factor. Figure 6-2 shows an example of a cropped image.

The crop filter

Figure 6-2. The crop filter

Example 6-1. CropTest.java

import java.awt.*; 
import java.awt.event.*; 
import java.awt.image.*; 
import corejava.*; 

public class CropTest extends CloseableFrame 
   implements ActionListener, AdjustmentListener 
{  public CropTest() 
   {  MenuBar mbar = new MenuBar(); 
      Menu m = new Menu("File"); 
      MenuItem m1 = new MenuItem("Open"); 
      m1.addActionListener(this); 
      m.add(m1); 
      MenuItem m2 = new MenuItem("Exit"); 
      m2.addActionListener(this); 
      m.add(m2); 
      mbar.add(m); 
      setMenuBar(mbar); 

      scroller = new Scrollbar(Scrollbar.HORIZONTAL, 0, 0, 
         0, 100); 
      scroller.setValue(50); 
      scroller.setBlockIncrement(10); 
      scroller.addAdjustmentListener(this); 
      add(scroller, "South"); 
   } 

   public double getScrollValue() 
   {  return (double)scroller.getValue() 
         / scroller.getMaximum(); 
   } 

   public void adjustmentValueChanged(AdjustmentEvent evt) 
   {  getFilteredImage(getScrollValue(), fileName); 
   } 

   public void actionPerformed(ActionEvent evt) 
   {  String arg = evt.getActionCommand(); 
      if (arg.equals("Open")) 
      {  FileDialog d = new FileDialog(this, 
            "Open file", FileDialog.LOAD); 
         d.setDirectory(lastDir); 
         d.show(); 
         String f = d.getFile(); 
         lastDir = d.getDirectory(); 
         if (f != null) 
         {  fileName = lastDir + f; 
            getFilteredImage(getScrollValue(), fileName); 
         } 
      } 
      else if(arg.equals("Exit")) System.exit(0); 
   } 

   public void getFilteredImage(double d, String file) 
   {  Toolkit tk = Toolkit.getDefaultToolkit(); 
      orig = tk.getImage(file); 
      MediaTracker mt = new MediaTracker(this); 
      mt.addImage(orig, 0); 
      try 
      {  mt.waitForAll(); 
         int w = orig.getWidth(this); 
         int h = orig.getHeight(this); 
         int neww = (int)(d * w); 
         int newh = (int)(d * h); 
         ImageProducer prod = orig.getSource(); 
         ImageFilter filter = new CropImageFilter 
            ((w - neww) / 2, (h - newh) / 2, neww, newh); 
         ImageProducer filteredProd 
            = new FilteredImageSource(prod, filter); 
         filtered = tk.createImage(filteredProd); 
         repaint(); 
      } 
      catch (InterruptedException e) 
      {  e.printStackTrace(); 
      } 
   } 

   public void paint(Graphics g) 
   {  g.translate(getInsets().left, getInsets().top); 
      if (filtered != null) 
         g.drawImage(filtered, 0, 0, this); 
   } 

   public static void main(String[] args) 
   {  Frame f = new CropTest(); 
      f.setVisible(true); 
   } 

   private Image orig; 
   private Image filtered; 
   private String fileName = ""; 
   private String lastDir = ""; 
   private Scrollbar scroller; 
}

RGBImageFilter

The most common image filters used in the AWT are subclasses of the abstract class RGBImageFilter. Subclasses of RGBImageFilter classes give you filters that let you modify individual pixels based on their current color and transparency. To build a subclass of RGBImageFilter, simply override the filterRGB method. For example, the following filter can make more or less of the background show through by changing the alpha portion of the pixel.

TIP

If the filtering you build is based only on the old pixel’s color, the AWT can optimize the filtering process for images that use an IndexColorModel. All it has to do is filter the image’s color map rather than each pixel. To filter the image’s color map, set the canFilterIndexColorModel instance field in your class to true. (The default is false.)

class TransparentImageFilter extends RGBImageFilter 
{  public TransparentImageFilter(double d) 
      throws IllegalArgumentException 
   {  if ((d < 0.0) || (d > 1.0)) 
         throw new IllegalArgumentException(); 
      level = d; 
      canFilterIndexColorModel = true; 
   } 

   public int filterRGB(int x, int y, int argb) 
   {  int alpha = (argb >> 24) & 0xFF; 
      alpha = (int)(alpha * level); 
      return ((argb & 0x00FFFFFF) | (alpha << 24)); 
   } 

   private double level; 
}

Example 6-2 shows how to use this RGB image filter. You can select a file with the File menu option. Use the scrollbar at the bottom of the screen to change the transparency level. When the slider is all the way to the left, the yellow background color hides the image. When the slider is all the way to the right, the image covers the background. At other settings, the background shines through to varying degrees (see Figure 6-3).

The transparent filter

Figure 6-3. The transparent filter

Example 6-2. TransparentImageFilterTest.java

import java.awt.*; 
import java.awt.event.*; 
import java.awt.image.*; 
import corejava.*; 

public class TransparentImageFilterTest extends CloseableFrame 
   implements ActionListener, AdjustmentListener 
{  public TransparentImageFilterTest() 
   {  MenuBar mbar = new MenuBar(); 
      Menu m = new Menu("File"); 
      MenuItem m1 = new MenuItem("Open"); 
      m1.addActionListener(this); 
      m.add(m1); 
      MenuItem m2 = new MenuItem("Exit"); 
      m2.addActionListener(this); 
      m.add(m2); 
      mbar.add(m); 
      setMenuBar(mbar); 

      scroller = new Scrollbar(Scrollbar.HORIZONTAL, 0, 0, 
         0, 100); 
      scroller.setValue(50); 
      scroller.setBlockIncrement(10); 
      scroller.addAdjustmentListener(this); 
      add(scroller, "South"); 
      setBackground(Color.yellow); 
   } 

   public double getScrollValue() 
   {  return (double)scroller.getValue() / scroller.getMaximum(); 
   } 

   public void adjustmentValueChanged(AdjustmentEvent evt) 
   {  getFilteredImage(getScrollValue(), fileName); 
   } 

   public void actionPerformed(ActionEvent evt) 
   {  String arg = evt.getActionCommand(); 
      if (arg.equals("Open")) 
      {  FileDialog d = new FileDialog(this, 
            "Open file", FileDialog.LOAD); 
         d.setDirectory(lastDir); 
         d.show(); 
         String f = d.getFile(); 
         lastDir = d.getDirectory(); 
         if (f != null) 
         {  fileName = lastDir + f; 
            getFilteredImage(getScrollValue(), fileName); 
         } 
      } 
      else if(arg.equals("Exit")) System.exit(0); 
   } 

   public void getFilteredImage(double d, String file) 
   {  Toolkit tk = Toolkit.getDefaultToolkit(); 
      orig = tk.getImage(file); 
      ImageProducer prod = orig.getSource(); 
      ImageFilter filter = new TransparentImageFilter(d); 
      ImageProducer filteredProd 
         = new FilteredImageSource(prod, filter); 
      filtered = tk.createImage(filteredProd); 
      repaint(); 
   } 

   public void paint(Graphics g) 
   {  g.translate(getInsets().left, getInsets().top); 
      if (filtered != null) 
         g.drawImage(filtered, 0, 0, this); 
   } 
   public static void main(String[] args) 
   {  Frame f = new TransparentImageFilterTest(); 
      f.show(); 
   } 

   private Image orig; 
   private Image filtered; 
   private String fileName = ""; 
   private String lastDir = ""; 
   private Scrollbar scroller; 
} 

class TransparentImageFilter extends RGBImageFilter 
{  public TransparentImageFilter(double d) 
      throws IllegalArgumentException 
   {  if ((d < 0.0) || (d > 1.0)) 
         throw new IllegalArgumentException(); 
      level = d; 
      canFilterIndexColorModel = true; 
   } 

   public int filterRGB(int x, int y, int argb) 
   {  int alpha = (argb >> 24) & 0xFF; 
      alpha = (int)(alpha * level); 
      return ((argb & 0x00FFFFFF) | (alpha << 24)); 
   } 

   private double level; 
}

Obviously, RGB filters are about the simplest kinds of filters to create since you are working with one pixel at a time. More complex filters such as sharpening or blurring filters typically look at the value of the original and (at least) the eight pixels that typically surround the pixel you want to modify. You build one of these more sophisticated image filters by subclassing the ImageFilter class. The reason to use filters (rather than directly manipulating arrays of pixels) is to enable incremental rendering. Since the image can come from a slow network source, it is desirable if the filter processes pixels whenever they are available and passes them on to the image consumer as soon as possible. That way, parts of the image can be displayed as soon as the pixels are downloaded and processed. In contrast, if all pixels of an image need to be available before the image processing starts, then the user viewing the processed image may have to wait a long time before seeing the result.

Unfortunately, writing a good image filter is quite complex. Here is the basic process.

The filter is called repeatedly by the image producer. First, the producer calls the setDimensions, setHints, and setColorModel methods to tell the filter about the image size and pixel delivery. Then, the producer keeps calling setPixels to deliver pixels to the filter. Finally, the producer calls imageComplete to signal completion.

The setHints method tells the filter how the pixels will be delivered. The options are:

  • COMPLETESCANLINES —. The pixels will be delivered in complete scan lines; that is, each time setPixels is called, one or more complete scan lines are delivered.

  • RANDOMPIXELORDER —. The pixels will be delivered in a random order. This happens, for example, for interlaced GIF files.

  • TOPDOWNLEFTRIGHT —. The pixels will be delivered from the top to the bottom, and each delivery contains either complete scan lines or a part of a single scan line, with scan line parts delivered left to right.

  • SINGLEPASS —. The pixels will be delivered in a single pass. This flag is false, for example, for a progressively rendered JPEG, in which a better representation follows an initial approximate representation. When this flag is true, each pixel is delivered exactly once.

SINGLEFRAME —. The image consists of a single frame. This flag is false for an animated GIF.

A filter that wants to support incremental rendering has to implement two separate strategies for processing the image. If the image is delivered in random order (for example, an interlaced GIF), then the image filter must first store all incoming pixels in an array and perform the processing when its imageComplete method is called. However, if the image is delivered in a more regular fashion, then the image filter should process the available pixels immediately.

To further complicate matters, there are two separate setPixels methods:

void setPixels(int x, int y, int w, int h, 
   ColorModel model, int[] pixels, int off, int scansize)

and

void setPixels(int x, int y, int w, int h, 
   ColorModel model, byte[] pixels, int off, int scansize)

One of them delivers the pixels in an int[] array; the other, in a byte[] array. Either one may be called by the image producer, depending on whether pixels are represented as integers (for example, in the default color model) or as bytes (in an indexed color model with, at most, 256 colors).

Finally, note that each call to setPixels can use a different color model! Before pixels are delivered, the image producer may call the setColorModel method to inform the filter which color model is likely to be used by most or all calls to setPixels, but there is no guarantee that all calls to setPixels use that color model. That means that you must translate the incoming pixels to the default color model. Or, if you are really ambitious, you can keep the pixels in the color model that was set by the setColorModel and hope that it does not change— usually it will not. But if it does change, you must immediately translate all cached pixels to the default color model and use the default color model from then on.

Before any pixels are sent, the setDimensions method of the filter is called with the width and height of the image. You can use that information to allocate buffers to hold the pixels.

The image filter needs to tell its consumer about the image that it is about to produce. That is, it must make the following calls to the consumer object:

  • setDimensions

  • setHints

  • setColorModel

  • setPixels

  • imageComplete

The methods of the ImageFilter base class automatically notify the image consumer. In your derived class, you should, therefore, pass these calls on to super. For example, if the filtered image has the same size as the original, then you notify the superclass, which then notifies the consumer.

class MyFilter extends ImageFilter 
{  public void setDimensions(int w, int h) 
   {  width = w; height = h; 
      super.setDimensions(w, h); 
   } 
   . . . 
}

However, you will probably call super.setPixels with some delay, when you have the actual pixels available, or in the imageComplete method, when all source pixels are acquired.

Example 6-3 shows an image filter in action. This filter blurs an image (see Figure 6-4). It does this as follows: For each pixel, we compute the average color values of the pixel and its eight neighboring pixels. Depending on a blur factor, we then replace the pixel with a weighted average of the original pixel and the computed average. If the blur factor is zero, the original pixel is kept, and the result is the original sharp image. If the blur factor is one, then the original image is replaced by the average, and the result is a blurred image. As with the preceding two programs, you can read in new images with the File menu option and adjust the blur factor with the scrollbar.

The blur filter

Figure 6-4. The blur filter

The implementation of this filter follows the general plan that was just described. Before the pixel delivery starts, the setDimensions, setHints, and setColorModel methods are called. We remember the width and height so that we can later allocate buffers of the appropriate size, and we tell the ImageFilter superclass to notify the consumer that the output image has the same size as the input image. We ignore the color model and ask the superclass to tell the consumer that the output image will have the default color model.

If the hints indicate that the input image is delivered in a single pass, in complete scan lines, and top to bottom, then we render the output incrementally. That decision is recorded in the Boolean variable incremental. Otherwise, we buffer the entire input in the bufferPixels array. For incremental rendering, we remember three scan lines at a time in the inPixels array. We can then compute the blur of the middle scan line. That computation is handled in the emitFilteredScanLine method.

The setPixels method feeds the pixels either to the bufferPixels or to the inPixels array, depending on the incremental rendering status. When the output is rendered incrementally, we call the emitFilteredScanLine method each time a new scan line has been acquired. Otherwise, we emit all output scan lines in the imageComplete method.

Note that, in both cases, the output image is rendered in a single pass, in complete scan lines, and top to bottom. In the setHints method, we ask the ImageFilter superclass to give the consumer that happy news.

Example 6-3. BlurFilterTest.java

import java.awt.*; 
import java.awt.event.*; 
import java.awt.image.*; 
import corejava.*; 

public class BlurFilterTest extends CloseableFrame 
   implements ActionListener, AdjustmentListener 
{  public BlurFilterTest() 
   {  MenuBar mbar = new MenuBar(); 
      Menu m = new Menu("File"); 
      MenuItem m1 = new MenuItem("Open"); 
      m1.addActionListener(this); 
      m.add(m1); 
      MenuItem m2 = new MenuItem("Exit"); 
      m2.addActionListener(this); 
      m.add(m2); 
      mbar.add(m); 
      setMenuBar(mbar); 

      scroller = new Scrollbar(Scrollbar.HORIZONTAL, 0, 0, 
         0, 100); 
      scroller.setValue(50); 
      scroller.setBlockIncrement(10); 
      scroller.addAdjustmentListener(this); 
      add(scroller, "South"); 
   } 

   public double getScrollValue() 
   {  return (double)scroller.getValue() 
         / scroller.getMaximum(); 
   } 

   public void adjustmentValueChanged(AdjustmentEvent evt) 
   {  getFilteredImage(getScrollValue(), fileName); 
   } 

   public void actionPerformed(ActionEvent evt) 
   {  String arg = evt.getActionCommand(); 
      if (arg.equals("Open")) 
      {  FileDialog d = new FileDialog(this, 
            "Open file", FileDialog.LOAD); 
         d.setDirectory(lastDir); 
         d.show(); 
         String f = d.getFile(); 
         lastDir = d.getDirectory(); 
         if (f != null) 
         {  fileName = lastDir + f; 
            getFilteredImage(getScrollValue(), fileName); 
         } 
      } 
      else if(arg.equals("Exit")) System.exit(0); 
   } 

   public void getFilteredImage(double d, String file) 
   {  Toolkit tk = Toolkit.getDefaultToolkit(); 
      orig = tk.getImage(file); 
      ImageProducer prod = orig.getSource(); 
      ImageFilter filter = new BlurFilter(d); 
      ImageProducer filteredProd 
         = new FilteredImageSource(prod, filter); 
      filtered = tk.createImage(filteredProd); 
      repaint(); 
   } 

   public void paint(Graphics g) 
   {  g.translate(getInsets().left, getInsets().top); 
      if (filtered != null) 
         g.drawImage(filtered, 0, 0, this); 
   } 

   public static void main(String[] args) 
   {  Frame f = new BlurFilterTest(); 
      f.show(); 
   } 

   private Image orig; 
   private Image filtered; 
   private String fileName = ""; 
   private String lastDir = ""; 
   private Scrollbar scroller; 
} 

class BlurFilter extends ImageFilter 
{  public BlurFilter(double d) 
      throws IllegalArgumentException 
   {  if ((d < 0.0) || (d > 1.0)) 
         throw new IllegalArgumentException(); 
      level = d; 
   } 

   public void setPixels(int x, int y, int w, int h, 
      ColorModel model, byte pixels[], 
      int off, int scansize) 
   {
      if (incremental) 
      {  for (int i = 0; i < h; i++) 
         {  emitFilteredScanLine(); 
            for (int j = 0; j < w; j++) 
            {
               inPixels[2][j] 
                  = model.getRGB(0xFF & pixels[i * scansize 
                     + j + off]); 
            } 
         } 
      } 
      else 
      {  if (bufferPixels == null) 
            bufferPixels = new int[width * height]; 
         for (int i = 0; i < h; i++) 
            for (int j = 0; j < w; j++) 
               bufferPixels[(i + y) * width + j + x] 
                  = model.getRGB(pixels[i * scansize 
                     + j + off]); 
      } 
   } 

   public void setPixels(int x, int y, int w, int h, 
      ColorModel model, int pixels[], 
      int off, int scansize) 
   {  if (incremental) 
      {  for (int i = 0; i < h; i++) 
         {   emitFilteredScanLine(); 
             for (int j = 0; j < w; j++) 
                 inPixels[2][j] 
                    = model.getRGB(pixels[i * scansize 
                       + j + off]); 
         } 
      } 
      else 
      {  if (bufferPixels == null) 
            bufferPixels = new int[width * height]; 
         for (int i = 0; i < h; i++) 
            for (int j = 0; j < w; j++) 
               bufferPixels[(i + y) * width + j + x] 
                  = model.getRGB(pixels[i * scansize 
                     + j + off]); 
      } 
   } 

   public void imageComplete(int status) 
   {  if (status == ImageConsumer.STATICIMAGEDONE 
         || status == ImageConsumer.SINGLEFRAMEDONE) 
      {  if (!incremental) 
         {  for (int i = 0; i < height; i++) 
            {  emitFilteredScanLine(); 
               for (int j = 0; j < width; j++) 
               {  inPixels[2][j] 
                     = bufferPixels[i * width + j]; 
               } 
            } 
         } 
         emitFilteredScanLine(); 
         emitFilteredScanLine(); 
      } 
      super.imageComplete(status); 
   } 

   public void emitFilteredScanLine() 
   {  if (inPixels == null) 
      {  inPixels = new int[3][width]; 
         outPixels = new int[width]; 
         yout = -2; 
      } 
      if (yout >= 0) 
      {  for (int i = 0; i < width; i++) 
         {  int count = 0; 
            int asum = 0; 
            int rsum = 0; 
            int gsum = 0; 
            int bsum = 0; 
            for (int y = -1; y <= 1; y++) 
               for (int x = -1; x <= 1; x++) 
                  if (0 <= yout + y && yout + y < height 
                     && 0 <= i + x && i + x < width) 
                  {  count++; 
                     int p = inPixels[1 + y][i + x]; 
                     asum += (p >> 24) & 0xFF; 
                     rsum += (p >> 16) & 0xFF; 
                     gsum += (p >> 8)  & 0xFF; 
                     bsum += p         & 0xFF; 
                  } 

            int p = inPixels[1][i]; 
            int a = (int)((level * asum) / count 
               + (1 - level) * ((p >> 24) & 0xFF)); 
            int r = (int)((level * rsum) / count 
               + (1 - level) * ((p >> 16) & 0xFF)); 
            int g = (int)((level * gsum) / count 
               + (1 - level) * ((p >> 8) & 0xFF)); 
            int b = (int)((level * bsum) / count 
               + (1 - level) * (p & 0xFF)); 

            outPixels[i] = (a << 24) | (r << 16) 
               | (g << 8) | b; 
         } 
         super.setPixels(0, yout, width, 1, 
            ColorModel.getRGBdefault(), outPixels, 0, width); 
      } 
      int[] temp = inPixels[0]; 
      inPixels[0] = inPixels[1]; 
      inPixels[1] = inPixels[2]; 
      inPixels[2] = temp; 
      yout++; 
   } 

   public void setColorModel(ColorModel model) 
   {  super.setColorModel(ColorModel.getRGBdefault()); 
   } 

   public void setDimensions(int w, int h) 
   {  width = w; height = h; 
      super.setDimensions(w, h); 
   } 

   public void setHints(int h) 
   {  incremental = (h & ImageConsumer.TOPDOWNLEFTRIGHT) != 0 
         && (h & ImageConsumer.COMPLETESCANLINES) != 0 
         && (h & ImageConsumer.SINGLEPASS) != 0; 
      super.setHints(ImageConsumer.TOPDOWNLEFTRIGHT | 
         ImageConsumer.COMPLETESCANLINES); 
   } 

   private boolean incremental = false; 
   private int width; 
   private int height; 
   private double level; 
   private int yout; 
   private int[] bufferPixels; 
   private int[][] inPixels; 
   private int[] outPixels; 
}

Memory Image Sources

As you have seen, image filters can be quite complex. Unless you require incremental rendering, it is usually easier to work with arrays of pixels. Where do these arrays come from? They can come from actual images, or they can be made from scratch, from mathematical formulas, or just by specifying each pixel manually. In any case, you edit an image by the following process:

  1. Obtain an array of pixels.

  2. Transform the array.

  3. Convert the array back into an image.

The last step of this process is carried out by the MemoryImageSource class.

The MemoryImageSource class produces an image from an array of integers. A MemoryImageSource object is an ImageProducer. Therefore, you can obtain an image with the createImage method in the Component or Toolkit class. Alternatively, the MemoryImageSource can become the input of an image filter.

The simplest constructor for a MemoryImageSource object is:

MemoryImageSource(int width, int height, int[] pixels, 
   int offset, int scansize)

This constructor creates an image of size width × height, using the information in the array of pixels interpreted according to the default color model. The offset parameter tells the constructor where to start. (For example, use 0 to start with the beginning of the array.) Finally, the scansize parameter is the number of pixels per line (and therefore is usually equal to the width parameter).

The simplest use of the MemoryImageSource class is to build up an icon directly from an array of pixels. The program in Example 6-4 does just that, drawing a purple letter “C” and a cyan letter “J” whose pixels have been specified directly. For example:

0xFFFF00FF

means a pixel that is opaque and purple, since the opaque, red, and blue values are all set to 255. We scale the image by a factor of 10 so that you can see the individual pixels clearly. Figure 6-5 shows the output of the program.

Constructing an image from individual pixels

Figure 6-5. Constructing an image from individual pixels

Example 6-4. MemoryImageTest.java

import java.awt.*; 
import java.awt.image.*; 
import corejava.*; 

public class MemoryImageTest extends CloseableFrame 
{  public void paint (Graphics g) 
   {  g.translate(getInsets().left, getInsets().top); 
      if (memImage == null) 
         memImage=createImage(new MemoryImageSource(9, 6, 
            pixArray, 0, 9)); 
      g.drawImage(memImage, 0, 0, 90, 60, this); 
      // scale 9 x 6 image to 90 x 60 
   } 

   public static void main (String[] args) 
   {  Frame f = new MemoryImageTest(); 
      f.show(); 
   } 

   private int[] pixArray = 
   {  0, 0xFFFF00FF, 0xFFFF00FF, 0, 0, 
      0xFF007F7F, 0xFF007F7F, 0xFF007F7F, 0xFF007F7F, 
      0xFFFF00FF, 0, 0, 0xFFFF00FF, 0, 
      0, 0, 0xFF007F7F, 0, 
      0xFFFF00FF, 0, 0, 0, 0, 
      0, 0, 0xFF007F7F, 0, 
      0xFFFF00FF, 0, 0, 0, 0, 
      0, 0, 0xFF007F7F, 0, 
      0xFFFF00FF, 0, 0, 0xFFFF00FF, 0, 
      0xFF007F7F, 0, 0xFF007F7F, 0, 
      0, 0xFFFF00FF, 0xFFFF00FF, 0, 0, 
      0, 0xFF007F7F, 0, 0 
   }; 


   private Image memImage; 
}

A Formula-based Example: A Black-and-White Mandelbrot Set

Obviously, building a larger image by translating the image pixel by pixel directly into an array would be rather unpleasant. (It wouldn’t be hard to build an icon editor program to do this, of course. Ultimately, all non-vector-based paint programs are working on a pixel-by-pixel level.) Usually, however, you build up the MemoryImageSource mathematically via some formula. In this section, we bow to tradition and do a Mandelbrot set, as shown in Figure 6-6. To keep the code down to its essentials, we stick to black and white only.

A Mandelbrot set

Figure 6-6. A Mandelbrot set

The idea of the Mandelbrot set is that you use an iterative process to transform points according to a formula that comes ultimately from the mathematics of complex numbers. For the simplest Mandelbrot set, you iterate:

xValue = (xValue * xValue) – (yValue * yValue) + xCoord; 
yValue = (2 * xValue * yValue) + yCoord;

and check whether the point “escapes to infinity” [1]. The idea of the following code is that we place a 0xFFFFFFFF in the pixel array if the point (x,y) “escapes to infinity.” We build up the array point by point in a routine we call pixArray, which returns an array of integers. Example 6-5 shows the code.

Example 6-5. MandelbrotTest.java

import java.awt.*; 
import java.awt.image.*; 
import corejava.*; 

public class MandelbrotTest extends CloseableFrame 
{  public static void main (String[] args) 
   {  Frame f = new MandelbrotTest(300, 300, 2.0, 2.0); 
      f.setSize(400,400); 
      f.show(); 
   } 

   public MandelbrotTest(int h, int w, double x, double y) 
   {  width = w; 
      height = h; 
      xSize = x; 
      ySize = y; 
      xOffset = (2.0 * xSize) / width; 
      yOffset = (2.0 * ySize) / height; 
      memImage = createImage(new MemoryImageSource(width, 
         height, pixArray(), 0, width)); 
   } 

   public void update(Graphics g) 
   // to avoid flicker--see vol. 1 ch. 7 
   {  paint(g); 
   } 

   public void paint(Graphics g) 
   {  g.translate(getInsets().left, getInsets().top); 
      g.drawImage(memImage, 0, 0, null); 
   } 

   public int[] pixArray() 
   {  int[] pixels = new int[height * width]; 
      for (int i = 0; i < height; i++) 
         for (int j = 0; j < width; j++) 
            if (escapesToInfinity(i, j)) 
               pixels[i * width + j] = 0xFF000000; 
            else 
               pixels[i * width + j] = 0xFFFFFFFF; 
      return pixels; 
   } 

   private boolean escapesToInfinity(int row, int col) 
   {  double xCoord = - xSize + col * xOffset; 
      double yCoord = - ySize + row * yOffset; 
      double xValue = 0.0; 
      double yValue = 0.0; 
      int iterations = 0; 
      while (iterations < MAX_ITERATIONS) 
      {  xValue = (xValue * xValue) 
            - (yValue * yValue) + xCoord; 
         yValue = (2 * xValue * yValue) + yCoord; 
         if (xValue > 2 || yValue > 2) break; 
         iterations++; 
      } 
      return iterations == MAX_ITERATIONS; 
   } 

   private Image memImage; 
   private int width; 
   private int height; 
   private double xSize; 
   private double ySize; 
   private double xOffset; 
   private double yOffset; 
   private int MAX_ITERATIONS = 16; 
}

Memory Source Animations

One nice feature added to the MemoryImageSource class in Java 1.1 is the ability to use the class to do animations. To do an animation, you simply:

  1. Construct a MemoryImageSource from an array of pixels.

  2. Call setAnimated(true).

  3. Attach one or more image consumers to the MemoryImageSource.

  4. Repeat the following for each image consumer:

    1. Update the contents of the pixel array.

    2. Call newPixels.

    3. Repaint the image.

    4. Sleep for a short time.

The following “poor man’s morphing” program uses the animation feature in MemoryImageSource. The program works like this: We start with an original image and a final image that are of the same size. In our example, the original image is a purple square and the final image is a blue-green circle (see Figure 6-7).

The morphing program

Figure 6-7. The morphing program

We interpolate between the original pixel and the final pixel via the standard formula

intermediateValue = (1 - t) * initialValue + t * finalValue

so that at time t = 0 we use the original value and at time t = 1 we use the final value. This interpolation needs to be carried out separately for the red, green, and blue color values.

The animation is placed into a separate thread that keeps going back and forth between the original and final image. As you can see, the animation works well, and it is very easy to program. But the interpolation does not give a Hollywoodlike morphing effect. For a really good morphing effect, one must work harder and actually gradually transform the shapes, not just interpolate the pixel colors.

Example 6-6 shows the animation program.

Example 6-6. Morph.java

import java.util.*; 
import java.awt.*; 
import java.awt.image.*; 
import java.net.*; 
import corejava.*; 
public class Morph extends CloseableFrame implements Runnable 
{  public Morph() 
   {  originalPixels = new int[SIZE * SIZE]; 
      for (int x = 0; x < SIZE; x++) 
         for (int y = 0; y < SIZE; y++) 
            if (inSquare(x, y)) 
               originalPixels[y * SIZE + x] = 0xFFFF00FF; 
            else 
               originalPixels[y * SIZE + x] = 0xFFFFFFFF; 

      finalPixels = new int[SIZE * SIZE]; 
      for (int x = 0; x < SIZE; x++) 
         for (int y = 0; y < SIZE; y++) 
            if (inCircle(x, y)) 
               finalPixels[y * SIZE + x] = 0xFF007F7F; 
            else 
               finalPixels[y * SIZE + x] = 0xFFFFFFFF; 

      anim = new Thread(this); 
      anim.start(); 
   } 

   private boolean inSquare(int x, int y) 
   {  return SIZE / 10 <= x && x <= SIZE * 9 / 10 
         && SIZE / 10 <= y && y <= SIZE * 9 / 10; 
   } 

   private boolean inCircle(int x, int y) 
   {  double dx = x - SIZE / 2; 
      double dy = y - SIZE / 2; 
      return (dx * dx) / (SIZE * SIZE / 4) + 
         (dy * dy) / (SIZE * SIZE / 4) <= 1; 
   } 

   public static void main(String[] args) 
   {  Frame f = new Morph(); 
      f.show(); 
   } 

   public void run() 
   {  intermediatePixels = new int[SIZE * SIZE]; 
      System.arraycopy(originalPixels, 0, 
         intermediatePixels, 0, SIZE * SIZE); 
      MemoryImageSource mis 
         = new MemoryImageSource(SIZE, SIZE, 
            intermediatePixels, 0, SIZE); 
      mis.setAnimated(true); 
      theImage = createImage(mis); 
      repaint(); 
      try 
      {  double t = 0; 
         int direction = 1; 
         while (true) 
         {  t = t + direction * 0.05; 
            if (t <= 0 || t >= 1) direction *= -1; 
            interpolatePixels(t); 
            mis.newPixels(); 
            repaint(); 
            anim.sleep(100); 
         } 
      } 
      catch(InterruptedException e){} 
   } 

   public void paint(Graphics g) 
   {  g.translate(getInsets().left, getInsets().top); 
      g.drawImage(theImage, 0, 0, SIZE, SIZE, null); 
   } 

   public void update(Graphics g) 
   {  paint(g); 
   } 

   private void interpolatePixels(double t) 
   {  for(int i = 0; i < SIZE * SIZE; i++) 
      {  int p = originalPixels[i]; 
         int q = finalPixels[i]; 

         int r = (int)((1 - t) * (p & 0xFF0000) 
            + t * (q  & 0xFF0000)) & 0xFF0000; 
         int g = (int)((1 - t) * (p & 0xFF00) 
            + t * (q & 0xFF00)) & 0xFF00; 
         int b = (int)((1 - t) * (p & 0xFF) 
            + t * (q & 0xFF)); 

         intermediatePixels[i] = 0xFF000000 | r | g | b; 
      } 
   } 

   private Image theImage; 
   private int[] originalPixels; 
   private int[] finalPixels; 
   private int[] intermediatePixels; 
   private Thread anim; 
   private static final int SIZE = 100; 
}

Pixel Grabbing

As we already mentioned, the ImageFilter classes are useful for incremental filtering, but they can be difficult to implement. It is usually much easier to manipulate the image after it has been loaded into an array of pixels. In the preceding section, you saw how to create such an array from scratch. In this section, you will see how a PixelGrabber can grab the pixels of an existing image and put them into an array.

The most convenient way to grab the pixels of an image is as follows:

  1. Use the constructor

    PixelGrabber(image, 0, 0, -1, -1, true)

    Then, all pixels of the image will grabbed. The last parameter indicates all pixels should be converted to the default color model.

  2. Call the grabPixels method and check that it returns true.

  3. Obtain the dimensions of the grabbed image with the getWidth and getHeight methods.

  4. Call getPixels to get the pixel array. You need to cast the return value to int[]. (If you don’t convert pixels to the default color model and the color model of the image is an index color model with, at most, 256 colors, then the method returns a byte[] array instead. For that reason, the return type of getPixels is declared as an Object, and you must cast it to the appropriate array type.)

  5. Access individual pixels. The pixel with coordinates (x,y) is stored in

    pixels[y * width + x].

The code for this sequence looks like this:

try 
{  PixelGrabber grabber = new PixelGrabber(image, 0, 0, -1, -1, 
      true) 
   if (grabber.grabPixels()) 
   {  int width = grabber.getWidth() 
      int height = grabber.getHeight(); 
      int[] pixels = (int[])grabber.getPixels(); 
      for (int x = 0; x < width; x++) 
         for (int y = 0; y < height; y++) 
            do something with pixels[y * width + x]; 
   } 
} 
catch(InterruptedException e) {}

In Example 6-7, we will transpose an image, that is, flip the x- and y-coordinates of every pixel (see Figure 6-8). To achieve this, we grab the pixels of an image, compute the transposed pixels, and use the MemoryImageSource class of the preceding section to turn the transposed pixels back into an image.

Transposing the pixels of an image

Figure 6-8. Transposing the pixels of an image

In this case, there is no benefit to incremental rendering because all pixels must be available before the transformed image can be computed. Therefore, it makes sense to use the pixel grabber instead of an incremental filter.

Example 6-7. PixelGrabberTest.java

import java.awt.*; 
import java.awt.event.*; 
import java.awt.image.*; 
import corejava.*; 

public class PixelGrabberTest extends CloseableFrame 
   implements ActionListener 
{  public PixelGrabberTest() 
   {  MenuBar mbar = new MenuBar(); 
      Menu m = new Menu("File"); 
      MenuItem m1 = new MenuItem("Open"); 
      m1.addActionListener(this); 
      m.add(m1); 
      MenuItem m2 = new MenuItem("Exit"); 
      m2.addActionListener(this); 
      m.add(m2); 
      mbar.add(m); 
      setMenuBar(mbar); 
   } 

   public void actionPerformed(ActionEvent evt) 
   {  String arg = evt.getActionCommand(); 
      if (arg.equals("Open")) 
      {  FileDialog d = new FileDialog(this, 
            "Open file", FileDialog.LOAD); 
         d.setDirectory(lastDir); 
         d.show(); 
         String f = d.getFile(); 
         lastDir = d.getDirectory(); 
         if (f != null) 
         {  fileName = lastDir + f; 
            transposeImage(fileName); 
         } 
      } 
      else if(arg.equals("Exit")) System.exit(0); 
   } 

   public void transposeImage(String f) 
   {  Image image = Toolkit.getDefaultToolkit().getImage(f); 
      PixelGrabber grabber 
         = new PixelGrabber(image, 0, 0, -1, -1, true); 
      try 
      {  if (grabber.grabPixels()) 
         {  width = grabber.getWidth(); 
            height = grabber.getHeight(); 
            int[] pixels = (int[])grabber.getPixels(); 
            int[] transposedPixels = new int[height * width]; 
            for (int x = 0; x < width; x++) 
               for (int y = 0; y < height; y++) 
                  transposedPixels[x * height + y] 
                     = pixels[y * width + x]; 
            transposedImage = createImage(new 
               MemoryImageSource(height, width, 
                  transposedPixels, 0, height)); 
            repaint(); 
         } 
      } 
      catch(InterruptedException e) {} 
   } 

   public void paint (Graphics g) 
   {  g.translate(getInsets().left, getInsets().top); 
      if (transposedImage != null) 
         g.drawImage(transposedImage, 0, 0, 
            height, width, this); 
   } 

   public static void main (String[] args) 
   {  Frame f = new PixelGrabberTest(); 
      f.show(); 
   } 

   private Image transposedImage; 
   private int width; 
   private int height; 
   private String fileName = ""; 
   private String lastDir = ""; 
}

Data Transfer

One of the most powerful and convenient user interface mechanisms of graphical user interface environments (such as Windows and X Window System) is cut and paste. You select some data in one program and cut or copy it to the clipboard. Then, you select another program and paste the clipboard contents into that application. Using the clipboard, you can transfer text, images, or other data from one document to another, or, of course, from one place in a document to another place in the same document. Cut and paste is so natural that most computer users never think about it.

However, in Java 1.0, there was no support for cut and paste. You could not cut and paste between Java applications. For example, if you have a browser written in Java (such as HotJava), then you could not copy text and images from a Web page and transfer them into a Java-based word processor.

Actually, there is one form of cut and paste that was always supported: you can cut and paste text in text fields and text areas as long as the peer classes that implement the text edit components can communicate with the system clipboard.

Even though the clipboard is conceptually simple, implementing clipboard services is actually harder than you might think. Suppose you copy text from a word processor into the clipboard. If you paste that text into another word processor, then you expect that the fonts and formatting will stay intact. That is, the text in the clipboard needs to retain the formatting information. But if you paste the text into a plain text field, then you expect that just the characters are pasted in, without additional formatting codes.

The situation is even more complicated for graphics. Many graphics programs produce images that can be scaled to different sizes without quality loss. These images are made up of elements such as lines, circles, text, and so on, that can be scaled to different sizes. Other graphics programs treat images as collections of pixels. These images cannot be scaled without introducing “jaggies.” Suppose a scalable image, say, a flowchart, is copied onto the clipboard. Then, it is pasted into another program. If the target program is a bitmap editor, then the image should be pasted as a bitmap. If the target program is a word processor, it would be better to paste the image as a graphics metafile—a file format that retains the geometric elements of the image.

The system clipboard of a graphical user interface environment can hold the clipboard data in multiple formats, and the program that provides the data must negotiate the data format with the program that consumes the data.

Furthermore, individual programs usually have their own local clipboards. When you cut and paste text inside a word processor, then the word processor saves the data in its own format in a local clipboard. There is no universal standard for exchanging formatted text. When text is pasted onto the system clipboard, some formatting information is usually lost. While this loss is tolerable for text interchange from one word processor to another, it does not suffice for moving data within one word processor. Program users have the legitimate expectation that no information is lost when they move information from one place in a document to another place in the same document. To achieve perfect information transfer within a program, a local clipboard is required whenever that information is richer than the data that can be stored in the clipboard.

The system clipboard implementations of Microsoft Windows, OS/2 and the Macintosh are similar, but, of course, there are slight differences. And the X Windows clipboard mechanism is much more limited—cutting and pasting of anything but plain text is only sporadically supported. These differences are a major challenge for Java programs. A Java program is supposed to run unmodi-fied on many platforms. As so often, Java must cater to the lowest common denominator, and again, as so often, that lowest common denominator is the X Window system. Since the X Window system has no standard method for storing formatted text or graphics on the clipboard, Java programs are currently limited to storing only plain text on the system clipboard. You will learn in this chapter how a Java program can place text onto the system clipboard and how to retrieve text that was placed there by another program.

However, when copying data from one Java program to another, you can overcome the plain text limitation. Consider how attachments are sent by e-mail. E-mail messages can hold only text, and some mail gateways are quite restrictive about the maximum length of each text line. But e-mail systems have learned to cope with these limitations. Attachments, which can consist of binary data, are transferred in MIME (Multipurpose Internet Mail Extensions) format.

NOTE

NOTE

For an HTML version of the RFC (Request for Comment) that defines the MIME format, see, for example, http://www.oac.uci.edu/indiv/ehood/MIME.

MIME-compliant mailers know how to encode attachments into plain text and how to decode the MIME format and present the attachments to the user. Of course, if you use an ancient mail reader (such as the original Unix mail program), then the attachments will simply show up as strange-looking text.

It is possible to use the same approach to transfer data from one MIME-aware Java program to another by placing encoded data onto the clipboard. Of course, it makes no sense to paste that text into a non-Java program. But if the target Java program can understand the header that describes the encoding format, then it can decode the information. Currently, Java has no support for this transfer method. But since Java does use MIME data types to describe data formats, we found it reasonable to encode clipboard data in MIME format as well. We give you a sample program that shows how you can easily exchange arbitrary serializable objects between Java applications through the system clipboard by using this approach. Perhaps a later version of Java will officially use a similar mechanism for data transfer between Java programs.

Finally, Java supports a local clipboard that you can use to store arbitrary Java objects. Naturally, this is not nearly as difficult as transferring data between programs. Why can’t you simply store the clipboard data in a global variable? The Java mechanism also supports format negotiation. (In Java, the data formats are called flavors.)

Table 6-1 summarizes the data transfer capabilities of the new clipboard mechanism.

Table 6-1. Capabilities of the Java data transfer mechanism

Transfer

Format

Between Java and native program

Plain text

Between two MIME-aware Java programs

Text-encoded data

Within one Java program

Any object

Classes and Interfaces for Data Transfer

Data transfer in Java is implemented in a package called java.awt.datatransfer that has three classes, two interfaces, and a new checked exception class. Table 6-2 contains brief descriptions of the parts of this package.

Table 6-2. Classes in the java.awt.datatransfer package

Transferable interface

Objects that can be transferred must implement this interface.

Clipboard class

A class that encapsulates a clipboard. Transferable objects are the only items that can be put on or taken off a clipboard. The system clipboard is a concrete example of a Clipboard.

ClipboardOwner interface

A class that wants to be able to copy data to a clipboard must implement this interface.

DataFlavor class

A way of identifying the type (or “flavor”) of the data that was placed on a clipboard.

StringSelection class

The only concrete class that Java 1.1 supplies that implements Transferable and, as a convenience, ClipboardOwner. Used to transfer text strings.

UnsupportedFlavorException

Thrown by a Transferable when it can’t give you data in that flavor.

Transferring Text

The best way to get comfortable with the data transfer classes is to start with the simplest (and, therefore, only) supported situation for Java 1.1: transferring text to and from the system clipboard. The idea of the following program is simple. First we get a reference to the system clipboard:

Clipboard sysClipboard = 
   Toolkit.getDefaultToolkit().getSystemClipboard();

For strings to be transferred to the clipboard, they need to be wrapped into StringSelection objects. The constructor takes the text you want to transfer. The actual transfer is done by a call to setContents, which takes a StringSelection object and a ClipBoardOwner as parameters.

The second parameter is used as the target of the lostOwnership callback when the contents of the clipboard change. This callback is necessary to support “delayed formatting” of complex data. If data is simple (such as a string), then it is simply placed on the clipboard and the class that placed it there moves on to do the next thing. However, if a class wants to place complex data that can be formatted in multiple flavors onto the clipboard, then it may not actually want to prepare all the flavors, since there is a good chance that most of them are never needed. However, then it needs to hold on to all information that is needed to create the flavors later when they are requested. The lostOwnership callback can release that information since it is no longer needed when a different item is placed onto the clipboard. If no callback is needed, you can simply set the second parameter of setContents to null.

private void copyIt() 
{  String text = textArea.getSelectedText(); 
   StringSelection selection = new StringSelection(text); 
   sysClipboard.setContents(selection, null); 
}

Finally, to get the contents, we first need to get a Transferable by a call to getContents(). Once we have the Transferable, we use its getTransferData method to actually pick up the information. This method has to be encased in a try/catch block because of the possibility of throwing an UnsupportedFlavorException.

private void pasteIt() 
{  String text; 
   Transferable selection = sysClipboard.getContents(this); 
   {  try 
      {  text = (String)(selection.getTransferData 
            (DataFlavor.stringFlavor)); 
         int start = textArea.getSelectionStart(); 
         int end = textArea.getSelectionEnd(); 
         textArea.replaceRange(text, start, end); 
      } 
      catch(Exception e) {} 
   } 
}

Example 6-8 is a program that demonstrates cutting and pasting between a Java application and the system clipboard. Figure 6-9 shows a screen shot. If you select an area of text in the text area and click on Copy, then the selected text is copied to the system clipboard. As Figure 6-10 shows, the copied text does indeed get stored on the system clipboard. When you subsequently click on the Paste button, the contents of the clipboard (which may come from a non-Java program) are pasted at the cursor position.

The TextTransferTest program

Figure 6-9. The TextTransferTest program

The Windows clipboard viewer after a copy

Figure 6-10. The Windows clipboard viewer after a copy

Example 6-8. TextTransferTest.java

import java.io.*; 
import java.awt.*; 
import java.awt.datatransfer.*; 
import java.awt.event.*; 
import corejava.*; 


public class TextTransferTest 
   extends CloseableFrame implements ActionListener 
{  public TextTransferTest() 
   {  add (textArea, "Center"); 
      Panel p = new Panel(); 
      Button copy = new Button ("Copy"); 
      p.add(copy); 
      copy.addActionListener(this); 
      Button paste = new Button ("Paste"); 
      p.add (paste); 
      paste.addActionListener(this); 
      add (p, "South"); 
      sysClipboard 
         = Toolkit.getDefaultToolkit().getSystemClipboard(); 
   } 

   public void actionPerformed(ActionEvent evt) 
   {  String arg = evt.getActionCommand(); 
      if (arg.equals("Copy")) copyIt(); 
      else if (arg.equals("Paste")) pasteIt(); 
   } 

   private void copyIt() 
   {  String text = textArea.getSelectedText(); 
      if (text.equals("")) text = textArea.getText(); 
      StringSelection selection = new StringSelection(text); 
      sysClipboard.setContents(selection, null); 
   } 

   private void pasteIt() 
   {  String text; 
      Transferable selection = sysClipboard.getContents(this); 
      {  try 
         {  text = (String)(selection.getTransferData 
               (DataFlavor.stringFlavor)); 
            int start = textArea.getSelectionStart(); 
            int end = textArea.getSelectionEnd(); 
            textArea.replaceRange(text, start, end); 
         } 
         catch(Exception e) 
         {} 
      } 
   } 

   public static void main(String[] args) 
   {  Frame f = new TextTransferTest(); 
      f.show(); 
   } 

   private TextArea textArea = new TextArea(); 
   private Clipboard sysClipboard; 
}

Building a Transferable

Objects that you want to transfer via the clipboard must implement the Transferable interface. The StringSelection class is currently the only class in the Java standard library that implements the Transferable interface. We will first build an ImageSelection class that can be used to transfer images through the local clipboard. Finally, we will build up the machinery to construct a Transferable for serializable Java objects.

The Transferable Interface and Data Flavors

The Transferable interface has three methods, two of which help you discover what flavors the transferred object supports. These methods let you find out whether data stored on a clipboard is useable by a specific part of the program that can handle data only of a certain type. For example, if you had a bitmap image on the clipboard, it could be delivered either as an object of type Image or as a GIF file. In this case, the Clipboard object would report that it comes in two flavors. Flavors are represented by instances of the DataFlavor class.

You can find out all data flavors that a transferable object supports:

DataFlavor[] flavors = transferable.getTransferDataFlavors()

Or, you can test whether a particular flavor is supported.

DataFlavor flavor = . . .; 
if (transferable.isDataFlavorSupported(flavor)) { . . . }

Finally, there is a method to actually transfer the data.

Object data = transferable.getTransferData(flavor);

Of course, this is where all the real work will need to be done. This method will need to look at the flavor and return an object that contains the transfer data formatted according to the flavor. The method throws an UnsupportedFlavorException if the flavor is not supported.

Currently, the DataFlavor class has two constructors. Objects can be constructed with two names, a human-readable name (such as "GIF Bitmap" ) and a MIME type name (such as "image/gif" ). For example,

DataFlavor gifFlavor 
   = new DataFlavor("image/gif", "GIF Bitmap");

When data of this type is retrieved, the Transferable object must yield an InputStream object from which the data can be read.

InputStream in 
   = (InputStream)transferable.getTransferData(gifFlavor);

It is then up to the application to turn the stream into an image object.

Of course, it is more convenient to retrieve Java objects directly from the clipboard. Since Java objects do not yet have a standard MIME type, JavaSoft has created one:

application/x-java-serialized-object; class=classname

(The x- prefix indicates that this is an experimental name, not one that is sanctioned by IANA, the organization that assigns standard MIME type names.) For example, the content type of an Image object would be:

application/x-java-serialized-object; class=java.awt.Image

Such a data flavor is constructed as

DataFlavor imageFlavor 
   = new DataFlavor(java.awt.Image.class, "AWT Image");

This flavor can be retrieved easily:

Image img = (Image)transferable.getTransferData(imageFlavor);

Building an Image Transferable

Let us put the information of the preceding section to work and design a transferable image class. Right now, we only want to transfer images inside a single Java program; that is, we will just use a local clipboard.

Here’s what you need to do:

  1. Define a class ImageSelection that implements the Transferable interface.

    class ImageSelection implements Transferable { . . . };
  2. Define the single supported flavor as a class flavor for the class java.awt.image, with a human-presentable name of "AWT Image". Construct a static object imageFlavor to represent that flavor.

    public static final DataFlavor imageFlavor 
       = new DataFlavor(java.awt.Image.class, "AWT Image");
  3. Make the getTransferDataFlavors method return an array with the single entry, imageFlavor of type DataFlavor.

    private static DataFlavor[] flavors = { imageFlavor }; 
    public DataFlavor[] getTransferDataFlavors() 
    {  return flavors; 
    }
  4. Have the isDataFlavorSupported check whether the requested flavor is equal to the imageFlavor object.

    public boolean isDataFlavorSupported(DataFlavor flavor) 
    {  return flavor.equals(imageFlavor); 
    }
  5. Have the ImageSelection constructor accept and store an object of type Image.

    public ImageSelection(Image image) 
    {  theImage = image; 
    } 
    private Image theImage;
  6. Return a reference to the stored Image object as the value of getTransferData.

    public synchronized Object getTransferData 
       (DataFlavor flavor) 
       throws UnsupportedFlavorException 
    {  if(flavor.equals(imageFlavor)) 
       {  return theImage; 
       } 
       else 
       {  throw new UnsupportedFlavorException(flavor); 
       } 
    }

See Example 6-9 for the complete source code.

Of course, all this is a bit underwhelming. The ImageSelection class is simply a wrapper for Image objects. This class, to be more interesting, would need to be able to deliver its contents in more than one way, for example, as an Image object and a GIF file. Then, you would see some real work in the getTransferData method. It would look like this:

public synchronized Object getTransferData 
   (DataFlavor flavor) 
   throws UnsupportedFlavorException 
{  if(flavor.equals(imageFlavor)) 
   {  return theImage; 
   } 
   else if(flavor.equals(gifFlavor)) 
   {  byte[] gifBytes = . . .; // translate image to GIF 
         format 
      return new ByteArrayInputStream(gifBytes); 
   } 
   else 
   {  throw new UnsupportedFlavorException(flavor); 
   } 
}

We will not do work that here, since it is a bit tedious to compute the GIF format of an image.

Using the ImageSelection Class

The program of Example 6-9 creates two windows. You can load an image file into each window, or you can copy and paste between the windows, with the “Edit|Copy” and “Edit|Paste” menu options (see Figure 6-11). Coding the copy and paste operations using the ImageSelection class is not much different than using the StringSelection class.

The ImageTransferTest program

Figure 6-11. The ImageTransferTest program

private void copyIt() 
{  ImageSelection selection = new ImageSelection(theImage); 
   localClipboard.setContents(selection, null); 
} 

private void pasteIt() 
{  Transferable selection 
      = localClipboard.getContents(this); 
   try 
   {  theImage = (Image)selection.getTransferData 
         (ImageSelection.imageFlavor); 
      repaint(); 
   } 
   catch(Exception e) {} 
}

In this case, selection is an ImageSelection instance. We obviously also need to cast the return value of getTransferData to an Image this time rather than to a string, and use a call to repaint() rather than adding the result to the text area. Note that we are using a local clipboard rather than the system clipboard. It is constructed as

static Clipboard localClipboard = new Clipboard("local");

We cannot use the system clipboard because, at this time, Java can transfer only text into the system clipboard.

Example 6-9 shows the full code for the example.

Example 6-9. ImageTransferTest.java

import java.awt.*; 
import java.awt.image.*; 
import java.awt.event.*; 
import java.awt.datatransfer.*; 
import corejava.*; 

public class ImageTransferTest extends CloseableFrame 
   implements ActionListener 
{  public ImageTransferTest() 
   {  MenuBar mbar = new MenuBar(); 
      Menu m = new Menu("File"); 
      MenuItem m1 = new MenuItem("Open"); 
      m1.addActionListener(this); 
      m.add(m1); 
      MenuItem m2 = new MenuItem("Exit"); 
      m2.addActionListener(this); 
      m.add(m2); 
      mbar.add(m); 
      m = new Menu("Edit"); 
      MenuItem m3 = new MenuItem("Copy"); 
      m3.addActionListener(this); 
      m.add(m3); 
      MenuItem m4 = new MenuItem("Paste"); 
      m4.addActionListener(this); 
      m.add(m4); 
      mbar.add(m); 
      setMenuBar(mbar); 
   } 

   public void actionPerformed(ActionEvent evt) 
   {  String arg = evt.getActionCommand(); 
      if (arg.equals("Open")) 
      {  FileDialog d = new FileDialog(this, 
            "Open file", FileDialog.LOAD); 
         d.setDirectory(lastDir); 
         d.show(); 
         String f = d.getFile(); 
         lastDir = d.getDirectory(); 
         if (f != null) 
         {  theImage = Toolkit.getDefaultToolkit().getImage 
               (lastDir + f); 
            repaint(); 
         } 
      } 
      else if(arg.equals("Exit")) System.exit(0); 
      else if (arg.equals("Copy")) copyIt(); 
      else if (arg.equals("Paste")) pasteIt(); 
   } 

   public void paint(Graphics g) 
   {  g.translate(getInsets().left, getInsets().top); 
      if(theImage != null) 
         g.drawImage(theImage, 0, 0, this); 
   } 

   private void copyIt() 
   {  ImageSelection selection = new ImageSelection(theImage); 
      localClipboard.setContents(selection, null); 
   } 
   private void pasteIt() 
   {  Transferable selection 
         = localClipboard.getContents(this); 
      try 
      {  theImage = (Image)selection.getTransferData 
            (ImageSelection.imageFlavor); 
         repaint(); 
      } 
      catch(Exception e) {} 
   } 

   public static void main(String [] args) 
   {  Frame f1 = new ImageTransferTest(); 
      Frame f2 = new ImageTransferTest(); 
      f1.setTitle("Frame 1"); 
      f2.setTitle("Frame 2"); 
      f2.setLocation(300, 100); 
      f1.show(); 
      f2.show(); 
   } 

   private static Clipboard localClipboard 
      = new Clipboard("local"); 
   private Image theImage; 
   private String lastDir = ""; 
} 

class ImageSelection implements Transferable 
{  public ImageSelection(Image image) 
   {  theImage = image; 
   } 

   public DataFlavor[] getTransferDataFlavors() 
   {  return flavors; 
   } 

   public boolean isDataFlavorSupported(DataFlavor flavor) 
   {  return flavor.equals(imageFlavor); 
   } 

   public synchronized Object getTransferData 
      (DataFlavor flavor) 
      throws UnsupportedFlavorException 
   {  if(flavor.equals(imageFlavor)) 
      {  return theImage; 
      } 
      else 
      {  throw new UnsupportedFlavorException(flavor); 
      } 
   } 
   public static final DataFlavor imageFlavor 
      = new DataFlavor(java.awt.Image.class, "AWT Image"); 

   private static DataFlavor[] flavors = { imageFlavor }; 
   private Image theImage; 
}

Transferring Java Objects via the System Clipboard

Now, suppose you want to paste the images by using the system clipboard rather than a private clipboard. This capability would have the nice consequence that the image would persist and so you could paste images between different Java programs even if the original program was over. (This is better than using RMI: RMI certainly allows you to transfer images between Java programs, but RMI requires both programs to be running at the same time.)

Unfortunately, if you try to place a Transferable other than a StringSelection on the system clipboard, you will find that nothing happens. The system clipboard doesn’t have a clue how to store a Java object, and it simply ignores the request. The result is you can’t get the object back off the system clipboard because it never really got there in the first place!

In this section, we show you how to overcome this limitation by encoding objects into text strings and placing those text strings onto the system clipboard. This method works for any serializable Java object. We choose the following simple text encoding:

Content-type: application/x-java-serialized-object 
Content-length: length 

serialized object data in BASE64 encoding

For example,

Content-type: application/x-java-serialized-object 
Content-length: 80311 

rO0ABXNyAAZCaXRtYXA8A5/mgeUpsAIAA0kABmhlaWdodEkABXdpZHRoWwAGcGl4ZWxzdAACW0l4 
cAAAAIwAAABqdXIAAltJTbpgJnbqsqUCAAB4cAAAOfj///////////////////////////////// 
. . .

The BASE64 encoding is a commonly used method to encode binary data as printable characters. The exact encoding scheme is not important. You can find a description in the MIME RFC, and the code in Example 6-10 contains classes to carry out the encoding and decoding.

Because the system clipboard class can put only text onto the clipboard, we designed a special MimeClipboard class. This class delegates storage and retrieval requests to another clipboard, typically the system clipboard, and it handles the data encoding and decoding.

class MimeClipboard extends Clipboard 
{  public MimeClipboard(Clipboard cb) 
   {  . . . 
      clip = cb; 
   } 

   public synchronized void setContents(Transferable contents, 
      ClipboardOwner owner) 
   {encode data and put on clip 
   } 

   public synchronized Transferable getContents 
      (Object requestor) 
   {get data from clip and decode 
   } 

   private Clipboard clip; 
}

We also use a transfer wrapper SerializableSelection for serializable objects—it is exactly analogous to the ImageSelection class of the preceding section. When a Transferable object is placed onto a MimeClipboard, the following happens:

  1. If the Transferable object is a StringSelection, then it is simply put on the clipboard.

  2. Otherwise, if the Transferable is of the type SerializableSelection, then the object is serialized into a sequence of bytes. The serialized bytes are encoded in BASE64, the MIME header is added, and the resulting string is placed on the clipboard.

    public synchronized void setContents(Transferable 
       contents, ClipboardOwner owner) 
    {  if (contents instanceof SerializableSelection) 
       {  try 
          {  DataFlavor flavor 
                = SerializableSelection.serializableFlavor; 
             Serializable obj = (Serializable) 
                contents.getTransferData(flavor); 
             String enc = encode(obj); 
             String header = "Content-type: " 
                + flavor.getMimeType() 
                + "
    Content-length: " 
                + enc.length() + "
    
    "; 
             StringSelection selection 
                = new StringSelection(header + enc); 
             clip.setContents(selection, owner); 
          } 
          catch(UnsupportedFlavorException e) {} 
          catch(IOException e) {} 
       } 
       else clip.setContents(contents, owner); 
    }

When a Transferable object is read from the clipboard, these steps are reversed.

  1. The StringSelection is obtained from the system clipboard.

  2. If the string doesn’t start with Content-type, then the string selection object is returned. (A more sophisticated implementation should be able to handle the various content types and return data other than Java serialized objects as input streams.)

  3. Otherwise, the BASE64 data is converted to binary and read as object stream data. The resulting object is wrapped into a SerializableSelection transfer object, which is returned.

    public synchronized Transferable getContents 
       (Object requestor) 
    {  Transferable contents = clip.getContents(requestor); 
    
       if (contents instanceof StringSelection) 
       {  String data = (String)contents.getTransferData 
                (DataFlavor.stringFlavor); 
    
          if (!data.startsWith("Content-type: ")) 
             return contents; 
          int start = . . .; // skip three newlines 
          Serializable obj = decode(data, start); 
          SerializableSelection selection 
             = new SerializableSelection(obj); 
          return selection; 
       } 
       else return contents; 
    }

You use this clipboard just like any other clipboard:

Clipboard mimeClipboard 
      = new MimeClipboard 
         (Toolkit.getDefaultToolkit().getSystemClipboard()); 
. . . 
private void copyIt() 
{  SerializableSelection selection 
      = new SerializableSelection(theBitmap); 
   mimeClipboard.setContents(selection, null); 
}

To encode an object, we serialize it to a stream. The Base64OutputStream class encodes all bytes written to it into BASE64. We layer an ObjectOutputStream on top of it. Here is the code from the encode method.

StringBuffer sbuf = new StringBuffer(); 
Base64OutputStream bout 
   = new Base64OutputStream(sbuf); 
ObjectOutputStream out 
   = new ObjectOutputStream(bout); 
out.writeObject(obj); 
out.flush(); 
return sbuf.toString();

To decode, we follow the same approach. A Base64InputStream has its read method defined to turn BASE64 characters into bytes, and an ObjectInputStream reads the serialized object data from that stream. Here is the code from the decode method.

Base64InputStream bin 
   = new Base64InputStream(s, start); 
ObjectInputStream in 
   = new ObjectInputStream(bin); 
Object obj = in.readObject(); 
return (Serializable)obj;

When this technique is put to work for transfering images, there is a minor technical setback: the Image class does not implement Serializable. To overcome this problem, we designed a serializable class Bitmap that holds the pixels of an image in the default color map. A bitmap can be constructed from an image, and the getImage method can construct an equivalent image from a bitmap object. This construction happens with the PixelGrabber and MemoryImageSource classes that you saw earlier in this chapter.

class Bitmap implements Serializable 
{  public Bitmap(Image img) 
   {  try 
      {  PixelGrabber pg 
            = new PixelGrabber(img, 0, 0, -1, -1, true); 
         if (pg.grabPixels()) 
         {  width = pg.getWidth(); 
            height = pg.getHeight(); 
            pixels = (int[])pg.getPixels(); 
         } 
      } 
      catch(InterruptedException e) {} 
   } 

   public Image getImage() 
   {  return Toolkit.getDefaultToolkit().createImage(new 
         MemoryImageSource(width, height, pixels, 0, width)); 
   } 

   private int width; 
   private int height; 
   private int[] pixels; 
}

Remember from Chapter 1 that we need not actually write any methods for serialization and deserialization. Java will automatically serialize the width and height fields and the array of pixels.

Now we have all the pieces together to write a program that can copy an image to the system clipboard. The program in Example 6-10 does just that. Run the program, load an image, and copy it into the clipboard. Then, close the program and run it again. Select Edit|Paste and watch how the image transferred into the new instance of the program. Or run several copies of the program, as in Figure 6-12, and copy and paste between them.

Data is copied between two instances of the MimeClipboardTest program

Figure 6-12. Data is copied between two instances of the MimeClipboardTest program

If you use the clipboard viewer or if you simply select Paste in your word processor, you can see the MIME encoding of the clipboard data (see Figure 6-13). However, what you cannot do is have the bitmap in the clipboard pasted as an image into your word processor. The system clipboard does not know that the data is actually an image, and it does not understand the encoding. For the same reason, you cannot select an image in your Web browser, copy it into the clipboard, and paste it into our example program. As mentioned previously, it is currently not possible to transfer anything other than text between a Java program and a non-Java application.

The clipboard contents after an image is copied

Figure 6-13. The clipboard contents after an image is copied

Example 6-10. MimeClipboardTest.java

import java.io.*; 
import java.awt.*; 
import java.awt.image.*; 
import java.awt.event.*; 
import java.awt.datatransfer.*; 
import corejava.*; 

public class MimeClipboardTest extends CloseableFrame 
   implements ActionListener 
{  public MimeClipboardTest() 
   {  MenuBar mbar = new MenuBar(); 
      Menu m = new Menu("File"); 
      MenuItem m1 = new MenuItem("Open"); 
      m1.addActionListener(this); 
      m.add(m1); 
      MenuItem m2 = new MenuItem("Exit"); 
      m2.addActionListener(this); 
      m.add(m2); 
      mbar.add(m); 
      m = new Menu("Edit"); 
      MenuItem m3 = new MenuItem("Copy"); 
      m3.addActionListener(this); 
      m.add(m3); 
      MenuItem m4 = new MenuItem("Paste"); 
      m4.addActionListener(this); 
      m.add(m4); 
      mbar.add(m); 
      setMenuBar(mbar); 
   } 

   public void actionPerformed(ActionEvent evt) 
   {  String arg = evt.getActionCommand(); 
      if (arg.equals("Open")) 
      {  FileDialog d = new FileDialog(this, 
            "Open file", FileDialog.LOAD); 
         d.setDirectory(lastDir); 
         d.show(); 
         String f = d.getFile(); 
         lastDir = d.getDirectory(); 
         if (f != null) 
         {  theImage = Toolkit.getDefaultToolkit().getImage 
               (lastDir + f); 
            theBitmap = new Bitmap(theImage); 
            repaint(); 
         } 
      } 
      else if(arg.equals("Exit")) System.exit(0); 
      else if (arg.equals("Copy")) copyIt(); 
      else if (arg.equals("Paste")) pasteIt(); 
   } 
   public void paint(Graphics g) 
   {  g.translate(getInsets().left, getInsets().top); 
      if(theImage != null) 
         g.drawImage(theImage, 0, 0, this); 
   } 

   private void copyIt() 
   {  SerializableSelection selection 
         = new SerializableSelection(theBitmap); 
      mimeClipboard.setContents(selection, null); 
   } 

   private void pasteIt() 
   {  Transferable selection 
         = mimeClipboard.getContents(this); 
      try 
      {  theBitmap = (Bitmap)selection.getTransferData 
            (SerializableSelection.serializableFlavor); 
         theImage = theBitmap.getImage(); 
         repaint(); 
      } 
      catch(Exception e) {} 
   } 

   public static void main(String [] args) 
   {  Frame f = new MimeClipboardTest(); 
      f.show(); 
   } 

   private static Clipboard mimeClipboard 
      = new MimeClipboard 
         (Toolkit.getDefaultToolkit().getSystemClipboard()); 
   private Bitmap theBitmap; 
   private Image theImage; 
   private String lastDir = ""; 
} 

class Bitmap implements Serializable 
{  public Bitmap(Image img) 
   {  try 
      {  PixelGrabber pg 
            = new PixelGrabber(img, 0, 0, -1, -1, true); 
         if (pg.grabPixels()) 
         {  width = pg.getWidth(); 
            height = pg.getHeight(); 
            pixels = (int [])pg.getPixels(); 
         } 
      } 
      catch(InterruptedException e) {} 
   } 
   public Image getImage() 
   {  return Toolkit.getDefaultToolkit().createImage(new 
         MemoryImageSource(width, height, pixels, 0, width)); 
   } 

   private int width; 
   private int height; 
   private int[] pixels; 
} 

class SerializableSelection implements Transferable 
{  public SerializableSelection(Serializable object) 
   {  theObject = object; 
   } 

   public boolean isDataFlavorSupported(DataFlavor flavor) 
   {  return flavor.equals(serializableFlavor); 
   } 

   public synchronized Object getTransferData 
      (DataFlavor flavor) 
      throws UnsupportedFlavorException 
   {  if(flavor.equals(serializableFlavor)) 
      {  return theObject; 
      } 
      else 
      {  throw new UnsupportedFlavorException(flavor); 
      } 
   } 

   public DataFlavor[] getTransferDataFlavors() 
   {  return flavors; 
   } 

   public static final DataFlavor serializableFlavor 
      = new DataFlavor(java.io.Serializable.class, 
      "Serializable Object"); 

   private static DataFlavor[] flavors 
      = { serializableFlavor }; 

   private Serializable theObject; 
} 

class MimeClipboard extends Clipboard 
{  public MimeClipboard(Clipboard cb) 
   {  super("MIME/" + cb.getName()); 
      clip = cb; 
   } 
   public synchronized void setContents(Transferable contents, 
      ClipboardOwner owner) 
   {  if (contents instanceof SerializableSelection) 
      {  try 
         {  DataFlavor flavor 
               = SerializableSelection.serializableFlavor; 
            Serializable obj = (Serializable) 
               contents.getTransferData(flavor); 
            String enc = encode(obj); 
            String header = "Content-type: " 
               + flavor.getMimeType() 
               + "
Content-length: " 
               + enc.length() + "

"; 
            StringSelection selection 
               = new StringSelection(header + enc); 
            clip.setContents(selection, owner); 
         } 
         catch(UnsupportedFlavorException e) 
         {} 
         catch(IOException e) 
         {} 
      } 
      else clip.setContents(contents, owner); 
   } 

   public synchronized Transferable getContents 
      (Object requestor) 
   {  Transferable contents = clip.getContents(requestor); 

      if (contents instanceof StringSelection) 
      {  String data = null; 
         try 
         {  data = (String)contents.getTransferData 
               (DataFlavor.stringFlavor); 
         } 
         catch(UnsupportedFlavorException e) 
         { return contents; } 
         catch(IOException e) 
         { return contents; } 

         if (!data.startsWith("Content-type: ")) 
            return contents; 
         int start = -1; 
         // skip three newlines 
         for (int i = 0; i < 3; i++) 
         {  start = data.indexOf('
', start + 1); 
            if (start < 0) return contents; 
         } 
         Serializable obj = decode(data, start); 
         SerializableSelection selection 
            = new SerializableSelection(obj); 
         return selection; 
      } 
      else return contents; 
   } 
   private static String encode(Serializable obj) 
   {  try 
      {  StringBuffer sbuf = new StringBuffer(); 
         Base64OutputStream bout 
            = new Base64OutputStream(sbuf); 
         ObjectOutputStream out 
            = new ObjectOutputStream(bout); 
         out.writeObject(obj); 
         out.flush(); 
         return sbuf.toString(); 
      } 
      catch(Exception e) 
      {  return ""; 
      } 
   } 

   private static Serializable decode(String s, int start) 
   {  try 
      {  Base64InputStream bin 
            = new Base64InputStream(s, start); 
         ObjectInputStream in 
            = new ObjectInputStream(bin); 
         Object obj = in.readObject(); 
         return (Serializable)obj; 
      } 
      catch(Exception e) 
      {  return null; 
      } 
   } 

   private Clipboard clip; 
} 

/* BASE64 encoding encodes 3 bytes into 4 characters. 
   |11111122|22223333|33444444| 
   Each set of 6 bits is encoded according to the 
   toBase64 map. If the number of input bytes is not 
   a multiple of 3, then the last group of 4 characters 
   is padded with one or two = signs. Each output line 
   is at most 76 characters. 
*/ 

class Base64OutputStream extends OutputStream 
{  public Base64OutputStream(StringBuffer sb) 
   {  sbuf = sb; 
   } 

   public void write(int c) throws IOException 
   {  inbuf[i] = c; 
      i++; 
      if (i == 3) 
      {  sbuf.append(toBase64[(inbuf[0] & 0xFC) >> 2]); 
         sbuf.append(toBase64[((inbuf[0] & 0x03) << 4) | 
            ((inbuf[1] & 0xF0) >> 4)]); 
         sbuf.append(toBase64[((inbuf[1] & 0x0F) << 2) | 
            ((inbuf[2] & 0xC0) >> 6)]); 
         sbuf.append(toBase64[inbuf[2] & 0x3F]); 
         col += 4; 
         i = 0; 
         if (col >= 76) 
         {  sbuf.append('
'), 
            col = 0; 
         } 
      } 
   } 

   public void flush() 
   {  if (i == 1) 
      {  sbuf.append(toBase64[(inbuf[0] & 0xFC) >> 2]); 
         sbuf.append(toBase64[(inbuf[0] & 0x03) << 4]); 
         sbuf.append('='), 
         sbuf.append('='), 
      } 
      else if (i == 2) 
      {  sbuf.append(toBase64[(inbuf[0] & 0xFC) >> 2]); 
         sbuf.append(toBase64[((inbuf[0] & 0x03) << 4) | 
            ((inbuf[1] & 0xF0) >> 4)]); 
         sbuf.append(toBase64[(inbuf[1] & 0x0F) << 2]); 
         sbuf.append('='), 
      } 
      sbuf.append('
'), 
   } 

   private static char[] toBase64 = 
   {  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 
      'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 
      'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 
      'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 
      'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 
      'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 
      'w', 'x', 'y', 'z', '0', '1', '2', '3', 
      '4', '5', '6', '7', '8', '9', '+', '/' 
   }; 

   StringBuffer sbuf; 
   int col = 0; 
   int i = 0; 
   int[] inbuf = new int[3]; 
} 

class Base64InputStream extends InputStream 
{  public Base64InputStream(String s, int start) 
   {  str = s; 
      pos = start; 
      i = 0; 
   } 
   public int read() 
   {  while (pos < str.length() && 
         Character.isWhitespace(str.charAt(pos))) 
      {  pos++; 
      } 
      if (pos >= str.length()) return -1; 
      if (i == 0) 
      {  int ch1 = str.charAt(pos) & 0x7F; 
         int ch2 = str.charAt(pos + 1) & 0x7F; 
         i++; 
         return (fromBase64[ch1] << 2) 
            | (fromBase64[ch2] >> 4); 
      } 
      else if (i == 1) 
      {  int ch1 = str.charAt(pos + 1) & 0x7F; 
         int ch2 = str.charAt(pos + 2) & 0x7F; 
         if (ch2 == '=') return -1; 
         i++; 
         return ((fromBase64[ch1] & 0x0F) << 4) 
            | (fromBase64[ch2] >> 2); 
      } 
      else 
      {  int ch1 = str.charAt(pos + 2) & 0x7F; 
         int ch2 = str.charAt(pos + 3) & 0x7F; 
         if (ch2 == '=') return -1; 
         i = 0; 
         pos += 4; 
         return ((fromBase64[ch1] & 0x03) << 6) 
            | fromBase64[ch2]; 
      } 
   } 

   private static int[] fromBase64 = 
   {  -1, -1, -1, -1, -1, -1, -1, -1, 
      -1, -1, -1, -1, -1, -1, -1, -1, 
      -1, -1, -1, -1, -1, -1, -1, -1, 
      -1, -1, -1, -1, -1, -1, -1, -1, 
      -1, -1, -1, -1, -1, -1, -1, -1, 
      -1, -1, -1, 62, -1, -1, -1, 63, 
      52, 53, 54, 55, 56, 57, 58, 59, 
      60, 61, -1, -1, -1, -1, -1, -1, 
      -1,  0,  1,  2,  3,  4,  5,  6, 
       7,  8,  9, 10, 11, 12, 13, 14, 
      15, 16, 17, 18, 19, 20, 21, 22, 
      23, 24, 25, -1, -1, -1, -1, -1, 
      -1, 26, 27, 28, 29, 30, 31, 32, 
      33, 34, 35, 36, 37, 38, 39, 40, 
      41, 42, 43, 44, 45, 46, 47, 48, 
      49, 50, 51, -1, -1, -1, -1, -1 
   }; 

   String str; 
   int pos; 
   int i; 
}


[1] We omit the mathematics behind this equation and the test for “escaping to infinity.” For more on the mathematics of fractals, there are hundreds of books out there; one that is quite thick and comprehensive is: Chaos and Fractals: New Frontiers of Scienceby Heinz-Otto Peitgen, Hartmut Jurgens, and Dietmar Saupe (Springer Verlag, 1992).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.74.55