javaswingawtscreen-resolutionimageicon

Can I supply image icons in Java in a higher resolution to avoid blurred icons after scaling?


I am designing a GUI with Java Swing and AWT (Java 8) and am struggling with the icons I use.

I load a large PNG image and scale it to 18x18px and then use it in a button or label. It works well in all resolutions when the operating system does not zoom in.

However, with the advent of large screen resolutions (hidpi), it is common practice to use operating system settings to zoom in on user interface controls, including buttons and such things in Java applications. For example, on Windows I use a 150% or 200% scaling of user elements with my 4K resolution to ensure the user interface is still usable. I imagine many users will do so as well.

When that is the case, however, the icons are merely increased in size after already scaling them down to 18x18px. That is, I first scale them down and then the operating system tries to scale them up again with the little information that is still left in the image.

Is there any way to design image icons in Java that are based on a higher resolution when the zooming/scaling capabilities of the operating system are used in order to avoid them appearing blurred?

Here is a working example:

import java.awt.Container;
import java.awt.Image;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

@SuppressWarnings("serial")
class Example extends JFrame {

    public static void main(String[] args) {
        new Example();
    }
    
    public Example() {
        Container c = getContentPane();
        JPanel panel = new JPanel();
        ImageIcon icon = new ImageIcon(new ImageIcon(getClass().getResource("tabler-icon-beach.png")).getImage().getScaledInstance(18, 18, Image.SCALE_SMOOTH));
        JButton button = new JButton("Test button", icon);
        panel.add(button);
        c.add(panel);
        this.pack();
        this.setLocationRelativeTo(null);
        this.setVisible(true);
    }
}

You can find the icon here. All icons are available as PNG or SVG files.

To illustrate the problem, let me first show you two screenshots in the normal 100% screen resolution:

On Linux with 100% zoom:

100% zoom on Linux

On Windows with 100% zoom:

100% zoom on Windows

And now when I set Windows 7 to have a 200% magnification of layout elements, it's obviously just the 18x18px version stretched out, which becomes blurred:

200% zoom on Windows

Is there any way to provide a higher-resolution image icon that is used when the operating system uses a scaling that is larger than 100%? Moreover, you can see that even at 100% the image quality is not perfect; is there any way to improve that as well?


Solution

  • Java 8 does not support High DPI, the UI gets scaled up by Windows. You should use Java 11 or a later version which support per-monitor High DPI settings.

    If your goal is to make the icons look crisp, prepare a set of icons for different resolutions using BaseMultiResolutionImage (the basic implementation of MultiResolutionImage) to provide higher resolution alternatives. (These are not available in Java 8.)

    You say that you scaled down the original image (240×240) to 18×18px. If the UI needs a higher resolution according to the system setting, all it has now is your small icon (18×18) which will be scaled up, which results in poor quality. You should use a MultiResolutionImage or paint the original image into the required size, letting Graphics to scale it down for you.

    No Down-Scale

    This is the simplest way I came up with to make the icon 18×18 without downscaling the original image:

    private static final String IMAGE_URL =
            "https://tabler-icons.io/static/tabler-icons/icons-png/beach.png";
    
    private static ImageIcon getIcon() {
        return new ImageIcon(Toolkit.getDefaultToolkit()
                                    .getImage(new URL(IMAGE_URL))) {
            @Override
            public int getIconWidth() {
                return 18;
            }
            @Override
            public int getIconHeight() {
                return 18;
            }
    
            @Override
            public synchronized void paintIcon(Component c, Graphics g,
                                               int x, int y) {
                g.drawImage(getImage(), x, y, 18, 18, null);
            }
        };
    }
    

    I left out the exception handling code for MalformedURLException which can be thrown from the URL constructor.

    In this case, the painted image gets down-scaled each time it's painted, which is ineffective. Yet the quality is better. Well, for the standard resolution screen, it's nearly the same as if you down-scaled the image when loading. But in High DPI case, it looks better. It's because for 200% UI Scale, the image will be rendered to 36×36 pixels and these pixels will be created from the source of 240×240 rather than up-scaling the down-scaled version which lost its quality.

    The screenshot of the app without down-scaling at 200%

    To get even better results, I recommend using MultiResolutionImage.

    MultiResolutionImage

    The app below loads the images from base64-encoded strings (for simplicity so that there are no external dependencies). There are three variants provided: 24×24 (100%, 96dpi), 36×36 (150%, 144dpi), 48×48 (200%, 192dpi).

    If the current scale factor is set to any of the provided resolutions, the image will be rendered as is. If 125% or 175% are used, the larger image will be scaled down; if the scale is greater than 200%, then the image for 200% will be scaled up. You can add more resolutions if needed.

    The app doesn't compile in Java 8 because MultiResolutionImage is not available there. To compile it with JDK 11, you have to replace text blocks with regular String concatenation.

    import java.awt.Image;
    import java.awt.image.BaseMultiResolutionImage;
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.util.Arrays;
    import java.util.Base64;
    
    import javax.imageio.ImageIO;
    import javax.swing.ImageIcon;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.SwingUtilities;
    
    public class BeachIconButton {
        public static void main(String[] args) {
            SwingUtilities.invokeLater(BeachIconButton::new);
        }
    
        private BeachIconButton() {
            JPanel panel = new JPanel();
            ImageIcon icon = getIcon();
            JButton button = new JButton("Test button", icon);
            panel.add(button);
    
            JFrame frame = new JFrame("Beach Icon Test");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().add(panel);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        }
    
        private static ImageIcon getIcon() {
            return new ImageIcon(
                   new BaseMultiResolutionImage(
                   Arrays.stream(new String[] { BEACH_100, BEACH_150, BEACH_200})
                         .map(BeachIconButton::loadImage)
                         .toArray(Image[]::new)));
        }
    
        private static Image loadImage(String base64) {
            try {
                return ImageIO.read(new ByteArrayInputStream(
                                    Base64.getMimeDecoder().decode(base64)));
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        private static final String BEACH_100 = """
                iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAA7DAAAO
                wwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAXxJ
                REFUSInl1E9LVVEUBfAfVJBIIoWGIDiRGtagN7Yg+gTZXPwICU4FyUGDahBRCTrK
                qUVNIxrkJHjWJMKZikIjSyHoYd0GZ186vOef7n2zWnB4e591WXudt/c5/G8Ywiw+
                4gd28QGLuNCt+C18RXHI+onJuuLjIVBgGWOYyPb2syI3q4qfzZw/ir1raMXeNBay
                k+zgfNUC25nTBWxGPhfffI78XfzOVD1FP+5lrgusoweXIv+C6xE3qxYocRFrWZHv
                eBPxA5yJeK9ugRJPdU7QGPoi3+1GfFBqZIFn+BVxC88jft9NgdL9q8gH8MSfUa3V
                5BKXQ6gl9aOdey1N2GBV4VO4ik/h8CWG67rMcQ53Hf5ErOBGXfEGtjKxVSxhHi/w
                LeMe4mQV8ROZ67fSZWrHadyW7kKBqaonuC89Bcc5uyKNZ+M4wTvS9d/AY/RmXK80
                iht/wa+HVgfKB61cTYzGaups8FH89kEFhjBSUzDnR0LrSFT5Sw7i/yH8BmQ0mnmX
                f2wqAAAAAElFTkSuQmCC""";
    
        private static final String BEACH_150 = """
                iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAACXBIWXMAABYlAAAW
                JQFJUiTwAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAoVJ
                REFUWIXt1kuoTVEYB/DfdZWiq7gykZBHHokyw0QGREpKLgMDGSojQ6+JARFi4JFn
                ySPyyoCJR8zcIUVJiPLopiMhLoO1d3ftc+7Z95xrnYHyr1V7feu/1/ff3/etby/+
                4x9DW8K9ZmE9FmTP7fiBp3iE03iS0F9dTMQN/G5gvMLMVopZhi8NisnHD6xohZiV
                2ea5o584h+WYgAclor5jaUoxE9ATOXiOednaUMUU9mKPkK5YVCXbJwkuKkZmYbS2
                t8rxpsz+QW2krqYSdF5tCnZjCX5F9h0Zf25k66nizEghaBSOV22cRyt/vhnxd0b2
                E7gQzXelEJRjnv6L96dQ9IR+9CxaWymcsnx+L6UgQnPtUizyfDwRaiiff8JwjIts
                71MLytGBy0I9xaLitG7LuCMi2+d4kyEJBVWwWkhPfz568UaI6Mho/XtCDTXYqO/L
                v+GF2jQ+wNZofrdVYjrwNnK0XYjOYSEt9bp20lMWY0/k5LVQJzlGCn2qur56Mb0V
                YiYLKcodddXhTcX1iJesU1cj/nc9NPA9axE2Cw02GUYITe6UYgoOCv+29pTOyjAH
                VxRT1N/4iP0Y2yohHTij9v810Khgiwavy43eqSfhGmZHtt/oxm2h4fUI0Zim75IW
                4wI24GuDPutiCF4q1sklAx/XJXisGK2zfysGhum7N3/F2ibebce+SNCdFIJgMQ4J
                F63BYBUOYEqzL87HLaEeKrivPCLrMk5lEPyezNf8MkHv9H9STioegDbF/vM3/Hdl
                grpLnBxFJ8bgWAmvWX53maBOId/jMRpHGnB8JOM2yx+f+eosE1SNNiH89TY/oTY1
                zfAHjS7hMv45G3exJiH/P/5d/AHE21JDZYKOHAAAAABJRU5ErkJggg==""";
    
        private static final String BEACH_200 = """
                iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAB2HAAAd
                hwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAA5FJ
                REFUaIHt2ctrXFUcB/BPYoWGVGOwBZtYQQoqhUp9IFEoulC04spGExQEpYoiVBBc
                qVAVwYX/gEWqooKboItqN4oV0frWPgR3XRSl9mHT2GjaJh0X5w6ZOXPuzO08roL5
                wlnM+T3O93cev3vOb1jCEv7f6CtpnBswjg1Yh2HM4wx+wh58jC9K4lMIfXgIB1Ap
                2H7FC7jwX+Bbh2uwW3HiqUDWlE26io040YJgkXYKd5TM3Z34K0HmNKYwgavxgLD/
                WwUxi1vKIr8GfyRIfIAravTuxtmE3g+Jvkrm86pek+/XuOfP4elIbz1mIr15bBUm
                IG8lPtHjjLkpMeg+jNboDOKXSOcsJjP5bU0CqODBsgOoHsRnsRyvJ+RP1vh4PpId
                in7v7WUA/diRE0QFhxN92yMfeyL5MxoP+nW9DAJuxY85QdS2AxiosVsrnJmqfAGX
                4/3I7uVeBwAX4DEcaRLAFFbV2LwSyXdn/ZNR/86es6/BCmyTTpnVWX4Tl2pMv49k
                PtZF/QfLIl+L9fhN/mrEK3XE4vYajGQzZRKPcTuOyw+k2rbV2FwSyY7GTvt7yTjC
                UQwV0FspEKf+nMBcVxmdJz7VeEVYkF6F43gKD0f9H5bOOsN9Gkluws04lpBV21z0
                +7myiRO+wgcjInE6HE/opNpYOZTrEV8RzghX6hjLhWvHKWny35RBNsZogtCrLWxG
                8JrG8zHRO5r5eCci8bvFDNMKY/hMeNRsF77wpWJM/f2mgke7PciyLvtbKby4NuJe
                9Y+QGdyEv7FLSJX/CfThfqGmM691Jqlkep8L6bWs2lQSd+F7xUjnte+EYkCpGMQb
                HRKP247M73mhneVbhY9wY0J2TpjRnUKV4bBQIxrGalyPe4RSY+oe9q1who61wasQ
                +vC1xtlbwFu4sqCftXhb+i70lR6ei+HEgPtxbZv+NuDnhM/hjpnmoE+YoepAu7Sx
                byOsELZkWyvQzlINCTWaGbwnpMROsUx4/16Md3GyE2cDQqXsS0wLRPfhJfWFqjyM
                Zrr78WcX7KczLlvVVzJyMSU/1U1brKSlMJnp9Mp+qkgAs00cVL+imxN244p9iTux
                ny0SQLMVqLY5PCHs2SGhRHi6gF2n9oVWYABbhOLUZbgoGyx+3rUi+Hhm26n96ozL
                FgXPQB4262yLdGrfFUxo/vfRCc1fTp3adwUjeFEod5/M2l7h38WREuyXsIQl1OAf
                9zFZ1uiy3BkAAAAASUVORK5CYII=""";
    }