Skip to main content

ImageIO: Just another example of better living by doing it yourself

Posted by chet on July 19, 2004 at 3:17 AM PDT

I've always known that ImageIO was a good thing to use since its inception. It reads and writes more formats than the original Image loading APIs, it has a pluggable interface for new image formats, it's the way of the future, it's more robust than the previous APIs, it's synchronous without the need for MediaTracker or that hacky ImageIcon workaround, blah, blah, blah....

I knew all that without actually working with it because it was apparent from the API. It is the way of the future for image reading and writing, and it is more robust and all the rest of that stuff. It's just a great full-featured package that's written from the ground up to provide what developers have been asking us to provide after using the old Image APIs for years.

But I'd never actually used the darned package...

(Petty justification: The folks working on the libraries for Java don't usually get enough time to play around with using the APIs; we're too busy making the stuff work. That's not to say that the people who wrote ImageIO didn't use it substantially in implementing it; of course they did. But I wasn't involved in that part of the API, and I'm busy enough in my little rendering and performance closet that I didn't take the opportunity to play around with ImageIO since it first came out.)

This all changed in the last month.

See, JavaOne was coming, which meant that I had a few demos to write. And it turns out I needed a couple of image utilities, and I needed them quickly. For both utilities, I began thinking "Now where could I find some simple program to do this ..." and then I realized how easy it would be to accomplish it from scratch in ImageIO. A few lines of code later and, presto!, I had my applications.

Not only did I get exactly the functionality that I was looking for, but I got the satisfaction of actually having written the code myself, which is so darned gratifying to us geeks. As they say, "Just another example of better living by doing it yourself." Or at least that's what I say.

Anyway, on with the code.


ImageScaler

The first utility came from a need to scale some images. We had a demo that was going to show images in a particular size all the time. Rather than scaling them on the fly or caching the scaled versions in the application, we figured it would be better to simply load images that were already of the size needed.

Knowing how these projects tend to go, what I really wanted was the ability to scale our full-size images (always keep the original full-size versions of media around...) down to some arbitrary size. And then, when the designers changed their minds about the size at the last minute, be able to quickly re-scale the images to some other arbitrary size.

We could do this through some image manipulation program, but it would be a major hassle to keep doing it (including having to go through the person that knew the program that ran the macro that ... you get the picture). So what I wanted was a simple standalone application to scale the images.

What I wrote was ImageScaler. This application takes the pathname to a directory (or uses "." as the default) and width/height values. It reads in all image files from that directory, creates a subdirectory ("scaled/"), and writes out scaled versions of those images in the specified width and height.

Here's the code:


package jcg.lhdemo;

import java.awt.*;
import java.awt.image.*;
import javax.imageio.ImageIO;
import java.io.File;

/**
* ImageScaler
*
* This class loads all images in a given directory and scales them to the
* given sizes, saving the results as JPEG files in a new "scaled/" subdirectory
* of the original directory.
*/
public class ImageScaler {
   
    // Default w/h values; overriden by command-line -width/-height parameters
    static int IMAGE_W = 150;
    static int IMAGE_H = 250;
   
    public static void main(String args[]) {
        // Default directory is current directory, overridden by -dir parameter
        String imagesDir = ".";
        for (int i = 0; i < args.length; ++i) {
            if (args[i].equals("-dir") && ((i + 1) < args.length)) {
                imagesDir = args[++i];
            } else if (args[i].equals("-width") && ((i + 1) < args.length)) {
                IMAGE_W = Integer.parseInt(args[++i]);
            } else if (args[i].equals("-height") && ((i + 1) < args.length)) {
                IMAGE_H = Integer.parseInt(args[++i]);
            }
        }
        // new subdirectory for scaled images
        String scaledImagesDir = imagesDir + File.separator + "scaled";
        // directory that holds original images
        File cwd = new File(imagesDir);
        // directory for scaled images
        File subdir = new File(scaledImagesDir);
        subdir.mkdir();
        File files[] = cwd.listFiles();
        // temporary image for every scaled instance
        BufferedImage scaledImg = new BufferedImage(IMAGE_W, IMAGE_H,
                                                    BufferedImage.TYPE_INT_RGB);
        Graphics2D gScaledImg = scaledImg.createGraphics();
        // Note the use of BILNEAR filtering to enable smooth scaling
        gScaledImg.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                                    RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        for (int i = 0; i < files.length; ++i) {
            try {
                // For every file in the directory, assume it's an image and
                // load it
                BufferedImage img = ImageIO.read(files[i]);
                // If we get here, we must have read the image file successfully.
                // Create a new File in the scaled subdirectory
                File scaledImgFile = new File(scaledImagesDir + File.separator +
                                              files[i].getName());
                // Scale the original image into the temporary image
                gScaledImg.drawImage(img, 0, 0, IMAGE_W, IMAGE_H, null);
                // Save the scaled version out to the file
                ImageIO.write(scaledImg, "jpeg", scaledImgFile);
            } catch (Exception e) {
                System.out.println("Problem with " + files[i]);
            }
        }
    }
}


Things to note about ImageScaler:

  • It assumes that the images are in JPEG format. Actually, it does read in the files in any format, but it writes them out in JPEG format with the original filenames, so if the images weren't JPEG to begin with, things will get a bit confusing. We could, of course, use any format that ImageIO supports (or any for which we have plugins); in this case I knew that we were dealing with JPEG images so I made that simplifying assumption in the code. The code could be easily extended to handle other or multiple formats instead.
  • Note the use of the BILINEAR rendering hint to the Graphics2D object; this tells Java 2D to do some smooth filtering on the image to make the scaled version look better than it would by using the default NEAREST_NEIGHBOR method. NEAREST_NEIGHBOR works great for many purposes (and is faster in general), but if you're trying to get higher quality and don't mind waiting just a tad longer for the result, use BILINEAR. And for those developers using jdk 5.0, note that the BICUBIC hint now works; you may get even better results with that value.
  • Aspect ratio: This code assumes that the original images are in the same aspect ratio as that determined by the width/height that we are scaling to. This may not be the case in any given situation, and might cause unattractive artifacts with widely differing aspect ratios. A more robust application could provide the ability to preserve the aspect ratio of the original instead of using the width/height parameters blindly.
  • A batch processing application like ImageScaler which acts on several files in the same format in a loop might be more performant by locating the appropriate reader and writer once, outside the for() loop, and then just calling the appropriate read() or write() functions inside the loop. I'll leave this as an exercise for the reader....


JpegConverter

A couple of weeks after I wrote and used ImageScaler, I ran into another problem where I needed a quick conversion utility. I had a series of BMP images that I wanted to convert to JPEG (in this case, because I needed a compressed image format). Once again I found myself writing a very quick program using ImageIO that did the job. In fact, I probably wrote and ran the program in less time than it would have taken me to find an appropriate utility out on the net, install it, and run it. And, once again, "it's another example of better living by doing it yourself".

This program takes a pointer to a directory (or uses "." by default), reads in all BMP image files in that directory (I constrained JpegConverter to convert only BMP files, but, like ImageScaler, this application could be easily extended to handle multiple formats) and then saves out a JPEG version of each file.

Here's the code:


import java.awt.*;
import java.awt.image.*;
import javax.imageio.ImageIO;
import java.io.File;

/**
* JpegConverter
*
* This class loads all BMP images in a given directory and saves each as a JPEG
* file in the same directory.  This code is specific to BMP, but it could be
* easily extended to read images of any type that ImageIO handles.
*/
public class JpegConverter {
   
    public static void main(String args[]) {
        // Default directory is current directory, overridden by -dir parameter
        String imagesDir = ".";
        for (int i = 0; i < args.length; ++i) {
            if (args[i].equals("-dir") && ((i + 1) < args.length)) {
                imagesDir = args[++i];
            }
        }
        // directory that holds original images
        File cwd = new File(imagesDir);
        File files[] = cwd.listFiles();
        for (int i = 0; i < files.length; ++i) {
            String fileName = files[i].getName();
            // converstion to lower case just for ease of replacing the
            // filename extension
            String fileNameLC = fileName.toLowerCase();
            if (fileName.endsWith("bmp")) {
                try {
                    // Replace original "bmp" filename extension with "jpg"
                    int extensionIndex = fileNameLC.lastIndexOf("bmp");
                    String fileNameBase = fileName.substring(0, extensionIndex);
                    BufferedImage img = ImageIO.read(files[i]);
                    // create new JPEG file
                    File convertedImgFile =
                            new File(imagesDir + File.separator +
                                     fileNameBase + "jpg");
                    // store original file out in JPEG format
                    ImageIO.write(img, "jpeg", convertedImgFile);
                } catch (Exception e) {
                    System.out.println("Problem with " + files[i]);
                }
            }
        }
    }
}

Notes:

  • Some of the notes for ImageScaler above apply to JpegConverter as well, so consider this note as a dereference to the notes above...


And so, in conclusion

Some readers might wonder: "What's the big deal?" After all, these applications are pretty simple, so why all the fuss?

Exactly my point; thanks for clarifying it. It is easy. So easy that I would encourage everyone to think about using ImageIO next time you have a simple (or hard) image input/output problem come up; see if the ImageIO API doesn't make solving that problem just a tad easier.

Related Topics >>