Generating HiDPI ImageIcon from original in Java Swing

Viewed 1142

I have an app which would use JLabel with an ImageIcon with an icon size of 32x32.

I'd now like to use a 64x64 image, load it and resize it to 32x32 if low DPI, otherwise use it as a high DPI image.

Resizing is easy, this trick works for example:

ImageIcon icon = ...
Image lowRes = icon.getImage().getScaledInstance(32, 32, Image.SCALE_SMOOTH);
return new ImageIcon(lowRes);

However, I fail to find a way to set the ImageIcon to be treated as a high DPI image.

I've tried playing around with MultiResolutionImage but with no success.

EDIT: Attempt using MultiResolutionImage in a naive way:

private ImageIcon loadIcon(String iconName)
{
  ImageIcon icon = new ImageIcon(getClass().getClassLoader()
                       .getResource("res/icons/toolbar/" + iconName));
  BaseMultiResolutionImage baseMultiResolutionImage = new BaseMultiResolutionImage(
    icon.getImage().getScaledInstance(32, 32, Image.SCALE_SMOOTH),
    icon.getImage()
  );
  return new ImageIcon(baseMultiResolutionImage);
}

Stacktrace:

2019-06-11 14:00:45,962 ERROR [AWT-EventQueue-0] Catch.all - Uncaught exception on [AWT-EventQueue-0]: Invalid Image variant
java.lang.IllegalArgumentException: Invalid Image variant
    at java.desktop/sun.awt.image.SurfaceManager.getManager(SurfaceManager.java:82)
    at java.desktop/sun.java2d.SurfaceData.getSourceSurfaceData(SurfaceData.java:218)
    at java.desktop/sun.java2d.pipe.DrawImage.renderImageCopy(DrawImage.java:572)
    at java.desktop/sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:67)
    at java.desktop/sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:1027)
    at java.desktop/sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3415)
    at java.desktop/sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3391)
    at java.desktop/javax.swing.ImageIcon.paintIcon(ImageIcon.java:425)
    at java.desktop/com.apple.laf.AquaButtonUI.paintIcon(AquaButtonUI.java:395)
    at java.desktop/com.apple.laf.AquaButtonUI.paint(AquaButtonUI.java:304)
    at java.desktop/javax.swing.plaf.ComponentUI.update(ComponentUI.java:161)
    at java.desktop/javax.swing.JComponent.paintComponent(JComponent.java:797)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1074)
    at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:907)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1083)
    at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:907)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1083)
    at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:907)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1083)
    at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:907)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1083)
    at java.desktop/javax.swing.JLayeredPane.paint(JLayeredPane.java:590)
    at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:907)
    at java.desktop/javax.swing.JComponent.paintToOffscreen(JComponent.java:5262)
    at java.desktop/javax.swing.RepaintManager$PaintManager.paintDoubleBufferedImpl(RepaintManager.java:1643)
    at java.desktop/javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(RepaintManager.java:1618)
    at java.desktop/javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1556)
    at java.desktop/javax.swing.RepaintManager.paint(RepaintManager.java:1323)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1060)
    at java.desktop/java.awt.GraphicsCallback$PaintCallback.run(GraphicsCallback.java:39)
    at java.desktop/sun.awt.SunGraphicsCallback.runOneComponent(SunGraphicsCallback.java:78)
    at java.desktop/sun.awt.SunGraphicsCallback.runComponents(SunGraphicsCallback.java:115)
    at java.desktop/java.awt.Container.paint(Container.java:2002)
    at java.desktop/java.awt.Window.paint(Window.java:3926)
    at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:876)
    at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:848)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:389)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:848)
    at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:823)
    at java.desktop/javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:772)
    at java.desktop/javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1884)
2 Answers

This is an example of taking an SVG icon and creating a multi-resolution image for hidpi screen support as referenced in a previous comment:

  // Create a multi-resolution image with all 0.25 scaling steps up to 3x
  final int size = ...; // base size = 16, 24, 32 etc.

  // Create all resolution variants that Windows 10 offers by default
  // Could probably drop some, e.g. 1.25 = 2.50 / 2 (Swing should handle that...)
  final List< Integer > sizes = ImmutableList.of(
      (int) ( size * 1.00 ), // Base image
      (int) ( size * 1.25 ),
      (int) ( size * 1.50 ),
      (int) ( size * 1.75 ),
      (int) ( size * 2.00 ),
      (int) ( size * 2.25 ),
      (int) ( size * 2.50 ),
      (int) ( size * 2.75 ),
      (int) ( size * 3.00 ) );

  final byte[] rawSvgBytes = ...; // Read bytes from SVG file

  Image[] images = new Image[ sizes.size() ];
  for ( int isize = 0; isize < sizes.size(); isize++ )
  {
    // Create a PNG transcoder
    PNGTranscoder t = new PNGTranscoder();

    // Set the transcoding hints
    t.addTranscodingHint( SVGAbstractTranscoder.KEY_WIDTH, Float.valueOf( sizes.get( isize ) ) );
    t.addTranscodingHint( SVGAbstractTranscoder.KEY_HEIGHT, Float.valueOf( sizes.get( isize ) ) );

    // Create the transcoder input
    TranscoderInput input = new TranscoderInput();
    input.setInputStream( new ByteArrayInputStream( rawSvgBytes ) );
    // Create the transcoder output
    ByteArrayOutputStream ostream = new ByteArrayOutputStream();
    TranscoderOutput output = new TranscoderOutput( ostream );

    // Transcode the image
    t.transcode( input, output );

    // Create an image and ensure its size is initialised
    Image image = Toolkit.getDefaultToolkit().createImage( ostream.toByteArray() );
    while ( image.getWidth( null ) == -1 )
    {
      // HACK! Wait for the image to be loaded, else icons may not render at the correct
      // location as the width and height returned to Swing are -1
    }
    images[ isize ] = image;
  }
  return new ImageIcon( new BaseMultiResolutionImage( images ) ); // First image always the base image

Ok, so the problem is as follows:

The image created by ImageIcon (and getScaledInstance on ImageIcon) are of the type ToolkitImage. These are not the BufferedImage that are expected by Swing.

One working solution is to take the images and then convert them to two BufferedImage instances. Here is an ugly solution to hack the code above:

  ImageIcon icon = new ImageIcon(getClass().getClassLoader().getResource("res/icons/toolbar/" + iconName));

  BufferedImage ax = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
  BufferedImage bx = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);

  Graphics g = ax.createGraphics();
  new ImageIcon(icon.getImage().getScaledInstance(32, 32, Image.SCALE_SMOOTH)).paintIcon(null, g, 0, 0);
  g.dispose();

  g = bx.createGraphics();
  icon.paintIcon(null, g, 0, 0);
  g.dispose();

  BaseMultiResolutionImage baseMultiResolutionImage = new BaseMultiResolutionImage(ax, bx);
  return new ImageIcon(baseMultiResolutionImage);
Related