ADDITIONAL SYSTEM INFORMATION :
Windows 10 and Java 10.0.2, 11-ea, or 12-ea.
Main monitor:
1920x1080 external monitor, non-HiDPI (100% scaling)
Secondary monitor:
2560x1440 Thinkpad X1 Carbon 6th gen laptop screen at 200% HiDPI scaling
A DESCRIPTION OF THE PROBLEM :
(Short version: SunGraphics2D.drawHiDPIImage should return false instead of null if img is a MultiResolutionImage with zero dimensions.)
In a multi-monitor setup on Windows 10, with one HiDPI monitor and one regular monitor, the use of a MultiResolutionImage can cause sun.awt.image.SurfaceManager.getManager to throw an "Invalid Image variant" exception when the containing window is dragged from one screen to another. See the stack trace pasted below.
The problem seems to be that sun.java2d.SunGraphics2D.drawHiDPIImage may return null even when the image is in fact a MultiResolutionImage, if the relevant image alternative has not yet been fully loaded by the time the first paint is attempted. The fix is probably trivial; just have SunGraphics2D.drawHiDPIImage return false instead of null in the cases where img is a MultiResolutionImage but the image can't be drawn yet (e.g. because the width and height is 0).
Waiting for the image to load using MediaTracker.waitForAll() "fixes" the problem for the simplest case. This is not always an option in client code, however; there are other MultiResolutionImage instances being created behind the scenes for instance when a JButton is disabled and its icon needs to be greyed-out.
Note that there is a timing aspect to this bug, so not all configurations might expose it. The two-monitor configuration appears to be needed, so that the loading of the double-resolution HiDPI icon is delayed until the window is dragged from a non-HiDPI screen to a HiDPI screen. In this configuration, however, I can reproduce the bug consistently every time.
See the exhibit at https://github.com/eirikbakke/InvalidImageVariantBugExhibit/blob/master/src/invalidimagevariant/InvalidImageVariantBugExhibit.java , which is also pasted in this bug report (though you'll need the two icon PNG files to run the example).
========== Stack Trace (from 10.0.2; nearly identical on 12-ea) ================
Exception in thread "AWT-EventQueue-0" 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.renderImageScale(DrawImage.java:635)
at java.desktop/sun.java2d.pipe.DrawImage.tryCopyOrScale(DrawImage.java:319)
at java.desktop/sun.java2d.pipe.DrawImage.transformImage(DrawImage.java:258)
at java.desktop/sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:76)
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/javax.swing.plaf.basic.BasicButtonUI.paintIcon(BasicButtonUI.java:358)
at java.desktop/javax.swing.plaf.basic.BasicButtonUI.paint(BasicButtonUI.java:275)
at java.desktop/com.sun.java.swing.plaf.windows.WindowsButtonUI.paint(WindowsButtonUI.java:167)
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.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:1633)
at java.desktop/javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(RepaintManager.java:1608)
at java.desktop/javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1546)
at java.desktop/javax.swing.RepaintManager.paint(RepaintManager.java:1313)
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:2000)
at java.desktop/java.awt.Window.paint(Window.java:3940)
at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:868)
at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:840)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87)
at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:840)
at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:815)
at java.desktop/javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:764)
at java.desktop/javax.swing.RepaintManager.access$1200(RepaintManager.java:69)
at java.desktop/javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1880)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:313)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
at java.desktop/java.awt.EventQueue.access$600(EventQueue.java:97)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1) Find a laptop with Windows 10 and a HiDPI screen. Attach an external monitor.
2) In the Windows 10 "Rearrange Multiple Displays" app, configure the external monitor to be the main monitor. Under "Scale and layout", set the main monitor's scaling level to 100% and the laptop monitor's scaling level to 200%.
3) Run the test application from https://github.com/eirikbakke/InvalidImageVariantBugExhibit (also pasted in this bug report, though you'll need the 16x16 icon_single_size.png and 32x32 icon_double_size.png files alongside the source for the application to work). The test JFrame should open up on the main (non-HiDPI) monitor.
4) Drag the window from the non-HiDPI monitor to the HiDPI monitor. An exception will be thrown.
5) Optional: comment out "waitForImage(ret);" and "button.setEnabled(false);", and repeat steps 3-4. In some cases, the exception will still appear, and sometimes, the icon will have the wrong position even on the non-HiDPI screen.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
As the window is dragged from the non-HiDPI monitor to the HiDPI monitor, the JButton should change its greyed-out icon from the 16x16 image with the text "Icon 1x" to the sharper 32x32 image with the text "Icon 2x". No exception should be thrown.
ACTUAL -
An exception is thrown, with the stack trace as pasted earlier. The icon may appear correctly if repainted again later.
In some cases, the icon may appear in the wrong position, even on the non-HiDPI screen. See step (5) above
and the screenshot at https://github.com/eirikbakke/InvalidImageVariantBugExhibit/blob/master/WrongIconPositionIfNotWaitingForImageToLoad.png
---------- BEGIN SOURCE ----------
// https://github.com/eirikbakke/InvalidImageVariantBugExhibit
package invalidimagevariant;
import java.awt.Canvas;
import java.awt.FlowLayout;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.image.BaseMultiResolutionImage;
import java.net.URL;
import java.util.Arrays;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
/*
Demonstration of an "Invalid Image variant" exception when attempting to use a
MultiResolutionImage on Windows 10 on Java 10.0.2, 11-ea, or 12-ea.
Setup:
* Windows 10
* OpenJDK 10.0.2.
* One HiDPI laptop screen set at scaling "200%" in the Windows "Display" settings app,
combined with a regular non-HiDPI external monitor as the main display.
(Tested on a Lenovo X1 Carbon 6th gen laptop.)
The situation in which the bug was consistently encountered was as follows:
1) Start this application. The JFrame will appear on the primary monitor. The icon in the
button says "ICON 1x".
2) Now drag the Window over to the HiDPI screen. An exception occurs.
(see stack trace attached to this bug report.)
*/
public class InvalidImageVariantBugExhibit {
public static void main(String[] args) {
System.out.println("Java Version: " + System.getProperty("java.version"));
SwingUtilities.invokeLater(InvalidImageVariantBugExhibit::onEDT);
}
private static Image loadImage(String resourceName) {
URL url = InvalidImageVariantBugExhibit.class.getResource(resourceName);
if (url == null)
throw new RuntimeException("Could not find resource URL for " + resourceName);
Image ret = Toolkit.getDefaultToolkit().getImage(url);
if (ret == null)
throw new RuntimeException("Failed to load image " + url);
/* Commenting out this may cause the "Invalid Image variant" exception to be thrown even when
"button.setEnabled(false)" is omitted. It may also cause incorrect icon positioning even on
regular non-HiDPI displays, as shown in the screenshot in
WrongIconPositionIfNotWaitingForImageToLoad.png. */
waitForImage(ret);
return ret;
}
private static void waitForImage(Image image) {
final MediaTracker mt = new MediaTracker(new Canvas());
mt.addImage(image, 0);
try {
mt.waitForAll();
} catch (InterruptedException e) {
throw new RuntimeException("Unexpected interrupt ", e);
}
if (mt.isErrorAny()) {
throw new RuntimeException("Unexpected MediaTracker error " +
Arrays.toString(mt.getErrorsAny()));
}
}
private static void onEDT() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | IllegalAccessException |
InstantiationException | UnsupportedLookAndFeelException e)
{
throw new RuntimeException(e);
}
JFrame frame = new JFrame();
frame.setSize(300, 100);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel(new FlowLayout());
Image img1x = loadImage("/invalidimagevariant/icon_single_size.png");
Image img2x = loadImage("/invalidimagevariant/icon_double_size.png");
BaseMultiResolutionImage mri = new BaseMultiResolutionImage(new Image[] { img1x, img2x });
Icon icon = new ImageIcon(mri);
JButton button = new JButton();
button.setIcon(icon);
button.setText("Test Button");
button.setEnabled(false);
panel.add(button);
frame.getContentPane().add(panel);
frame.setVisible(true);
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
In some cases, it is feasible to wait for the image to load using MediaTracker.waitForAll(). The problem still appears if the image is used as an ImageIcon in a disabled JButton or such, since Swing will in this case derive its own "greyed-out" MultiResolutionImage instance that the client code does not have access to.
FREQUENCY : always