.
.
Core Java
Technologies Technical Tips
.
   View this issue as simple text February 17, 2004    

In this Issue

Welcome to the Core Java Technologies Tech Tips for February 17, 2004. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).

This issue covers:

-Loading and Saving Images with the Image I/O Library
-Reflecting JavaBeans Components

These tips were developed using Java 2 SDK, Standard Edition, v 1.4.2.

This issue of the Core Java Technologies Tech Tips is written by John Zukowski, president of JZ Ventures, Inc.

See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.

.
.

LOADING AND SAVING IMAGES WITH THE IMAGE I/O LIBRARY

Introduced in J2SE 1.4, the javax.imageio package is the primary package for the Java Image I/O API. As its name implies, this package helps you read and write image files. You might wonder what's so important about this package. The fact is that you could read images with the getImage method of various classes like Toolkit and Applet since the initial release of the Java platform. But there is more to the javax.imageio package than simply reading images. One important point is that there was no writeImage or putImage previous to the Image I/O library. Now there is a way to write images. Also, you can now set properties such as compression level when you save images.

The first question many people ask when working with the Image I/O libraries is what formats are supported? With Sun's reference implementation, you get a specific set. However, the API is flexible enough so that you can install your own formats by extending the necessary classes in the javax.imageio.spi library. For the moment, let's put that aspect of the library aside. To discover the installed set of readers and writers, you simply ask the ImageIO class through its getReaderFormatNames() and getWriterFormatNames() methods (or getReaderMIMETypes() and getWriterMIMETypes() if you want to work directly with MIME types). Here's an example:

   import javax.imageio.*;

     public class GetList {
       public static void main(String args[]) {
         String readerNames[] = 
           ImageIO.getReaderFormatNames();
         printlist(readerNames, "Reader names:");
         String readerMimes[] = 
           ImageIO.getReaderMIMETypes();
         printlist(readerMimes, "Reader MIME types:");
         String writerNames[] = 
           ImageIO.getWriterFormatNames();
         printlist(writerNames, "Writer names:");
         String writerMimes[] = 
           ImageIO.getWriterMIMETypes();
         printlist(writerMimes, "Writer MIME types:");
       }
       private static void printlist(String names[], 
                                        String title) {
         System.out.println(title);
         for (int i=0, n=names.length; i<n; i++) {
           System.out.println("\t" + names[i]);
         }
       }
     }

If you run the GetList program with Sun's reference implementation (and no custom providers installed), you should see the following output:

   Reader names:
           jpeg
           gif
           JPG
           png
           jpg
           JPEG
   Reader MIME types:
           image/png
           image/jpeg
           image/x-png
           image/gif
   Writer names:
           jpeg
           JPG
           png
           jpg
           PNG
           JPEG
   Writer MIME types:
           image/png
           image/jpeg

Basically, you get support for reading GIF, JPEG, and PNG formatted images, and for writing JPEG and PNGs. There is no GIF writer provided.

As you work with the Image I/O libraries, you might notice that almost all the work is requested through the static methods of the ImageIO class. For basic usage, these static methods are all you need. For example, to read an image, you pass the location to one of the following read methods of ImageIO:

  • read(File input)
  • read(ImageInputStream stream)
  • read(InputStream input)
  • read(URL input)

The ImageInputStream is an interface of the javax.imageio.stream package that is seekable, and so it's potentially faster than a regular InputStream. Instead of getting an InputStream from a FileInputStream, and passing that InputStream into the read method, it's more efficient to pass the File object to be read, or to create a FileImageInputStream from the File. More specifically, instead of:

  InputStream is = new InputStream("myimage.jpg");
  ImageIO.read(is);

use either:

  File file = new File("myimage.jpg");
  ImageIO.read(file);

or

  File file = new File("myimage.jpg");
  FileImageInputStream fiis = 
    new FileImageInputStream(file);
  ImageIO.read(fiis);

The read method (in all cases) returns a BufferedImage object. You can then draw this image with the drawImage method of Graphics, or you can filter it with something like a ConvolveOp from the java.awt.image package.

The following program, ReadSharp, demonstrates reading and displaying an image. The program also applies a sharpness filter.

   import java.awt.image.*;
   import javax.imageio.*;
   import java.io.*;
   import java.awt.*;
   import javax.swing.*;
  
   public class ReadSharp {
     private static class FrameShower 
      implements Runnable {
       final Frame frame;
       public FrameShower(Frame frame) {
         this.frame = frame;
       }
       public void run() {
         frame.show();
       }
     }
     public static void main(String args[])
      throws IOException {
       if (args.length != 1) {
         System.err.println(
           "Please include image filename on command line");
         System.exit(-1);
       }
       // Read
       File file = new File(args[0]);
       BufferedImage input = ImageIO.read(file);
       // Convert
       Kernel sharpKernel =
               new Kernel(3, 3, new float[] {
          0.0f, -1.0f,  0.0f,
          -1.0f,  5.0f, -1.0f,
         0.0f, -1.0f,  0.0f
       });   
       ConvolveOp convolveOp =
         new ConvolveOp(
           sharpKernel, ConvolveOp.EDGE_NO_OP, null);
       int width = input.getWidth();
       int height = input.getHeight();
       BufferedImage output = new BufferedImage(
          width, height, BufferedImage.TYPE_INT_ARGB);
        convolveOp.filter(input, output);
       // Make screen
       Icon icon = new ImageIcon(output);
       JLabel label = new JLabel(icon);
       JFrame frame = new JFrame("Sharp Image");
       frame.setDefaultCloseOperation(
         JFrame.EXIT_ON_CLOSE);
       frame.getContentPane().add(
         label, BorderLayout.CENTER);
       frame.pack();
       // Show
       Runnable runner = new FrameShower(frame);
       EventQueue.invokeLater(runner);
     }
   }

When you run the ReadSharp program, specify an image name on the command line. For example, let's use one of the images from the February 10, 2004 Tech Tip titled Styling Digital Images with ConvolveOp.

   java ReadSharp BrightnessChanger.jpg

As ReadSharp demonstrates, you don't have to specify the image format when reading an image. Typically, the system will determine the correct reader based on the first few bytes of the stream (typically referred to as the magic number).

Writing images can be just as easy as reading them, though you can also specify metadata if you need more control. For basic support, there are the three forms of the write method:

  • write(RenderedImage im, String formatName, File output)
  • write(RenderedImage im, String formatName, ImageOutputStream output)
  • write(RenderedImage im, String formatName, OutputStream output)

All three method forms of the write method return a boolean. This indicates whether an appropriate writer object is available in the system. For example, asking for a GIF writer will return false because there is no GIF writer in the system.

BufferedImage is a type of RenderedImage, so you only need to pass to the write method the BufferedImage returned by the read method. This makes it easy to do simple transformations, such as from GIF to PNG:

   File inputFile = new File("image.gif");
   BufferedImage input = ImageIO.read(inputFile);
   File outputFile = new File("image.png");
   ImageIO.write(input, "PNG", outputFile);

Notice that the format name passed to the write method is one of those returned by getWriterFormatNames().

When you write an image, you can configure various writing parameter metadata, such as compression level. However, you can't do that directly with the write method of ImageIO. Instead, you must use some of the other Image I/O classes (and packages). More on how to specify this metadata through the ImageWriteParam class shortly.

It is possible to have more than one Reader or Writer provider for a specific format. Because of that, methods such as getImageWritersByFormatName of ImageIO return an Iterator. To customize output compression levels, you could examine all the installed providers and find the maximum compression level supported. Or you could simply use the first one and work with it. Let's take the simpler approach here:

   Iterator iter = 
     ImageIO.getImageWritersByFormatName("JPG");
   if (iter.hasNext()) {
     ImageWriter writer = (ImageWriter)iter.next();
     ...
   }

You can get the default writing parameters for a specific ImageWriter through its getDefaultWriteParam method. The method returns an ImageWriteParam object. For JPEGs, this is an instance of javax.imageio.plugins.jpeg.JPEGImageWriteParam (though you don't need to know this). To change the compression level, you need to tell the ImageWriteParam object that you want to set the compression mode explicitly:

   iwp.setCompressionMode(
     ImageWriteParam.MODE_EXPLICIT);

You then set the compression quality with:

   iwp.setCompressionQuality(floatLevel);

Instead of picking a value at random, you can ask the writer what compression quality values it supports (or how much it will compress things):

   float values[] = iwp.getCompressionQualityValues();

Here's a program, Compress, that converts any image that can be read through the Image I/O library. The program reads the image, and writes an image for each supported compression format. The Sun-provided JPEG encoder returns the values of 5%, 75%, and 95% from getCompressionQualityValues. Notice that the program writes directly with the ImageWriter object, not with the ImageIO object.

Because the output parameters are being customized, you must use an IIOImage object (IIO stands for Image IO) to represent the image to be written. You can't use a plain BufferedImage. The BufferedImage can be directly converted to an IIOImage as an extra step.

   import java.awt.image.*;
   import javax.imageio.*;
   import javax.imageio.stream.*;
   import java.io.*;
   import java.util.Iterator;
  
   public class Compress {
     public static void main(String args[]) throws IOException {
       if (args.length != 1) {
         System.err.println(
           "Please include image filename on command line");
         System.exit(-1);
       }
       // Read
       String name = args[0];
       File file = new File(name);
       BufferedImage input = ImageIO.read(file);
       // Get Writer and set compression
       Iterator iter = 
         ImageIO.getImageWritersByFormatName("JPG");
       if (iter.hasNext()) {
         ImageWriter writer = (ImageWriter)iter.next();
         ImageWriteParam iwp = 
           writer.getDefaultWriteParam();
         iwp.setCompressionMode(
           ImageWriteParam.MODE_EXPLICIT);
         float values[] = 
           iwp.getCompressionQualityValues();
         // Write one for each compression values
         for (int i=0, n=values.length; i<n; i++) {
           iwp.setCompressionQuality(values[i]);
           String newName = i + name;
           if (!newName.endsWith(".jpg")) {
             newName += ".jpg";
           }          
           File outFile = new File(newName);
           FileImageOutputStream output = 
             new FileImageOutputStream(outFile);
           writer.setOutput(output);
           IIOImage image = 
             new IIOImage(input, null, null);
           System.out.println(
             "Writing " + values[i] + "%");
           writer.write(null, image, iwp);
         }
       }
     }
   }

There is much more to the Image I/O API than shown here. The material in this tip should get you should get you started with reading and writing images, as well as customizing the output. Also of interest is the javax.imageio.event package, which allows you to listen for the progress of the image reading and writing operations.

For more information about the Image I/O library, see the Java Image I/O API Guide.

.
.

REFLECTING JAVABEANS COMPONENTS

The JavaBeans specification defines a way of creating reusable components that are easily pluggable into nearly any tool or environment. These components simplify the creation of applications such as JavaServer Pages (JSP)-based applications, or graphical, Swing-based programs.

A JavaBeans component, also known as a JavaBean, is designed to be visually manipulated in graphically-oriented tools such as an IDE. A JavaBean is a Java class that defines properties, has methods, and identifies events that it fires and to which it responds. The names of the methods in the class specify the properties. If you create the class appropriately, an IDE such as Java Studio will recognize the properties and events, and allow you to graphically program using the JavaBean. Or, if you program with JSP technology, if the properties of a JavaBean match the field names in an HTML form, the bean is automatically initialized.

In the simplest case, the naming convention for a JavaBean component property is a pair of methods, one to set the property, and one to get its value:

   public void setPropertyName(DataType newValue)
   public DataType getPropertyName()

DataType is replaced with the data type for the property, and PropertyName in the method name is replaced with the actual name of the property.

Getter and setter methods like those shown above are a static way of accessing and setting properties. But there's a dynamic way. Instead of calling the get and set methods directly, let's use the Reflection and Introspection APIs to dynamically access the properties of an object. This dynamic access doesn't require compilation-time knowledge of a class or its getter and setter methods.

Here's an example of dynamically accessing properties using the Reflection and Introspection APIs. The following program, GetProp, accesses the text property of a JButton component using a Utils class (yet to be created):

   import javax.swing.JButton;

   public class GetProp {
     public static void main(String args[]) 
      throws Exception {
       JButton button = new JButton("Help");
       String text = 
         (String)Utils.getProperty(button, "text");
       System.out.println("Button label: " + text);
     }
   }

Getting the current value of a property for a JavaBean component involves three steps. First, you must get the PropertyDescriptor for that property. Descriptors exist for the JavaBean class, not a JavaBean instance. Also, there isn't a simple way to get the descriptor for a specific property. Instead, you must get the descriptor for each property, and then find the specific descriptor that you need. So, getting the PropertyDescriptor involves its own set of tasks, as follows:

  • Get the class for the JavaBean:
    Class beanClass = bean.getClass();

  • Get the BeanInfo for the Class:
    BeanInfo beanInfo =
    Introspector.getBeanInfo(beanClass);


  • Get all the PropertyDescriptor objects for the properties of the class:
    PropertyDescriptor descriptor[] =
    beanInfo.getPropertyDescriptors();


  • Identify the specific PropertyDescriptor of interest:
    for (int i=0, n=descriptor.length; i<n; i++) {
    if (descriptor[i].getName().equals(name)) {
    return descriptor[i];
    }
    }
    return null;

The BeanInfo for a class exposes the properties, methods, and event set descriptors of that class.

After you have the PropertyDescriptor for the property of interest, you can look up the current setting for the property. To do that, you need to get the reader method associated with the property. Typically, this would be a getPropertyName method (although the JavaBeans specification permits beans to have reader methods with different names as identified by the BeanInfo for a class).

   Method read = descriptor.getReadMethod();

After you have the Method object from the java.lang.reflect package, you can invoke it through the normal Reflection API mechanism. By definition, reader methods must have no arguments, so you can pass an empty Object array as the last argument to invoke. Invoking requires you to specify which instance of the JavaBean is being examined.

   return (read.invoke(bean, new Object[0]));

You can also extend the getProperty method to get all the properties at once.

   Map map = Utils.getProperties(bean);

Then, in the returned Map, you can more thoroughly examine or display all the property settings. Here's what such a method would look like:

   public static Map getProperties(Object bean) 
       throws IntrospectionException, IllegalAccessException, 
         InvocationTargetException {
     BeanInfo beanInfo = 
       Introspector.getBeanInfo(bean.getClass());
     PropertyDescriptor descriptor[] =
       beanInfo.getPropertyDescriptors();
     Map map = new HashMap(descriptor.length);
     Object value;
     for (int i=0, n=descriptor.length; i<n; i++) {
       Method read = descriptor[i].getReadMethod();
         value = read.invoke(bean, new Object[0]);
         map.put(descriptor[i].getName(), value);
       }
     }
     return map;
   }

For each PropertyDescriptor in the BeanInfo, the Map method gets its read method, invokes it, and stores the property name and value in the map.

There are two major caveats about this getProperties method. The Map returned represents the state of the JavaBean at a moment in time. As soon as the method returns, the bean could have its property values changed. These would not be reflected in the Map returned by getProperties. Second, using the Reflection and Introspection APIs to get all the property values is a costly operation. Direct method invocation is much more efficient. If all you need is one property setting, use the simpler getProperty version, instead of getting a Map and looking up the single value.

Here is the complete Utils class, with both the singular and plural versions of getting property values.

   import java.beans.*;
   import java.lang.reflect.*;
   import java.util.*;

   public class Utils {
     private Utils() {
     }

     public static Object getProperty(Object bean, 
      String name)
      throws NoSuchMethodException, 
      IllegalAccessException, IntrospectionException, 
      InvocationTargetException {
       Object returnValue = null;
       PropertyDescriptor descriptor =
         getPropertyDescriptor(bean, name);
       if (descriptor != null) {
         Method read = descriptor.getReadMethod();
         if (read != null) {
           returnValue = 
             read.invoke(bean, new Object[0]);
         } else {
             throw new NoSuchMethodException(
             "No reader for " + name + " property");
         }
       } else {
         throw new NoSuchMethodException(
           "No " + name + " property");
       }
       return returnValue;
     }

     public static Map getProperties(Object bean)
      throws IntrospectionException, 
      IllegalAccessException,
      InvocationTargetException {
       BeanInfo beanInfo = 
         Introspector.getBeanInfo(bean.getClass());
       PropertyDescriptor descriptor[] =
         beanInfo.getPropertyDescriptors();
       Map map = new HashMap(descriptor.length);
       Object value;
       for (int i=0, n=descriptor.length; i<n; i++) {
         Method read = descriptor[i].getReadMethod();
         if (read != null) {
           value = read.invoke(bean, new Object[0]);
           map.put(descriptor[i].getName(), value);
         }
       }
       return map;
     }

     private static PropertyDescriptor 
      getPropertyDescriptor(
        Object bean, String name) 
        throws IntrospectionException {
       PropertyDescriptor returnValue = null;
       BeanInfo beanInfo = 
         Introspector.getBeanInfo(bean.getClass());
       PropertyDescriptor descriptor[] =
         beanInfo.getPropertyDescriptors();
       for (int i=0, n=descriptor.length; i<n; i++) {
         if (descriptor[i].getName().equals(name)) {
           returnValue = descriptor[i];
           break;
         }
       }
       return returnValue;
     }
   }

To test the completed Utils class, the following extends the earlier GetProp class with a sample usage of the newer getProperties method.

   import javax.swing.JButton;
   import java.util.Iterator;
   import java.util.Map;
   import java.util.Set;

   public class GetProp {
     public static void main(String args[]) 
      throws Exception {
       JButton button = new JButton("Help");
       String text = 
         (String)Utils.getProperty(button, "text");
       System.out.println("Button label: " + text);
       Map map = Utils.getProperties(button);
       Set entrySet = map.entrySet();
       for (Iterator iter = 
         entrySet.iterator(); iter.hasNext(); ) {
          Map.Entry entry = (Map.Entry)iter.next();
          System.out.println(entry.getKey() + ":\t" + 
            entry.getValue());
        }
      }
    }

When you run GetProp, you should see output that begins like this:

   Button label: Help
   verifyInputWhenFocusTarget:     true
   displayedMnemonicIndex: -1
   width:  0
   layout: javax.swing.OverlayLayout@17a29a1
   actionCommand:  Help
   opaque: true

In addition to using the Utils class to get the value of properties from a JavaBean, you can extend the Utils class to set properties. In this case, you use the getWriteMethod of PropertyDescriptor.

There's much more to the JavaBean Introspection API than shown in this tip. For more information about the Reflection API, see these earlier Tech Tips:

For more information on JavaBeans, visit the JavaBeans home page and the JavaBeans trail in the Java Tutorial.

.
.
Reader Feedback
Excellent   Good   Fair   Poor  

If you have other comments or ideas for future technical tips, please type them here:

Comments:
If you would like a reply to your comment, please submit your email address:
Note: We may not respond to all submitted comments.

Have a question about Java programming? Use Java Online Support.

.
.

IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developers.sun.com/dispatcher.jsp?uid=6910008


Comments? Send your feedback on the Core Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=newslet

Subscribe to other Java developer Tech Tips:

- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).

To subscribe to these and other JDC publications:
- Go to the JDC Newsletters and Publications page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".


ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/developer/JDCTechTips/index.html


Copyright 2004 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.


This document is protected by copyright. For more information, see:
http://java.sun.com/developer/copyright.html


Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks (http://www.sun.com/suntrademarks/) of Sun Microsystems, Inc. in the United States and other countries.

Sun Microsystems,
Inc.
.
.