|
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.
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.
|