JDK-8212226 : SurfaceManager throws "Invalid Image variant" for MultiResolutionImage (Windows)
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.awt
  • Affected Version: 9,11,12,14,15
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_10
  • CPU: x86_64
  • Submitted: 2018-10-05
  • Updated: 2024-11-20
  • Resolved: 2020-08-05
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11 JDK 16
11.0.10-oracleFixed 16 b12Fixed
Related Reports
Relates :  
Description
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



Comments
Fix Request (11u) I would like to backport this patch to 11u for parity with Oracle 11.0.10-oracle. The original patch does not apply cleanly. 11u patch has been reviewed.
02-09-2020

11u code review: https://mail.openjdk.java.net/pipermail/jdk-updates-dev/2020-September/003739.html
01-09-2020

URL: https://hg.openjdk.java.net/jdk/jdk/rev/228f79238630 User: psadhukhan Date: 2020-08-15 05:39:36 +0000
15-08-2020

URL: https://hg.openjdk.java.net/jdk/client/rev/228f79238630 User: kizune Date: 2020-08-05 09:52:56 +0000
05-08-2020

I see it - but this is exactly what is broken. Should drawHiDPIImage return false then we just redraw the old image until next time draw is called on the surface, but because it returns null we assume that this image is not suitable for HiDPI painting and unsuccessfully trying to scale it out instead. The mechanism using for scaling out does not work (and not intended to work) with the MultiResolutionImage - that should be dealt with by drawHiDPI.
27-05-2020

FYI, I remember that at some point it was attempted to implement that initially we draw the default image resolution(if the correct resolution variant is not ready yet.) Not sure that it actually works this way.
25-05-2020

The reason is that in SinGraphics2D.drawHiDPIImage() method in case of MultiResolutionImage we are creating a new ImageObserver with the initial width and height set to -1. Then we immediately asking the observer to provide image width and height. Since the observer initialization is asynchronous first time it returns the (-1,-1) as a dimension stating that it is not initialized yet. In order to fix this issue i propose to return Boolean.FALSE to indicate that image is still changing and should be painted later.
25-05-2020

http://mail.openjdk.java.net/pipermail/awt-dev/2020-March/015822.html
30-03-2020

I used 175% scaling in my Windows 7 laptop attached to external monitor. After commenting waitForImage(ret); code i am seeing invalid image variant exception: 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.renderImageXform(DrawImage.java:414) at java.desktop/sun.java2d.pipe.DrawImage.transformImage(DrawImage.java:264) 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.paint(JComponent.java:1083) at java.desktop/javax.swing.JComponent.paintToOffscreen(JComponent.java:5255) at java.desktop/javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:246) at java.desktop/javax.swing.RepaintManager.paint(RepaintManager.java:1323) at java.desktop/javax.swing.JComponent._paintImmediately(JComponent.java:5203) at java.desktop/javax.swing.JComponent.paintImmediately(JComponent.java:5013) at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:888) 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) 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$4.run(EventQueue.java:721) at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715) at java.base/java.security.AccessController.doPrivileged(AccessController.java:389) at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85) 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)
20-11-2018

SunGraphics2D.drawHiDPIImage should return false instead of null if img is a MultiResolutionImage with zero dimensions. The issue require a multi-monitor setup on Windows 10 with one HiDPI monitor and one regular monitor. Tested with environment: 1920*1080 with 100% scaling (External Monitor) Windows 10 Laptop (1920*1080) with 175% scaling (Laptop Scree) Checked with JDK 9, 10.0.2, 11 and 12 ea b14 and failed to run with bwloe exception: Exception in thread "AWT-EventQueue-0" java.lang.RuntimeException: Unexpected MediaTracker error [sun.awt.image.ToolkitImage@6f820883] The issue still need to be verfied in actual HiDPI environment as reported at: https://github.com/eirikbakke/InvalidImageVariantBugExhibit/tree/master/src/invalidimagevariant Actual reported exception is: Exception in thread "AWT-EventQueue-0" java.lang.IllegalArgumentException: Invalid Image variant To verify, use the attached source code with respective .png files.
16-10-2018