My Software-Developer Blog

Blog-Archiv

Freitag, 31. Oktober 2025

Auto-Scaling Java Swing Image Viewer

Java ImageIO can render JPEG, GIF, PNG, BMP and WBMP image formats. It was introduced long ago in Java 1.4 (Sun times).

Ever wanted to show an image in Java/Swing in a user-friendly way? Here is a 110 lines image-viewer that sizes images proportionally to the window size while keeping the image's aspect ratio. It also resizes the image when the user resizes its container-window.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.*;

public class ImageViewer extends JPanel
{
    private final JTabbedPane tabbedPane;
    private List<BufferedImage> images = new ArrayList<>();
    
    public ImageViewer()  {
        super(new BorderLayout());
        
        add(this.tabbedPane = new JTabbedPane());
        
        // on every frame resize, resize focused image to fit frame
        addComponentListener(new ComponentAdapter() {
            // we need a deferred resize, because component-events come very frequently
            private final ActionListener imageResizer = new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    final int selectedIndex = tabbedPane.getSelectedIndex();
                    if (images.size() > selectedIndex && images.get(selectedIndex) != null) {
                        final BufferedImage image = images.get(selectedIndex);
                        final JComponent selectedTab = (JComponent) tabbedPane.getSelectedComponent();
                        setImageOnTab(selectedIndex, selectedTab, image);
                    }
                }
            };
            
            private final Timer timer = new Timer(500, imageResizer);
            
            {
                timer.setRepeats(false); // default is true
            }
            
            @Override
            public void componentResized(ComponentEvent event) {
                final int selectedIndex = tabbedPane.getSelectedIndex();
                if (selectedIndex >= 0 && images.size() > selectedIndex && images.get(selectedIndex) != null)
                    if (timer.isRunning())
                        timer.restart();
                    else
                        timer.start();
            }
        }); // end ComponentAdapter
    }
    
    /** Replaces all currently shown images by the new ones. */
    public void showImages(File [] files)  {
        tabbedPane.removeAll(); // remove all former images
        images.clear();
        
        for (int i = 0; i < files.length; i++)  {
            final int index = i;
            tabbedPane.addTab(files[index].getName(), new JPanel());
            
            SwingUtilities.invokeLater(() -> { // show image later when tabbedPane has a size
                final JComponent tab = (JComponent) tabbedPane.getComponentAt(index);
                try {
                    final BufferedImage image = ImageIO.read(files[index]);
                    if (image == null)
                        throw new IllegalArgumentException("Can not read "+files[index]);
                    
                    images.add(index, image);
                    
                    setImageOnTab(index, tab, image);
                }
                catch (Exception e) {
                    if (images.size() <= index)
                        images.add(index, null); // occupy index
                    
                    final JLabel error = new JLabel(e.getMessage());
                    tabbedPane.setComponentAt(index, error);
                }
            });
        }
    }

    private void setImageOnTab(int index, JComponent tab, BufferedImage image) {
        final Dimension canvasDimension = new Dimension(tab.getWidth(), tab.getHeight());
        if (canvasDimension.width > 0 && canvasDimension.height > 0) {
            final Dimension imageDimension = new Dimension(image.getWidth(), image.getHeight());
            final Dimension scaled = calculateProportionalScale(imageDimension, canvasDimension);
            final ImageIcon icon = new ImageIcon((scaled != null)
                    ? image.getScaledInstance(scaled.width, scaled.height, Image.SCALE_DEFAULT)
                    : image);
            tabbedPane.setComponentAt(index, new JLabel(icon));
        }
        // else: resize event will render the image
    }

    private Dimension calculateProportionalScale(Dimension image, Dimension canvas) {
        if (canvas.width >= image.width && canvas.height >= image.height)
            return null; // canvas is bigger than image, nothing to do
        
        final double widthRatio  = (double) canvas.width  / (double) image.width;
        final double heightRatio = (double) canvas.height / (double) image.height;
        final double smallerRatio = Math.min(widthRatio, heightRatio);
        
        final int width = (int) Math.round(smallerRatio * (double) image.width);
        final int height = (int) Math.round(smallerRatio * (double) image.height);
        
        return new Dimension(width, height);
    }
}

To have this functionality as reusable as possible, the class extends JPanel (a Swing-component), not JFrame (which is a window). On line 12 we have a nested tabbed pane that will show each loaded image on its own tab. Line 13 allocates a list for all images to load in future, in order of the tabs on which they will be shown.

The constructor on line 15 starts with adding the tabbed pane which has no tabs yet, because no image files were given. It then creates a component-listener on line 21 that listens for resize events of the tabbed pane. This is a little longer than expected, because Swing sends lots of resize events, and scaling the image on each of these events would make no sense. On line 23 we have an ActionListener that performs the resize. The timer on line 35 will call this resizer. On line 37 there is an object-instance-initializer that once sets the timer to NOT repeat its action every 500 milliseconds. Finally on line 42 there is the resize receiver. It prolongs the timer when it has been started already, else it starts it. That means, as long as there are tight resize events coming in intervals below 500 millis, nothing will happen. But when a longer break happens the image will be resized.

What happens on resize is implemented starting from line 25. When a tab was selected and an image is available for it, it calls the setImageOnTab() method. Before we go to that, let's look at the showImages() method on ine 54.

The showImages() can be called multiple times with different image files. It will always dismiss the currently shown images and create new image tabs. Thus it first clears its object-state on line 55 and 56. Then it loops the given files and creates a tab for each one. It does not yet render the image on the created tab, because there might be situations when the tab has no size yet, and then the image would not know how big or small it should become. So on line 62 a deferred image rendering has been implemented through invokeLater(). The showImages() method finishes after starting all these functions to be invoked.

What happens now is that every started function will be executed one by one in the EventQueue, without concurrency. On line 63, the indexed tab is retrieved, then the image file is loaded and added to the images list on line 69. We need this image-cache because on window-resize the image will be scaled again. Finally the setImageOnTab() method is called on line 71, this is the same as we had in the resize-listener on line 30.

The setImageOnTab() method on line 84 doesn't do anything when the given tab (canvas) has no size yet. For sure there will come a resize-event afterwards and the method will be called again from line 30. But when a size of the tab is present, it scales the image to it and then puts a label with the resulting ImageIcon on it. In case the canvas (tab) is bigger than the image, no scaling is needed, this will be fast then.

Whether scaling is needed is decided in calculateProportionalScale() on line 97. If the canvas (tab) is smaller than the image (the normal case for photos), both width- and height-ratios are calculated. The smaller of them will be sufficient to render the image so that it fits perfectly into the tab. On line 105 and 106 the scaled dimension is then calculated and returned on line 108.


Here is main() procedure so that you can test the class. Image files must be given as commandline arguments, like

java -cp . ImageViewer image1.jpeg image2.gif
    /** Test main. */
    public static void main(String[] args) {
        if (args.length <= 0) {
            System.out.println("SYNTAX:java ImageViewer imageFile [imageFile ...]");
        }
        else {
            final File[] files = new File[args.length];
            for (int i = 0; i < args.length; i++)
                files[i] = new File(args[i]);

            final ImageViewer imageViewer = new ImageViewer();
            
            final JFrame frame = new JFrame("Image Viewer"); // title bar text
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().add(imageViewer);
            frame.setSize(new Dimension(400, 400));
            frame.setLocationByPlatform(true);
            frame.setVisible(true);
            
            imageViewer.showImages(files);
        }
    }


That's it, no more. Hope this was helpful for someone, be it due to the scaling algorithm, be it due to Swing techniques!