我能否以更高分辨率提供 Java 中的图像图标以避免缩放后图标模糊?

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

我正在使用 Java Swing 和 AWT (Java 8) 设计一个 GUI,并且我正在努力使用我使用的图标。

我加载了一个大的 PNG 图像并将其缩放为 18x18 像素,然后在按钮或标签中使用它。当操作系统不放大时,它在所有分辨率下都能正常工作。

但是,随着大屏幕分辨率 (hidpi) 的出现,通常的做法是使用操作系统设置来放大用户界面控件,包括 Java 应用程序中的按钮和此类内容。例如,在 Windows 上,我使用 4K 分辨率对用户元素进行 150% 或 200% 缩放,以确保用户界面仍然可用。我想很多用户也会这样做。

然而,在这种情况下,图标只是在缩小到 18x18 像素后尺寸有所增加。也就是说,我首先将它们缩小,然后操作系统尝试使用图像中仍然存在的少量信息再次将它们放大。

在使用操作系统的zooming/scaling功能时,有什么方法可以设计基于更高分辨率的Java中的图像图标,以避免它们显得模糊吗?

这是一个工作示例:

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);
    }
}

您可以找到图标 here。所有图标均以 PNG 或 SVG 文件形式提供。

为了说明问题,先给大家看两张正常100%屏幕分辨率的截图:

开启 Linux 100% 缩放:

开启 Windows 100% 缩放:

而现在当我设置Windows 7 将布局元素放大200% 时,显然只是18x18px 版本被拉伸,变得模糊:

有什么方法可以提供更高分辨率的图像图标,以便在操作系统使用大于 100% 的缩放比例时使用?此外,您可以看到即使是 100% 的图像质量也不完美;还有什么办法可以改善吗?

Java 8 不支持高 DPI,UI 被放大 Windows。您应该使用支持 per-monitor 高 DPI 设置的 Java 11 或更高版本。

如果您的目标是使图标看起来清晰,请使用 BaseMultiResolutionImage (the basic implementation of MultiResolutionImage) 准备一组不同分辨率的图标以提供更高分辨率的替代方案。 (这些在 Java 8 中不可用。)

你说你把原图(240×240)缩小到18×18px。如果UI根据系统设置需要更高的分辨率,它现在只有你的小图标(18×18)会被放大,导致质量很差。您应该使用 MultiResolutionImage 或将原始图像绘制成所需的大小,让 Graphics 为您缩小它。

没有Down-Scale

这是我在不缩小原始图像尺寸的情况下制作 18×18 图标的最简单方法:

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);
        }
    };
}

我省略了 MalformedURLException 的异常处理代码,它可以从 URL 构造函数中抛出。

在这种情况下,绘制的图像每次绘制都会得到down-scaled,这是无效的。不过质量更好。好吧,对于标准分辨率屏幕,它几乎与加载图像时 down-scaled 相同。但在高 DPI 情况下,它看起来更好。这是因为对于 200% UI 比例,图像将被渲染为 36×36 像素,这些像素将从 240×240 的源创建而不是 up-scaling down-scaled 版本失去了它的质量。

为了获得更好的结果,我建议使用 MultiResolutionImage

多分辨率图像

下面的应用程序从 base64 编码的字符串加载图像(为简单起见,因此没有外部依赖项)。提供了三种变体:24×24(100%,96dpi),36×36(150%,144dpi),48×48(200%,192dpi)。

如果当前比例因子设置为任何提供的分辨率,图像将按原样呈现。如果使用 125% 或 175%,则较大的图像将按比例缩小;如果比例大于 200%,则 200% 的图像将按比例放大。如果需要,您可以添加更多分辨率。

该应用无法在 Java 8 中编译,因为 MultiResolutionImage 在那里不可用。要用 JDK 11 编译它,您必须用常规字符串连接替换文本块。

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=""";
}