Image I/O Utilities Grab Bag Blog

Version 2


    Image I/O is a powerful API for reading and writing images in an extensible variety of formats. Because this power comes with a degree of complexity that can overwhelm developers new to Image I/O (and possibly frustrate experienced developers), this article presents a grab bag of useful Image I/O utilities that can make your Image I/O experiences more productive. Help yourself to them.

    These utilities are implemented by theca.mb.javajeff.iioutil package'sIIOUtilities class and its static methods, and by theUnsupportedFormatException support class. After introducing these methods, and three example applications that demonstrate their usefulness, I use Apache Ant to build and package these classes into an iioutil.jar JAR file, and build these applications.

    Note: I built and tested this article's code with Sun's J2SE 5.0 SDK and Apache Ant 1.6.5. Windows 98 SE was the underlying platform.

    Create Image Viewer Accessory for File Chooser

    A javax.swing.JFileChooser instance can be customized through an accessory (ajavax.swing.JComponent subclass instance) to view images, to present additional dialog box controls, or to do something else. After creating the accessory, this component is attached to the file chooser by invokingJFileChooser's public void setAccessory(JComponent newAccessory) method.

    Because an image viewer is a common accessory,IIOUtilities provides a public static JComponent createImageViewerAccessory(JFileChooser fc, int width, int height) method. This method creates and returns an image viewer accessory for the specified file chooser, having the specified dimensions. No exceptions are thrown.

    I've created an Example1 application that demonstratescreateImageViewerAccessory(). This application creates and displays a single open file chooser with the image viewer accessory. When the user highlights a file whose filename suffix matches one of Image I/O's supported suffixes, the image is rendered on the image viewer's drawing area. Check out Figure 1.

    The image viewer centers small images on its drawing area
    Figure 1. The image viewer centers small images on its drawing area

    This application's public static void main(String [] args) method first creates a JFileChooser that references the current directory. This method next invokescreateImageViewerAccessory() to create the accessory, followed by setAccessory() to attach the accessory to the file chooser. This open file chooser is then shown (with no parent component):

    JFileChooser fcOpen = new JFileChooser (new File (".")); fcOpen.setAccessory (IIOUtilities. createImageViewerAccessory (fcOpen, 200, 150)); fcOpen.showOpenDialog (null);

    The createImageViewerAccessory() method creates the accessory as an instance of its IV (Image Viewer) local class. This method passes its arguments to IV's constructor, and then returns the resulting component. The following IIOUtilities excerpt presents and explains the workings of createImageViewerAccessory() and itsIV class:

    public static JComponent createImageViewerAccessory (JFileChooser fc, int width, int height) { class IV extends JComponent implements PropertyChangeListener { // Reference to BufferedImage whose contents // are displayed. Nothing is displayed if // reference is null. private BufferedImage image; // The following JFileChooser reference is // needed for registering a // PropertyChangeListener. private JFileChooser fc; // Accessory dimensions. private int width, height; // Construct the component. private IV (JFileChooser fc, int width, int height) { this.fc = fc; this.width = width; this.height = height; // Register a property change listener // with the file chooser so that this // component is made aware of file chooser // events (such as a user selecting a file). fc.addPropertyChangeListener (this); // Set this component's dimensions to // accommodate scaled images. The specified // values figure into the overall size of // the file chooser. setPreferredSize (new Dimension (width, height)); } // Paint this component in response to a // repaint() method call. protected void paintComponent (Graphics g) { // Please see this article's code file for // the contents of this method. } // Respond to property change events sent to // this component by the file chooser. public void propertyChange (PropertyChangeEvent evt) { // Please see this article's code file for // the contents of this method. } } // Return an IV accessory that works with the // specified filechooser. Furthermore, the // accessory's drawing area is bounded by the // specified dimensions. return new IV (fc, width, height); }

    Capture Screen Contents to File

    The java.awt.Robot class provides a public BufferedImage createScreenCapture(Rectangle screenRect)method for capturing screen contents (without the mouse cursor) to a java.awt.image.BufferedImage. Because it is convenient to capture screen contents and save these contents to a file in one step, IIOUtilities provides the following two methods:

    • public static void captureScreenToFile(String filename) captures the entire screen's contents to the file identified by filename. The filename's suffix determines the format in which the contents are saved--jpg identifies the JPEG format, for example.

      This method throws java.awt.AWTException (screen cannot be captured), (capture cannot be saved to a file), orUnsupportedFormatException (filename's suffix is not recognized by Image I/O).

    • public static void captureScreenToFile(Rectangle bounds, String filename) is similar to the previous method, except that a subset of the screen (as determined by thebounds parameter's screen coordinates) is saved.

      In addition to the previous method's exceptions, thiscaptureScreenToFile() method throws anIllegalArgumentException if any boundary value is negative, or if the capture area exceeds the screen's dimensions.

    I've created an Example2 application that demonstrates bothcaptureScreenToFile() methods. This application's GUI, which is shown in Figure 2, presents two buttons for capturing the GUI's frame window and the entire screen. The frame window is captured to window.jpg, and the entire screen is captured to screen.jpg.

    The button stays pressed during the capture, which occurs in the button's action listener
    Figure 2. The button stays pressed during the capture, which occurs in the button's action listener

    The "Capture frame window" button's action listener first determines the frame window's boundaries (for its width and height) and upper-left-corner screen location. After merging these values into a single java.awt.Rectangle, this rectangle andwindow.jpg are passed to the secondcaptureScreenToFile() method, which captures the window to this file:

    JButton btnWindow = new JButton ("Capture frame window"); ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent evt) { Rectangle bounds = frame.getBounds (); bounds.setLocation (frame. getLocationOnScreen ()); try { IIOUtilities. captureScreenToFile (bounds, "window.jpg"); JOptionPane. showMessageDialog (frame, "Window captured to " + "window.jpg."); } catch (Exception exc) { JOptionPane. showMessageDialog (frame, "Unable to capture " + "window."); exc.printStackTrace (); } } }; btnWindow.addActionListener (al);

    The "Capture entire screen" button's action listener is similar. Rather than examine this listener, let's take a look at thecaptureScreenToFile(Rectangle bounds, String filename)method, to see how it works.:

    public static void captureScreenToFile (Rectangle bounds, String filename) throws AWTException, IOException, UnsupportedFormatException { // Obtain screen size in case bounds is null. // Size is also used to validate bounds if not // null. Dimension size = Toolkit.getDefaultToolkit ().getScreenSize (); if (bounds == null) bounds = new Rectangle (size); else { String message = null; if (bounds.x < 0) message = "x < 0"; else if (bounds.width < 0) message = "width < 0"; else if (bounds.x + bounds.width > size.width) message = "x + width > screen width"; else if (bounds.y < 0) message = "y < 0"; else if (bounds.height < 0) message = "height < 0"; else if (bounds.y + bounds.height > size.height) message = "y + height > screen height"; if (message != null) throw new IllegalArgumentException (message); } // Attempt to capture the screen and save this // capture to a file. saveImageToFile (new Robot (). createScreenCapture (bounds), filename); }

    By the way, the other captureScreenToFile()method's single line of code invokes this method, passingnull to bounds.

    Save and Load Images

    Saving and loading images in a convenient manner is useful--the previous captureScreenToFile() source code'ssaveImageToFile (new Robot ().createScreenCapture (bounds), filename); method call illustrates this usefulness. To complete IIOUtilities, I've added the following image-save and image-load methods to this class:

    • public static void saveImageToFile(BufferedImage image, String filename) saves the contents of theBufferedImage to the file identified byfilename. An IOException is thrown if the image cannot be saved; an UnsupportedFormatExceptionis thrown if Image I/O does not support filename's suffix.

    • public static BufferedImage loadImageFromFile(String filename) loads an image from the file identified byfilename into a new BufferedImage. AnIOException is thrown if the image cannot be loaded; an UnsupportedFormatException is thrown if Image I/O does not support filename's suffix.

    I've created an Example3 application that uses these methods for image conversion. Example3 takes two command-line arguments: the first argument identifies the source image's path and name, and the second argument identifies the destination image's path and name. The conversion is performed by IIOUtilities.saveImageToFile (IIOUtilities.loadImageFromFile (args [0]), args [1]);.

    The saveImageToFile() method treats JPEG destinations in a special way: this method establishes a 95-percent compression quality so that the image is saved with low compression (and will look good when reloaded). If you prefer to dynamically specify compression quality, replace the hardcoded0.95f value in this method's source code (shown below) with a private static field and suitable accessor methods:

    public static void saveImageToFile (BufferedImage image, String filename) throws IOException, UnsupportedFormatException { // Validate presence of file suffix, which is used // to obtain appropriate image writer. Throw an // exception if there is no suffix -- what format // would be used in this case? int index = filename.lastIndexOf ('.'); if (index == -1) throw new UnsupportedFormatException ("No file suffix"); // Extract file suffix and use it to obtain an // Iterator of appropriate image writers. String suffix = filename.substring (index+1); Iterator iter = ImageIO.getImageWritersBySuffix (suffix); // Throw exception if there are no image writers -- // image format is not supported. if (!iter.hasNext ()) throw new UnsupportedFormatException ("No writer for suffix " + suffix); // Extract writer and set the writer's output // destination to the passed filename. ImageWriter writer = (ImageWriter) (); File file = new File (filename); writer.setOutput (ImageIO. createImageOutputStream (file)); // Write the image. If saving to a JPEG file, // assign a 95% compression quality -- the default // is 75%. if (suffix.equalsIgnoreCase ("jpeg") || suffix.equalsIgnoreCase ("jpg")) { ImageWriteParam iwp = writer.getDefaultWriteParam (); iwp.setCompressionMode (ImageWriteParam.MODE_EXPLICIT); iwp.setCompressionQuality (0.95f); writer.write (null, new IIOImage (image, null, null), iwp); } else writer.write (image); // Release any resources being held by the writer. writer.dispose (); }

    Build the Image I/O Utilities JAR file and Example Applications

    Now that we have explored the methods belonging to theIIOUtilities class, let's build this class and itsUnsupportedFormatException support class, and package them into iioutil.jar--and also build the example applications. We will use Apache Ant with build.xml to automate these tasks. This script assumes the following directory structure:

    src examples jar ca mb javajeff iioutil build.xml setup.bat

    Before using Apache Ant, this tool's bin directory must be added to the PATH environment variable. Also, theJAVA_HOME environment variable must refer to Java SE 5's home directory. I accomplish both tasks on my Windows platform with the following setup.bat file--you might want to use something similar on your platform.

    set JAVA_HOME=c:\progra~1\java\jdk1.5.0 set PATH=c:\ant\bin;c:\progra~1\java\jdk1.5.0\bin; c:\windows;c:\windows\command

    After making sure that the directory containing src, the batch file, and the XML script file is current, typeant (by itself) to build iioutil.jar and the example applications. Ant creates a bin directory that stores the JAR file and an examples subdirectory with application classfiles. This tool uses the followingbuild.xml file for these builds.

    <project name="build" default="buildall" basedir="."> <target name="buildall" depends="buildjar,buildex" description="build everything"> </target> <target name="buildex" description="build the examples"> <mkdir dir="${basedir}/bin/examples"/> <javac classpath="${basedir}/bin/iioutil.jar" srcdir="${basedir}/src/examples" destdir="${basedir}/bin/examples"/> </target> <target name="buildjar" description="build the jar"> <mkdir dir="${basedir}/bin/jar"/> <javac srcdir= "${basedir}/src/jar/ca/mb/javajeff/iioutil" destdir="${basedir}/bin/jar"/> <jar basedir="${basedir}/bin/jar" destfile="${basedir}/bin/iioutil.jar"/> </target> <target name="gendoc" description= "generate the API documentation"> <javadoc destdir="docs/api" author="true" sourcepath="${basedir}/src/jar" packagenames= "ca.mb.javajeff.iioutil.*"/> </target> <target name="cleanup"> <delete dir="${basedir}/bin"/> <delete dir="${basedir}/docs"/> </target> </project>

    Along with the default buildall task,build.xml reveals tasks for conveniently building the JAR file itself (ant buildjar), and building the examples without the JAR (ant buildex)--the JAR file must exist before building the examples. We can also generate Javadoc documentation (ant gendoc), and delete the binary and documentation directories (ant cleanup).

    Let's run the example applications. At the command line, change to the Ant-created bin\examples directory. Specify thejava command with the -cp or-classpath options to add iioutil.jar to the classpath, and specify an application classname. For example, specify java -cp ../iioutil.jar;. Example2 to run the Example2 application.


    Want more Image I/O utilities? Chet Haase's "ImageIO: Just another example of better living by doing it yourself" blog entry presents an ImageScaler application for creating scaled JPEG images. Consider migrating this application into a version ofIIOUtilities.saveImageToFile() that scales an image to a desired size, and then saves the scaled image.