6

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)
Nuoji
  • 3,438
  • 2
  • 21
  • 35
  • “I've tried playing around with MultiResolutionImage but with no success.” Show us that code. And please explain what happened when you tried it. – VGR Jun 10 '19 at 14:35
  • `I fail to find a way to set the ImageIcon to be treated as a high DPI image` - I don't know what that means. You can resize an image larger as well. However the quality will suffer. – camickr Jun 10 '19 at 16:07
  • @VGR I simply created a BaseMultiResolutionImage out of the highres + lowres version of the image, then fed that to ImageIcon. I got a runtime error from that. – Nuoji Jun 11 '19 at 10:53
  • @camickr In UIKit, to take an example, an image has both a scaling factor and a size. So you could create an image with a scaling factor of 2 and size of 32x32. That image would have an actual pixel size of 64x64. So you could then take that image and copy it to a 64x64 / scale factor 1 or 16x16 / scale factor 4. – Nuoji Jun 11 '19 at 10:57
  • Please add the full stack trace of the runtime error to your question. along with the code that caused it. – VGR Jun 11 '19 at 11:49
  • @VGR stacktrace + code added – Nuoji Jun 11 '19 at 12:05
  • I'm now starting to suspect that there is a bug in the JDK involving buttons and the multi-sized images. – Nuoji Jun 11 '19 at 13:38
  • Like this https://bugs.openjdk.java.net/browse/JDK-7183828? – nicktalbot Jun 11 '19 at 14:37

2 Answers2

6

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
nicktalbot
  • 416
  • 2
  • 6
  • Oh, this is a brilliantly helpful answer - thanks a 10^6! Idea: maybe you could include a `Thread.yield()` inside the while loop? – Luke Usherwood Oct 03 '20 at 15:30
3

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);
Nuoji
  • 3,438
  • 2
  • 21
  • 35
  • 1
    That is a rather roundabout way of doing what `ImageIO.read(MyApplication.class.getResource("res/icons/toolbar/" + iconName))` does. – VGR Jun 11 '19 at 14:27
  • 1
    I came to the same conclusion. By coincidence I was implementing something very similar today, except instead of using a larger raster image I am loading a SVG file and then rendering to multiple resolutions. I didn't hit the "Invalid Image variant" because rasterising an SVG using Apache Batik results in multiple ToolkitImage instances that are each backed by BufferedImage. One quirk I did need was to wait for the image to finish loading, else getWidth could return -1 to Swing which caused it to render in the wrong location. Will post the code. – nicktalbot Jun 11 '19 at 14:46