
I’m working on a small application to help me with naming and sorting my pictures, so one of the basic things it needs to do is load and scale an image. Found lots of information here and there on this, and this post is to share what I came up with.
When searching around trying to figure stuff like this out, it’s hard to credit where you found everything. A lot of search results point to forum.java.sun.com, with discussions that are alternatively helpful and abusive to newbie questions. When I started leaning towards a solution based on overriding the “paint” method, one of the forum threads pointed to a good tutorial: “Lesson: Performing Custom Painting,” and an in-depth explanation: “Painting in AWT and Swing.”
The core of this is a class that extends javax.swing.JPanel, uses javax.imageio.ImageIO to read the file in to a java.awt.Image class, and then calls the Image.getScaledInstance() method to scale the image. We override the JPanel’s paintComponent(Graphics g) method to draw the image. (Notice, “paintComponent” is overridden, not “paint.” In my reading I found it is recommended to not override paint.)
There is a way to determine the progress of the image loading and take action based on that, but I didn’t get in to that. I just assume the image is there and that works for my example so I’m not going to worry about it right now. Take a look at the classes in javax.imageio.event if you want to show a progress bar or code defensively around a not-fully loaded image. There is also java.awt.image.BufferedImage that could be used, but I’m getting by ok with plain old Image for now. (This isn’t a high-powered graphics application, you know.) :-)
Trial and Error
Before finally showing the code, I’ll mention a couple of things I tried that didn’t work out so well, which explains some of the reasons behind the eventual solution:
It’s Easier to Gain Than to Lose
Before settling on the custom JPanel approach, I tried using the icon property of the JLabel along with its componentResized event handler to trigger rescaling the image. But this only worked as the image was growing. The JLabel was anchored to grow and shrink with the containing JFrame (form), but when resizing the window to be smaller, the event handler didn’t get called. The JLabel stayed at its largest size. (I later tried and saw that the JPanel did this also.)
Stuck in a Rut
In my paintComponent override, I initially tried scaling the image in the Graphics g.drawImage() statement, but this would cause the method to be called repeatedly (and continually!) despite nothing else changing. The CPU would then start churning away, going up to 99%, with the fan whirring away under the high load.
Searching on this situation brought up some hints. One thread mentioned that you shouldn’t do anything in paintComponent that changes your component, as it will then keep redrawing itself. (Or something like that.) This didn’t seem like it should be a problem in my case. I didn’t think I should be changing anything by drawing the scaled image instead of the base image, and in any case, the next time around the scaled image should be the same as the size hadn’t changed. But it seemed clear that scaling the image dynamically in the paintComponent method was the cause of it getting called over and over and running up the CPU usage (some might say utilization, but I don’t care for that word, although it’s not as bad as saying “utilize” instead of “use”), so I had to try something different.
I created a separate method in ImagePanel (The JPanel extended class) to scale the image, and then called that from the containing JFrame’s componentResized event handler, so that when the ImagePanel component was called on to repaint itself, it would be drawing a plain image (which I had previously noted did not cause the loop problem). And that seems to do the trick.
The Code
I’m working on this project in NetBeans. I’m not going to post the whole project at this time, because I don’t think it’s necessary to demonstrate this particular approach. I’ll leave out all the generated GUI code in my sample. This means I don’t have a nice self-contained demo for you, but if you’re trying to do this you should already have a point of entry to work with, I hope.
ImagePanel.java
(This class has the complete code.)
/* * ImagePanel.java * * Copyright (C) 2007 Scott Carpenter (scottc at movingtofreedom dot org) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Created on November 9, 2007, 4:07 PM * */ package PicHelper; import java.io.*; import java.awt.*; import javax.swing.*; import javax.imageio.*; public class ImagePanel extends JPanel { private Image image; private Image scaledImage; private int imageWidth = 0; private int imageHeight = 0; //private long paintCount = 0; //constructor public ImagePanel() { super(); } public void loadImage(String file) throws IOException { image = ImageIO.read(new File(file)); //might be a situation where image isn't fully loaded, and // should check for that before setting... imageWidth = image.getWidth(this); imageHeight = image.getHeight(this); setScaledImage(); } //e.g., containing frame might call this from formComponentResized public void scaleImage() { setScaledImage(); } //override paintComponent public void paintComponent(Graphics g) { super.paintComponent(g); if ( scaledImage != null ) { //System.out.println("ImagePanel paintComponent " + ++paintCount); g.drawImage(scaledImage, 0, 0, this); } } private void setScaledImage() { if ( image != null ) { //use floats so division below won't round float iw = imageWidth; float ih = imageHeight; float pw = this.getWidth(); //panel width float ph = this.getHeight(); //panel height if ( pw < iw || ph < ih ) { /* compare some ratios and then decide which side of image to anchor to panel and scale the other side (this is all based on empirical observations and not at all grounded in theory)*/ //System.out.println("pw/ph=" + pw/ph + ", iw/ih=" + iw/ih); if ( (pw / ph) > (iw / ih) ) { iw = -1; ih = ph; } else { iw = pw; ih = -1; } //prevent errors if panel is 0 wide or high if (iw == 0) { iw = -1; } if (ih == 0) { ih = -1; } scaledImage = image.getScaledInstance( new Float(iw).intValue(), new Float(ih).intValue(), Image.SCALE_DEFAULT); } else { scaledImage = image; } //System.out.println("iw = " + iw + ", ih = " + ih + ", pw = " + pw + ", ph = " + ph); } } }
(Updated, 13 Nov 2007: Simplified the scaling code. For a while I had been dividing ints and not realizing the result would be an int, and this led to some hairier-than-necessary comparisons to preserve the aspect ratio. I later changed to floats but didn’t think to reevaluate the code.)
PicHelperUI.java
NetBeans makes it easy to add event handlers and shields me from having to understand all the details. I left in one part of the generated code to show where the formComponentResized listener is added.
/* * PicHelperUI.java * * Copyright (C) 2007 Scott Carpenter (scottc at movingtofreedom dot org) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Created on November 10, 2007, 3:15 PM * */ package PicHelper; public class PicHelperUI extends javax.swing.JFrame { //constructor public PicHelperUI() { initComponents(); //temp try { picPanel.loadImage("pics/taller.jpg"); //picPanel.loadImage("pics/wider.jpg"); } catch (Exception e) { e.printStackTrace(); } } /** This method is called from within the constructor to * initialize the form. * WARNING: Do NOT modify this code. The content of this method is * always regenerated by the Form Editor. */ // GENERATED CODE private void initComponents() { // Snipped a bunch of GUI code, but wanted to show: addComponentListener(new java.awt.event.ComponentAdapter() { public void componentResized(java.awt.event.ComponentEvent evt) { formComponentResized(evt); } }); // Snipped a bunch more GUI code } // END GENERATED CODE private void formComponentResized(java.awt.event.ComponentEvent evt) { picPanel.scaleImage(); } /** * @param args the command line arguments */ public static void main(String args[]) { java.awt.EventQueue.invokeLater(new Runnable() { public void run() { new PicHelperUI().setVisible(true); } }); } // Variables declaration - do not modify private javax.swing.JPanel panelSide; private javax.swing.JPanel panelTop; private PicHelper.ImagePanel picPanel; // End of variables declaration }
Related: Picture Filer: A free Java program for naming and sorting image files (Has a newer version of ImagePanel.)

9 Comments
Be thankful for the javax imageio libraries. Back when I first started loading JPEGs with the old awt image loaders, they were horribly slow. It could take 10 to 15 seconds to load an average quality JPEG. BufferedImages help too and they’re really not too complicated. I’ve got examples in my Isszy application at http://penguindreams.org/page/see/Isszy
I apologize if the code looks horrid. It was an early project. :)
15 November 2007 at 3:56 pm
Hi, Sumit. I do often feel thankful when I start learning something and find references to older ways. The challenge is to find the page that says, “Don’t use X anymore. Y is much better,” before finding the page explaining X to you. For images, that would be the MediaSomethingOrOther method, I think.
Thanks for the pointer to your project — looks like you had the same idea. I’d probably be able to cannibalize some of your code, but I don’t see any kind of license information with it. Would you consider releasing it under GPL v3?
15 November 2007 at 4:24 pm
I faced the same problem of repeated call to paint method when scaling ( getScaledInstance ) is performed inside paint. Solved by passing null (instead of ‘this’) in drawImage method. If images are preloaded, passing null as ImageObserver is not a bad idea it seems.
1 December 2008 at 9:28 am
Thanks, Java Coder.
1 December 2008 at 4:54 pm
I’ve been subclassing this widget extensively to fit my purposes, and I’ve noticed the following.
I use it to load 9 GIFs that are about 3000x2000px in size. Of course, they get stored in memory as uncompressed bitmaps (although I’d be glad if there was any other way, I haven’t researched it yet), so I increase Java’s heap to 128 MB max.
I’ve implemented my own setZoom method, so the images start out as zoomed to 50%. I switch to each of the 9 tabs, and the scaledImage of every instance gets created. If I try to zoom one of them to 100%, I run into an out of memory error. However, if I first zoom out to 25%, and then in to 100%, the image is displayed.
Does this mean that “scaledImage=image;” actually copies image? Or is this a side effect of floating point arithmethics, that zoomfactor is never really 1.0f? In my setScaledImage() method, I try setting scaledImage = null; beforehand, but it doesn’t help.
20 August 2009 at 8:50 am
I’d be surprised if
scaledImage = imagemade a copy.I don’t have any real insight to offer — sorry!
20 August 2009 at 9:08 am
Okay, so what I discovered is that my program runs out of memory in setScaledImage only if zooming to 200% – and that’s always, and for some reason the exception is caught twice (and the widget is set do display a text saying “Not enough memory to display the image at this zoom level”). The error when switching to 100% is in fact in paintComponent. I’ll let you know if I find a solution.
20 August 2009 at 5:00 pm
Another concern I have, though, is that the ImagePanel, when put in a JScrollPane, doesn’t scroll. Also, the lowered framerate with which my custom overlays are drawn at higher zoom levels means that the entire image, and not just the portion visible, is being painted. How about that?
20 August 2009 at 5:24 pm
This is great, thanks! Looks just what I need. But one question: Do you know how can I use this panel from NetBeans GUI Builder? It would be great if I could do so.
22 April 2010 at 6:23 am