changeset 9156:794fa1fde9e9

8011059: [macosx] Support automatic @2x images loading on Mac OS X Reviewed-by: serb, flar
author alexsch
date Thu, 20 Apr 2017 20:28:44 +0100
parents fea29ee2099c
children b86ab588efc5
files src/macosx/classes/sun/lwawt/macosx/LWCToolkit.java src/share/classes/java/awt/MediaTracker.java src/share/classes/sun/awt/SunHints.java src/share/classes/sun/awt/SunToolkit.java src/share/classes/sun/awt/image/MultiResolutionImage.java src/share/classes/sun/awt/image/MultiResolutionToolkitImage.java src/share/classes/sun/java2d/SunGraphics2D.java test/java/awt/image/MultiResolutionImageTest.java
diffstat 8 files changed, 1242 insertions(+), 136 deletions(-) [+]
line wrap: on
line diff
--- a/src/macosx/classes/sun/lwawt/macosx/LWCToolkit.java	Thu Apr 20 16:51:31 2017 +0100
+++ b/src/macosx/classes/sun/lwawt/macosx/LWCToolkit.java	Thu Apr 20 20:28:44 2017 +0100
@@ -35,14 +35,17 @@
 import java.awt.im.InputMethodHighlight;
 import java.awt.peer.*;
 import java.lang.reflect.*;
+import java.net.URL;
 import java.security.*;
 import java.util.*;
 import java.util.concurrent.Callable;
+import java.net.MalformedURLException;
 
 import sun.awt.*;
 import sun.lwawt.*;
 import sun.lwawt.LWWindowPeer.PeerType;
 import sun.security.action.GetBooleanAction;
+import sun.awt.image.MultiResolutionImage;
 
 import sun.util.CoreResourceBundleControl;
 
@@ -470,9 +473,30 @@
     @Override
     public Image getImage(final String filename) {
         final Image nsImage = checkForNSImage(filename);
-        if (nsImage != null) return nsImage;
+        if (nsImage != null) {
+            return nsImage;
+        }
+
+        if (imageCached(filename)) {
+            return super.getImage(filename);
+        }
 
-        return super.getImage(filename);
+        String fileneame2x = getScaledImageName(filename);
+        return (imageExists(fileneame2x))
+                ? getImageWithResolutionVariant(filename, fileneame2x)
+                : super.getImage(filename);
+    }
+
+    @Override
+    public Image getImage(URL url) {
+
+        if (imageCached(url)) {
+            return super.getImage(url);
+        }
+
+        URL url2x = getScaledImageURL(url);
+        return (imageExists(url2x))
+                ? getImageWithResolutionVariant(url, url2x) : super.getImage(url);
     }
 
     static final String nsImagePrefix = "NSImage://";
@@ -763,6 +787,38 @@
         return true;
     }
 
+    private static URL getScaledImageURL(URL url) {
+        try {
+            String scaledImagePath = getScaledImageName(url.getPath());
+            return scaledImagePath == null ? null : new URL(url.getProtocol(),
+                    url.getHost(), url.getPort(), scaledImagePath);
+        } catch (MalformedURLException e) {
+            return null;
+        }
+    }
+
+    private static String getScaledImageName(String path) {
+        if (!isValidPath(path)) {
+            return null;
+        }
+
+        int slash = path.lastIndexOf('/');
+        String name = (slash < 0) ? path : path.substring(slash + 1);
+
+        if (name.contains("@2x")) {
+            return null;
+        }
+
+        int dot = name.lastIndexOf('.');
+        String name2x = (dot < 0) ? name + "@2x"
+                : name.substring(0, dot) + "@2x" + name.substring(dot);
+        return (slash < 0) ? name2x : path.substring(0, slash + 1) + name2x;
+    }
+
+    private static boolean isValidPath(String path) {
+        return !path.isEmpty() && !path.endsWith("/") && !path.endsWith(".");
+    }
+
     @Override
     public boolean enableInputMethodsForTextComponent() {
         return true;
--- a/src/share/classes/java/awt/MediaTracker.java	Thu Apr 20 16:51:31 2017 +0100
+++ b/src/share/classes/java/awt/MediaTracker.java	Thu Apr 20 20:28:44 2017 +0100
@@ -28,6 +28,7 @@
 import java.awt.Component;
 import java.awt.Image;
 import java.awt.image.ImageObserver;
+import sun.awt.image.MultiResolutionToolkitImage;
 
 /**
  * The <code>MediaTracker</code> class is a utility class to track
@@ -222,10 +223,17 @@
      * @param     h    the height at which the image is rendered
      */
     public synchronized void addImage(Image image, int id, int w, int h) {
+        addImageImpl(image, id, w, h);
+        Image rvImage = getResolutionVariant(image);
+        if (rvImage != null) {
+            addImageImpl(rvImage, id, 2 * w, 2 * h);
+        }
+    }
+
+    private void addImageImpl(Image image, int id, int w, int h) {
         head = MediaEntry.insert(head,
                                  new ImageMediaEntry(this, image, id, w, h));
     }
-
     /**
      * Flag indicating that media is currently being loaded.
      * @see         java.awt.MediaTracker#statusAll
@@ -719,6 +727,15 @@
      * @since   JDK1.1
      */
     public synchronized void removeImage(Image image) {
+        removeImageImpl(image);
+        Image rvImage = getResolutionVariant(image);
+        if (rvImage != null) {
+            removeImageImpl(rvImage);
+        }
+        notifyAll();    // Notify in case remaining images are "done".
+    }
+
+    private void removeImageImpl(Image image) {
         MediaEntry cur = head;
         MediaEntry prev = null;
         while (cur != null) {
@@ -735,7 +752,6 @@
             }
             cur = next;
         }
-        notifyAll();    // Notify in case remaining images are "done".
     }
 
     /**
@@ -750,6 +766,15 @@
      * @since      JDK1.1
      */
     public synchronized void removeImage(Image image, int id) {
+        removeImageImpl(image, id);
+        Image rvImage = getResolutionVariant(image);
+        if (rvImage != null) {
+            removeImageImpl(rvImage, id);
+        }
+        notifyAll();    // Notify in case remaining images are "done".
+    }
+
+    private void removeImageImpl(Image image, int id) {
         MediaEntry cur = head;
         MediaEntry prev = null;
         while (cur != null) {
@@ -766,7 +791,6 @@
             }
             cur = next;
         }
-        notifyAll();    // Notify in case remaining images are "done".
     }
 
     /**
@@ -783,6 +807,16 @@
      */
     public synchronized void removeImage(Image image, int id,
                                          int width, int height) {
+        removeImageImpl(image, id, width, height);
+        Image rvImage = getResolutionVariant(image);
+        if (rvImage != null) {
+            removeImageImpl(rvImage, id, 2 * width, 2 * height);
+
+        }
+        notifyAll();    // Notify in case remaining images are "done".
+    }
+
+    private void removeImageImpl(Image image, int id, int width, int height) {
         MediaEntry cur = head;
         MediaEntry prev = null;
         while (cur != null) {
@@ -801,12 +835,18 @@
             }
             cur = next;
         }
-        notifyAll();    // Notify in case remaining images are "done".
     }
 
     synchronized void setDone() {
         notifyAll();
     }
+
+    private static Image getResolutionVariant(Image image) {
+        if (image instanceof MultiResolutionToolkitImage) {
+            return ((MultiResolutionToolkitImage) image).getResolutionVariant();
+        }
+        return null;
+    }
 }
 
 abstract class MediaEntry {
--- a/src/share/classes/sun/awt/SunHints.java	Thu Apr 20 16:51:31 2017 +0100
+++ b/src/share/classes/sun/awt/SunHints.java	Thu Apr 20 20:28:44 2017 +0100
@@ -171,7 +171,7 @@
         }
     }
 
-    private static final int NUM_KEYS = 9;
+    private static final int NUM_KEYS = 10;
     private static final int VALS_PER_KEY = 8;
 
     /**
@@ -252,6 +252,13 @@
     public static final int INTVAL_STROKE_PURE = 2;
 
     /**
+     * Image scaling hint key and values
+     */
+    public static final int INTKEY_RESOLUTION_VARIANT = 9;
+    public static final int INTVAL_RESOLUTION_VARIANT_DEFAULT = 0;
+    public static final int INTVAL_RESOLUTION_VARIANT_OFF = 1;
+    public static final int INTVAL_RESOLUTION_VARIANT_ON = 2;
+    /**
      * LCD text contrast control hint key.
      * Value is "100" to make discontiguous with the others which
      * are all enumerative and are of a different class.
@@ -449,6 +456,24 @@
                            SunHints.INTVAL_STROKE_PURE,
                            "Pure stroke conversion for accurate paths");
 
+    /**
+     * Image resolution variant hint key and value objects
+     */
+    public static final Key KEY_RESOLUTION_VARIANT =
+        new SunHints.Key(SunHints.INTKEY_RESOLUTION_VARIANT,
+                         "Global image resolution variant key");
+    public static final Object VALUE_RESOLUTION_VARIANT_DEFAULT =
+        new SunHints.Value(KEY_RESOLUTION_VARIANT,
+                           SunHints.INTVAL_RESOLUTION_VARIANT_DEFAULT,
+                           "Choose image resolutions based on a default heuristic");
+    public static final Object VALUE_RESOLUTION_VARIANT_OFF =
+        new SunHints.Value(KEY_RESOLUTION_VARIANT,
+                           SunHints.INTVAL_RESOLUTION_VARIANT_OFF,
+                           "Use only the standard resolution of an image");
+    public static final Object VALUE_RESOLUTION_VARIANT_ON =
+        new SunHints.Value(KEY_RESOLUTION_VARIANT,
+                           SunHints.INTVAL_RESOLUTION_VARIANT_ON,
+                           "Always use resolution-specific variants of images");
 
     public static class LCDContrastKey extends Key {
 
--- a/src/share/classes/sun/awt/SunToolkit.java	Thu Apr 20 16:51:31 2017 +0100
+++ b/src/share/classes/sun/awt/SunToolkit.java	Thu Apr 20 20:28:44 2017 +0100
@@ -36,6 +36,9 @@
 import java.awt.TrayIcon;
 import java.awt.SystemTray;
 import java.awt.event.InputEvent;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
 import java.net.URL;
 import java.security.PrivilegedAction;
 import java.util.*;
@@ -725,6 +728,194 @@
     static final SoftCache imgCache = new SoftCache();
 
     static Image getImageFromHash(Toolkit tk, URL url) {
+        checkPermissions(url);
+        synchronized (imgCache) {
+            Image img = (Image)imgCache.get(url);
+            if (img == null) {
+                try {
+                    img = tk.createImage(new URLImageSource(url));
+                    imgCache.put(url, img);
+                } catch (Exception e) {
+                }
+            }
+            return img;
+        }
+    }
+
+    static Image getImageFromHash(Toolkit tk,
+                                               String filename) {
+        checkPermissions(filename);
+        synchronized (imgCache) {
+            Image img = (Image)imgCache.get(filename);
+            if (img == null) {
+                try {
+                    img = tk.createImage(new FileImageSource(filename));
+                    imgCache.put(filename, img);
+                } catch (Exception e) {
+                }
+            }
+            return img;
+        }
+    }
+
+    public Image getImage(String filename) {
+        return getImageFromHash(this, filename);
+    }
+
+    public Image getImage(URL url) {
+        return getImageFromHash(this, url);
+    }
+
+    protected Image getImageWithResolutionVariant(String fileName,
+            String resolutionVariantName) {
+        synchronized (imgCache) {
+            Image image = getImageFromHash(this, fileName);
+            if (image instanceof MultiResolutionImage) {
+                return image;
+            }
+            Image resolutionVariant = getImageFromHash(this, resolutionVariantName);
+            image = createImageWithResolutionVariant(image, resolutionVariant);
+            imgCache.put(fileName, image);
+            return image;
+        }
+    }
+
+    protected Image getImageWithResolutionVariant(URL url,
+            URL resolutionVariantURL) {
+        synchronized (imgCache) {
+            Image image = getImageFromHash(this, url);
+            if (image instanceof MultiResolutionImage) {
+                return image;
+            }
+            Image resolutionVariant = getImageFromHash(this, resolutionVariantURL);
+            image = createImageWithResolutionVariant(image, resolutionVariant);
+            imgCache.put(url, image);
+            return image;
+        }
+    }
+
+
+    public Image createImage(String filename) {
+        checkPermissions(filename);
+        return createImage(new FileImageSource(filename));
+    }
+
+    public Image createImage(URL url) {
+        checkPermissions(url);
+        return createImage(new URLImageSource(url));
+    }
+
+    public Image createImage(byte[] data, int offset, int length) {
+        return createImage(new ByteArrayImageSource(data, offset, length));
+    }
+
+    public Image createImage(ImageProducer producer) {
+        return new ToolkitImage(producer);
+    }
+
+    public static Image createImageWithResolutionVariant(Image image,
+            Image resolutionVariant) {
+        return new MultiResolutionToolkitImage(image, resolutionVariant);
+    }
+
+    public int checkImage(Image img, int w, int h, ImageObserver o) {
+        if (!(img instanceof ToolkitImage)) {
+            return ImageObserver.ALLBITS;
+        }
+
+        ToolkitImage tkimg = (ToolkitImage)img;
+        int repbits;
+        if (w == 0 || h == 0) {
+            repbits = ImageObserver.ALLBITS;
+        } else {
+            repbits = tkimg.getImageRep().check(o);
+        }
+        return (tkimg.check(o) | repbits) & checkResolutionVariant(img, w, h, o);
+    }
+
+    public boolean prepareImage(Image img, int w, int h, ImageObserver o) {
+        if (w == 0 || h == 0) {
+            return true;
+        }
+
+        // Must be a ToolkitImage
+        if (!(img instanceof ToolkitImage)) {
+            return true;
+        }
+
+        ToolkitImage tkimg = (ToolkitImage)img;
+        if (tkimg.hasError()) {
+            if (o != null) {
+                o.imageUpdate(img, ImageObserver.ERROR|ImageObserver.ABORT,
+                              -1, -1, -1, -1);
+            }
+            return false;
+        }
+        ImageRepresentation ir = tkimg.getImageRep();
+        return ir.prepare(o) & prepareResolutionVariant(img, w, h, o);
+    }
+
+    private int checkResolutionVariant(Image img, int w, int h, ImageObserver o) {
+        ToolkitImage rvImage = getResolutionVariant(img);
+        // Ignore the resolution variant in case of error
+        return (rvImage == null || rvImage.hasError()) ? 0xFFFF :
+                checkImage(rvImage, 2 * w, 2 * h, MultiResolutionToolkitImage.
+                                getResolutionVariantObserver(
+                                        img, o, w, h, 2 * w, 2 * h));
+    }
+
+    private boolean prepareResolutionVariant(Image img, int w, int h,
+            ImageObserver o) {
+
+        ToolkitImage rvImage = getResolutionVariant(img);
+        // Ignore the resolution variant in case of error
+        return rvImage == null || rvImage.hasError() || prepareImage(
+                rvImage, 2 * w, 2 * h,
+                MultiResolutionToolkitImage.getResolutionVariantObserver(
+                        img, o, w, h, 2 * w, 2 * h));
+    }
+
+    private static ToolkitImage getResolutionVariant(Image image) {
+        if (image instanceof MultiResolutionToolkitImage) {
+            Image resolutionVariant = ((MultiResolutionToolkitImage) image).
+                    getResolutionVariant();
+            if (resolutionVariant instanceof ToolkitImage) {
+                return (ToolkitImage) resolutionVariant;
+            }
+        }
+        return null;
+    }
+
+    protected static boolean imageCached(Object key) {
+        return imgCache.containsKey(key);
+    }
+
+    protected static boolean imageExists(String filename) {
+        checkPermissions(filename);
+        return filename != null && new File(filename).exists();
+    }
+
+    @SuppressWarnings("try")
+    protected static boolean imageExists(URL url) {
+        checkPermissions(url);
+        if (url != null) {
+            try (InputStream is = url.openStream()) {
+                return true;
+            }catch(IOException e){
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private static void checkPermissions(String filename) {
+        SecurityManager security = System.getSecurityManager();
+        if (security != null) {
+            security.checkRead(filename);
+        }
+    }
+
+    private static void checkPermissions(URL url) {
         SecurityManager sm = System.getSecurityManager();
         if (sm != null) {
             try {
@@ -752,128 +943,6 @@
                     sm.checkConnect(url.getHost(), url.getPort());
             }
         }
-        synchronized (imgCache) {
-            Image img = (Image)imgCache.get(url);
-            if (img == null) {
-                try {
-                    img = tk.createImage(new URLImageSource(url));
-                    imgCache.put(url, img);
-                } catch (Exception e) {
-                }
-            }
-            return img;
-        }
-    }
-
-    static Image getImageFromHash(Toolkit tk,
-                                               String filename) {
-        SecurityManager security = System.getSecurityManager();
-        if (security != null) {
-            security.checkRead(filename);
-        }
-        synchronized (imgCache) {
-            Image img = (Image)imgCache.get(filename);
-            if (img == null) {
-                try {
-                    img = tk.createImage(new FileImageSource(filename));
-                    imgCache.put(filename, img);
-                } catch (Exception e) {
-                }
-            }
-            return img;
-        }
-    }
-
-    public Image getImage(String filename) {
-        return getImageFromHash(this, filename);
-    }
-
-    public Image getImage(URL url) {
-        return getImageFromHash(this, url);
-    }
-
-    public Image createImage(String filename) {
-        SecurityManager security = System.getSecurityManager();
-        if (security != null) {
-            security.checkRead(filename);
-        }
-        return createImage(new FileImageSource(filename));
-    }
-
-    public Image createImage(URL url) {
-        SecurityManager sm = System.getSecurityManager();
-        if (sm != null) {
-            try {
-                java.security.Permission perm =
-                    url.openConnection().getPermission();
-                if (perm != null) {
-                    try {
-                        sm.checkPermission(perm);
-                    } catch (SecurityException se) {
-                        // fallback to checkRead/checkConnect for pre 1.2
-                        // security managers
-                        if ((perm instanceof java.io.FilePermission) &&
-                            perm.getActions().indexOf("read") != -1) {
-                            sm.checkRead(perm.getName());
-                        } else if ((perm instanceof
-                            java.net.SocketPermission) &&
-                            perm.getActions().indexOf("connect") != -1) {
-                            sm.checkConnect(url.getHost(), url.getPort());
-                        } else {
-                            throw se;
-                        }
-                    }
-                }
-            } catch (java.io.IOException ioe) {
-                    sm.checkConnect(url.getHost(), url.getPort());
-            }
-        }
-        return createImage(new URLImageSource(url));
-    }
-
-    public Image createImage(byte[] data, int offset, int length) {
-        return createImage(new ByteArrayImageSource(data, offset, length));
-    }
-
-    public Image createImage(ImageProducer producer) {
-        return new ToolkitImage(producer);
-    }
-
-    public int checkImage(Image img, int w, int h, ImageObserver o) {
-        if (!(img instanceof ToolkitImage)) {
-            return ImageObserver.ALLBITS;
-        }
-
-        ToolkitImage tkimg = (ToolkitImage)img;
-        int repbits;
-        if (w == 0 || h == 0) {
-            repbits = ImageObserver.ALLBITS;
-        } else {
-            repbits = tkimg.getImageRep().check(o);
-        }
-        return tkimg.check(o) | repbits;
-    }
-
-    public boolean prepareImage(Image img, int w, int h, ImageObserver o) {
-        if (w == 0 || h == 0) {
-            return true;
-        }
-
-        // Must be a ToolkitImage
-        if (!(img instanceof ToolkitImage)) {
-            return true;
-        }
-
-        ToolkitImage tkimg = (ToolkitImage)img;
-        if (tkimg.hasError()) {
-            if (o != null) {
-                o.imageUpdate(img, ImageObserver.ERROR|ImageObserver.ABORT,
-                              -1, -1, -1, -1);
-            }
-            return false;
-        }
-        ImageRepresentation ir = tkimg.getImageRep();
-        return ir.prepare(o);
     }
 
     /**
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/share/classes/sun/awt/image/MultiResolutionImage.java	Thu Apr 20 20:28:44 2017 +0100
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package sun.awt.image;
+
+import java.awt.Image;
+import java.util.List;
+
+/**
+ * This interface is designed to provide a set of images at various resolutions.
+ *
+ * The <code>MultiResolutionImage</code> interface should be implemented by any
+ * class whose instances are intended to provide image resolution variants
+ * according to the given image width and height.
+ *
+ * For example,
+ * <pre>
+ * {@code
+ *  public class ScaledImage extends BufferedImage
+ *         implements MultiResolutionImage {
+ *
+ *    @Override
+ *    public Image getResolutionVariant(int width, int height) {
+ *      return ((width <= getWidth() && height <= getHeight()))
+ *             ? this : highResolutionImage;
+ *    }
+ *
+ *    @Override
+ *    public List<Image> getResolutionVariants() {
+ *        return Arrays.asList(this, highResolutionImage);
+ *    }
+ *  }
+ * }</pre>
+ *
+ * It is recommended to cache image variants for performance reasons.
+ *
+ * <b>WARNING</b>: This class is an implementation detail. This API may change
+ * between update release, and it may even be removed or be moved in some other
+ * package(s)/class(es).
+ */
+public interface MultiResolutionImage {
+
+    /**
+     * Provides an image with necessary resolution which best fits to the given
+     * image width and height.
+     *
+     * @param width the desired image resolution width.
+     * @param height the desired image resolution height.
+     * @return image resolution variant.
+     *
+     * @since JDK1.8
+     */
+    public Image getResolutionVariant(int width, int height);
+
+    /**
+     * Gets list of all resolution variants including the base image
+     *
+     * @return list of resolution variants.
+     * @since JDK1.8
+     */
+    public List<Image> getResolutionVariants();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/share/classes/sun/awt/image/MultiResolutionToolkitImage.java	Thu Apr 20 20:28:44 2017 +0100
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package sun.awt.image;
+
+import java.awt.Image;
+import java.awt.image.ImageObserver;
+import java.util.Arrays;
+import java.util.List;
+import sun.misc.SoftCache;
+
+public class MultiResolutionToolkitImage extends ToolkitImage implements MultiResolutionImage {
+
+    Image resolutionVariant;
+
+    public MultiResolutionToolkitImage(Image lowResolutionImage, Image resolutionVariant) {
+        super(lowResolutionImage.getSource());
+        this.resolutionVariant = resolutionVariant;
+    }
+
+    @Override
+    public Image getResolutionVariant(int width, int height) {
+        return ((width <= getWidth() && height <= getHeight()))
+                ? this : resolutionVariant;
+    }
+
+    public Image getResolutionVariant() {
+        return resolutionVariant;
+    }
+
+    @Override
+    public List<Image> getResolutionVariants() {
+        return Arrays.<Image>asList(this, resolutionVariant);
+    }
+
+    private static final int BITS_INFO = ImageObserver.SOMEBITS
+            | ImageObserver.FRAMEBITS | ImageObserver.ALLBITS;
+
+    private static class ObserverCache {
+
+        static final SoftCache INSTANCE = new SoftCache();
+    }
+
+    public static ImageObserver getResolutionVariantObserver(
+            final Image image, final ImageObserver observer,
+            final int imgWidth, final int imgHeight,
+            final int rvWidth, final int rvHeight) {
+
+        if (observer == null) {
+            return null;
+        }
+
+        synchronized (ObserverCache.INSTANCE) {
+            ImageObserver o = (ImageObserver) ObserverCache.INSTANCE.get(image);
+
+            if (o == null) {
+
+                o = new ImageObserver() {
+                    @Override
+                    public boolean imageUpdate(Image resolutionVariant, int flags,
+                                               int x, int y, int width, int height) {
+
+                            if ((flags & (ImageObserver.WIDTH | BITS_INFO)) != 0) {
+                                width = (width + 1) / 2;
+                            }
+
+                            if ((flags & (ImageObserver.HEIGHT | BITS_INFO)) != 0) {
+                                height = (height + 1) / 2;
+                            }
+
+                            if ((flags & BITS_INFO) != 0) {
+                                x /= 2;
+                                y /= 2;
+                            }
+
+                            return observer.imageUpdate(
+                                    image, flags, x, y, width, height);
+                    }
+                };
+
+                ObserverCache.INSTANCE.put(image, o);
+            }
+            return o;
+        }
+    }
+}
--- a/src/share/classes/sun/java2d/SunGraphics2D.java	Thu Apr 20 16:51:31 2017 +0100
+++ b/src/share/classes/sun/java2d/SunGraphics2D.java	Thu Apr 20 20:28:44 2017 +0100
@@ -61,6 +61,7 @@
 import java.awt.Rectangle;
 import java.text.AttributedCharacterIterator;
 import java.awt.Font;
+import java.awt.Point;
 import java.awt.image.ImageObserver;
 import java.awt.Transparency;
 import java.awt.font.GlyphVector;
@@ -92,6 +93,14 @@
 import java.util.Iterator;
 import sun.misc.PerformanceLogger;
 
+import sun.awt.image.MultiResolutionImage;
+
+import static java.awt.geom.AffineTransform.TYPE_FLIP;
+import static java.awt.geom.AffineTransform.TYPE_MASK_SCALE;
+import static java.awt.geom.AffineTransform.TYPE_TRANSLATION;
+import sun.awt.image.MultiResolutionToolkitImage;
+import sun.awt.image.ToolkitImage;
+
 /**
  * This is a the master Graphics2D superclass for all of the Sun
  * Graphics implementations.  This class relies on subclasses to
@@ -212,6 +221,7 @@
     protected Region devClip;           // Actual physical drawable in pixels
 
     private final int devScale;         // Actual physical scale factor
+    private int resolutionVariantHint;
 
     // cached state for text rendering
     private boolean validFontInfo;
@@ -249,6 +259,7 @@
         lcdTextContrast = lcdTextContrastDefaultValue;
         interpolationHint = -1;
         strokeHint = SunHints.INTVAL_STROKE_DEFAULT;
+        resolutionVariantHint = SunHints.INTVAL_RESOLUTION_VARIANT_DEFAULT;
 
         interpolationType = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
 
@@ -1224,6 +1235,10 @@
                 stateChanged = (strokeHint != newHint);
                 strokeHint = newHint;
                 break;
+            case SunHints.INTKEY_RESOLUTION_VARIANT:
+                stateChanged = (resolutionVariantHint != newHint);
+                resolutionVariantHint = newHint;
+                break;
             default:
                 recognized = false;
                 stateChanged = false;
@@ -1297,6 +1312,9 @@
         case SunHints.INTKEY_STROKE_CONTROL:
             return SunHints.Value.get(SunHints.INTKEY_STROKE_CONTROL,
                                       strokeHint);
+        case SunHints.INTKEY_RESOLUTION_VARIANT:
+            return SunHints.Value.get(SunHints.INTKEY_RESOLUTION_VARIANT,
+                                      resolutionVariantHint);
         }
         return null;
     }
@@ -3027,18 +3045,58 @@
     }
 // end of text rendering methods
 
-    private static boolean isHiDPIImage(final Image img) {
-        return SurfaceManager.getImageScale(img) != 1;
+    private boolean isHiDPIImage(final Image img) {
+        return (SurfaceManager.getImageScale(img) != 1) ||
+               (resolutionVariantHint != SunHints.INTVAL_RESOLUTION_VARIANT_OFF
+                    && img instanceof MultiResolutionImage);
     }
 
     private boolean drawHiDPIImage(Image img, int dx1, int dy1, int dx2,
                                    int dy2, int sx1, int sy1, int sx2, int sy2,
                                    Color bgcolor, ImageObserver observer) {
-        final int scale = SurfaceManager.getImageScale(img);
-        sx1 = Region.clipScale(sx1, scale);
-        sx2 = Region.clipScale(sx2, scale);
-        sy1 = Region.clipScale(sy1, scale);
-        sy2 = Region.clipScale(sy2, scale);
+
+        if (SurfaceManager.getImageScale(img) != 1) {  // Volatile Image
+            final int scale = SurfaceManager.getImageScale(img);
+            sx1 = Region.clipScale(sx1, scale);
+            sx2 = Region.clipScale(sx2, scale);
+            sy1 = Region.clipScale(sy1, scale);
+            sy2 = Region.clipScale(sy2, scale);
+        } else if (img instanceof MultiResolutionImage) {
+            // get scaled destination image size
+
+            int width = img.getWidth(observer);
+            int height = img.getHeight(observer);
+
+            Image resolutionVariant = getResolutionVariant(
+                    (MultiResolutionImage) img, width, height,
+                    dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2);
+
+            if (resolutionVariant != img && resolutionVariant != null) {
+                // recalculate source region for the resolution variant
+
+                ImageObserver rvObserver = MultiResolutionToolkitImage.
+                        getResolutionVariantObserver(img, observer,
+                                width, height, -1, -1);
+
+                int rvWidth = resolutionVariant.getWidth(rvObserver);
+                int rvHeight = resolutionVariant.getHeight(rvObserver);
+
+                if (0 < width && 0 < height && 0 < rvWidth && 0 < rvHeight) {
+
+                    float widthScale = ((float) rvWidth) / width;
+                    float heightScale = ((float) rvHeight) / height;
+
+                    sx1 = Region.clipScale(sx1, widthScale);
+                    sy1 = Region.clipScale(sy1, heightScale);
+                    sx2 = Region.clipScale(sx2, widthScale);
+                    sy2 = Region.clipScale(sy2, heightScale);
+
+                    observer = rvObserver;
+                    img = resolutionVariant;
+                }
+            }
+        }
+
         try {
             return imagepipe.scaleImage(this, img, dx1, dy1, dx2, dy2, sx1, sy1,
                                         sx2, sy2, bgcolor, observer);
@@ -3058,6 +3116,54 @@
         }
     }
 
+    private Image getResolutionVariant(MultiResolutionImage img,
+            int srcWidth, int srcHeight, int dx1, int dy1, int dx2, int dy2,
+            int sx1, int sy1, int sx2, int sy2) {
+
+        if (srcWidth <= 0 || srcHeight <= 0) {
+            return null;
+        }
+
+        int sw = sx2 - sx1;
+        int sh = sy2 - sy1;
+
+        if (sw == 0 || sh == 0) {
+            return null;
+        }
+
+        int type = transform.getType();
+        int dw = dx2 - dx1;
+        int dh = dy2 - dy1;
+        double destRegionWidth;
+        double destRegionHeight;
+
+        if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP)) == 0) {
+            destRegionWidth = dw;
+            destRegionHeight = dh;
+        } else if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP | TYPE_MASK_SCALE)) == 0) {
+            destRegionWidth = dw * transform.getScaleX();
+            destRegionHeight = dh * transform.getScaleY();
+        } else {
+            destRegionWidth = dw * Math.hypot(
+                    transform.getScaleX(), transform.getShearY());
+            destRegionHeight = dh * Math.hypot(
+                    transform.getShearX(), transform.getScaleY());
+        }
+
+        int destImageWidth = (int) Math.abs(srcWidth * destRegionWidth / sw);
+        int destImageHeight = (int) Math.abs(srcHeight * destRegionHeight / sh);
+
+        Image resolutionVariant
+                = img.getResolutionVariant(destImageWidth, destImageHeight);
+
+        if (resolutionVariant instanceof ToolkitImage
+                && ((ToolkitImage) resolutionVariant).hasError()) {
+            return null;
+        }
+
+        return resolutionVariant;
+    }
+
     /**
      * Draws an image scaled to x,y,w,h in nonblocking mode with a
      * callback object.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/java/awt/image/MultiResolutionImageTest.java	Thu Apr 20 20:28:44 2017 +0100
@@ -0,0 +1,620 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URL;
+import javax.imageio.ImageIO;
+import sun.awt.OSInfo;
+import sun.awt.SunHints;
+import java.awt.MediaTracker;
+import java.awt.geom.AffineTransform;
+import java.awt.image.ImageObserver;
+import java.util.Arrays;
+import java.util.List;
+import javax.swing.JPanel;
+import sun.awt.SunToolkit;
+import sun.awt.image.MultiResolutionImage;
+
+/**
+ * @test
+ * @bug 8011059
+ * @author Alexander Scherbatiy
+ * @summary [macosx] Make JDK demos look perfect on retina displays
+ * @run main MultiResolutionImageTest CUSTOM
+ * @run main MultiResolutionImageTest TOOLKIT_PREPARE
+ * @run main MultiResolutionImageTest TOOLKIT_LOAD
+ * @run main MultiResolutionImageTest TOOLKIT
+ */
+public class MultiResolutionImageTest {
+
+    private static final int IMAGE_WIDTH = 300;
+    private static final int IMAGE_HEIGHT = 200;
+    private static final Color COLOR_1X = Color.GREEN;
+    private static final Color COLOR_2X = Color.BLUE;
+    private static final String IMAGE_NAME_1X = "image.png";
+    private static final String IMAGE_NAME_2X = "image@2x.png";
+
+    public static void main(String[] args) throws Exception {
+
+        System.out.println("args: " + args.length);
+
+        if (args.length == 0) {
+            throw new RuntimeException("Not found a test");
+        }
+
+        String test = args[0];
+
+        System.out.println("TEST: " + test);
+        System.out.println("CHECK OS: " + checkOS());
+
+        if ("CUSTOM".equals(test)) {
+            testCustomMultiResolutionImage();
+        } else if (checkOS()) {
+            switch (test) {
+                case "CUSTOM":
+                    break;
+                case "TOOLKIT_PREPARE":
+                    testToolkitMultiResolutionImagePrepare();
+                    break;
+                case "TOOLKIT_LOAD":
+                    testToolkitMultiResolutionImageLoad();
+                    break;
+                case "TOOLKIT":
+                    testToolkitMultiResolutionImage();
+                    testImageNameTo2xParsing();
+                    break;
+                default:
+                    throw new RuntimeException("Unknown test: " + test);
+            }
+        }
+    }
+
+    static boolean checkOS() {
+        return OSInfo.getOSType() == OSInfo.OSType.MACOSX;
+    }
+
+    public static void testCustomMultiResolutionImage() {
+        testCustomMultiResolutionImage(false);
+        testCustomMultiResolutionImage(true);
+    }
+
+    public static void testCustomMultiResolutionImage(boolean enableImageScaling) {
+
+        Image image = new MultiResolutionBufferedImage();
+
+        // Same image size
+        BufferedImage bufferedImage = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT,
+                BufferedImage.TYPE_INT_RGB);
+        Graphics2D g2d = (Graphics2D) bufferedImage.getGraphics();
+        setImageScalingHint(g2d, enableImageScaling);
+        g2d.drawImage(image, 0, 0, null);
+        checkColor(bufferedImage.getRGB(3 * IMAGE_WIDTH / 4, 3 * IMAGE_HEIGHT / 4), false);
+
+        // Twice image size
+        bufferedImage = new BufferedImage(2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT,
+                BufferedImage.TYPE_INT_RGB);
+        g2d = (Graphics2D) bufferedImage.getGraphics();
+        setImageScalingHint(g2d, enableImageScaling);
+        g2d.drawImage(image, 0, 0, 2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, null);
+        checkColor(bufferedImage.getRGB(3 * IMAGE_WIDTH / 2, 3 * IMAGE_HEIGHT / 2), enableImageScaling);
+
+        // Scale 2x
+        bufferedImage = new BufferedImage(2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
+        g2d = (Graphics2D) bufferedImage.getGraphics();
+        setImageScalingHint(g2d, enableImageScaling);
+        g2d.scale(2, 2);
+        g2d.drawImage(image, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, null);
+        checkColor(bufferedImage.getRGB(3 * IMAGE_WIDTH / 2, 3 * IMAGE_HEIGHT / 2), enableImageScaling);
+
+        // Rotate
+        bufferedImage = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT,
+                BufferedImage.TYPE_INT_RGB);
+        g2d = (Graphics2D) bufferedImage.getGraphics();
+        setImageScalingHint(g2d, enableImageScaling);
+        g2d.drawImage(image, 0, 0, null);
+        g2d.rotate(Math.PI / 4);
+        checkColor(bufferedImage.getRGB(3 * IMAGE_WIDTH / 4, 3 * IMAGE_HEIGHT / 4), false);
+
+        // Scale 2x and Rotate
+        bufferedImage = new BufferedImage(2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
+        g2d = (Graphics2D) bufferedImage.getGraphics();
+        setImageScalingHint(g2d, enableImageScaling);
+        g2d.scale(-2, 2);
+        g2d.rotate(-Math.PI / 10);
+        g2d.drawImage(image, -IMAGE_WIDTH, 0, IMAGE_WIDTH, IMAGE_HEIGHT, null);
+        checkColor(bufferedImage.getRGB(3 * IMAGE_WIDTH / 2, 3 * IMAGE_HEIGHT / 2), enableImageScaling);
+
+        // General Transform
+        bufferedImage = new BufferedImage(2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
+        g2d = (Graphics2D) bufferedImage.getGraphics();
+        setImageScalingHint(g2d, enableImageScaling);
+        float delta = 0.05f;
+        float cos = 1 - delta * delta / 2;
+        float sin = 1 + delta;
+        AffineTransform transform = new AffineTransform(2 * cos, 0.1, 0.3, -2 * sin, 10, -5);
+        g2d.setTransform(transform);
+        g2d.drawImage(image, 0, -IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_HEIGHT, null);
+        checkColor(bufferedImage.getRGB(3 * IMAGE_WIDTH / 2, 3 * IMAGE_HEIGHT / 2), enableImageScaling);
+
+        int D = 10;
+        // From Source to small Destination region
+        bufferedImage = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
+        g2d = (Graphics2D) bufferedImage.getGraphics();
+        setImageScalingHint(g2d, enableImageScaling);
+        g2d.drawImage(image, IMAGE_WIDTH / 2, IMAGE_HEIGHT / 2, IMAGE_WIDTH - D, IMAGE_HEIGHT - D,
+                D, D, IMAGE_WIDTH - D, IMAGE_HEIGHT - D, null);
+        checkColor(bufferedImage.getRGB(3 * IMAGE_WIDTH / 4, 3 * IMAGE_HEIGHT / 4), false);
+
+        // From Source to large Destination region
+        bufferedImage = new BufferedImage(2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
+        g2d = (Graphics2D) bufferedImage.getGraphics();
+        setImageScalingHint(g2d, enableImageScaling);
+        g2d.drawImage(image, D, D, 2 * IMAGE_WIDTH - D, 2 * IMAGE_HEIGHT - D,
+                IMAGE_WIDTH / 2, IMAGE_HEIGHT / 2, IMAGE_WIDTH - D, IMAGE_HEIGHT - D, null);
+        checkColor(bufferedImage.getRGB(3 * IMAGE_WIDTH / 2, 3 * IMAGE_HEIGHT / 2), enableImageScaling);
+    }
+
+    static class MultiResolutionBufferedImage extends BufferedImage
+            implements MultiResolutionImage {
+
+        Image highResolutionImage;
+
+        public MultiResolutionBufferedImage() {
+            super(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
+            highResolutionImage = new BufferedImage(
+                    2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
+            draw(getGraphics(), 1);
+            draw(highResolutionImage.getGraphics(), 2);
+        }
+
+        void draw(Graphics graphics, float resolution) {
+            Graphics2D g2 = (Graphics2D) graphics;
+            g2.scale(resolution, resolution);
+            g2.setColor((resolution == 1) ? COLOR_1X : COLOR_2X);
+            g2.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
+        }
+
+        @Override
+        public Image getResolutionVariant(int width, int height) {
+            return ((width <= getWidth() && height <= getHeight()))
+                    ? this : highResolutionImage;
+        }
+
+        @Override
+        public List<Image> getResolutionVariants() {
+            return Arrays.asList(this, highResolutionImage);
+        }
+    }
+
+    static void testToolkitMultiResolutionImagePrepare() throws Exception {
+
+        generateImages();
+
+        File imageFile = new File(IMAGE_NAME_1X);
+        String fileName = imageFile.getAbsolutePath();
+
+        Image image = Toolkit.getDefaultToolkit().getImage(fileName);
+
+        SunToolkit toolkit = (SunToolkit) Toolkit.getDefaultToolkit();
+        toolkit.prepareImage(image, IMAGE_WIDTH, IMAGE_HEIGHT, new LoadImageObserver(image));
+
+        testToolkitMultiResolutionImageLoad(image);
+    }
+
+    static void testToolkitMultiResolutionImageLoad() throws Exception {
+
+        generateImages();
+
+        File imageFile = new File(IMAGE_NAME_1X);
+        String fileName = imageFile.getAbsolutePath();
+        Image image = Toolkit.getDefaultToolkit().getImage(fileName);
+        testToolkitMultiResolutionImageLoad(image);
+    }
+
+    static void testToolkitMultiResolutionImageLoad(Image image) throws Exception {
+
+        MediaTracker tracker = new MediaTracker(new JPanel());
+        tracker.addImage(image, 0);
+        tracker.waitForID(0);
+        if (tracker.isErrorAny()) {
+            throw new RuntimeException("Error during image loading");
+        }
+        tracker.removeImage(image, 0);
+
+        testImageLoaded(image);
+
+        int w = image.getWidth(null);
+        int h = image.getHeight(null);
+
+        Image resolutionVariant = ((MultiResolutionImage) image)
+                .getResolutionVariant(2 * w, 2 * h);
+
+        if (image == resolutionVariant) {
+            throw new RuntimeException("Resolution variant is not loaded");
+        }
+
+        testImageLoaded(resolutionVariant);
+    }
+
+    static void testImageLoaded(Image image) {
+
+        SunToolkit toolkit = (SunToolkit) Toolkit.getDefaultToolkit();
+
+        int flags = toolkit.checkImage(image, IMAGE_WIDTH, IMAGE_WIDTH, new SilentImageObserver());
+        if ((flags & (ImageObserver.FRAMEBITS | ImageObserver.ALLBITS)) == 0) {
+            throw new RuntimeException("Image is not loaded!");
+        }
+    }
+
+    static class SilentImageObserver implements ImageObserver {
+
+        @Override
+        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
+            throw new RuntimeException("Observer should not be called!");
+        }
+    }
+
+    static class LoadImageObserver implements ImageObserver {
+
+        Image image;
+
+        public LoadImageObserver(Image image) {
+            this.image = image;
+        }
+
+        @Override
+        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
+
+            if (image != img) {
+                throw new RuntimeException("Original image is not passed to the observer");
+            }
+
+            if ((infoflags & ImageObserver.WIDTH) != 0) {
+                if (width != IMAGE_WIDTH) {
+                    throw new RuntimeException("Original width is not passed to the observer");
+                }
+            }
+
+            if ((infoflags & ImageObserver.HEIGHT) != 0) {
+                if (height != IMAGE_HEIGHT) {
+                    throw new RuntimeException("Original height is not passed to the observer");
+                }
+            }
+
+            return (infoflags & ALLBITS) == 0;
+        }
+
+    }
+
+    static void testToolkitMultiResolutionImage() throws Exception {
+
+        generateImages();
+
+        File imageFile = new File(IMAGE_NAME_1X);
+        String fileName = imageFile.getAbsolutePath();
+        URL url = imageFile.toURI().toURL();
+        testToolkitMultiResolutionImageChache(fileName, url);
+
+        Image image = Toolkit.getDefaultToolkit().getImage(fileName);
+        testToolkitImageObserver(image);
+        testToolkitMultiResolutionImage(image, false);
+        testToolkitMultiResolutionImage(image, true);
+
+        image = Toolkit.getDefaultToolkit().getImage(url);
+        testToolkitImageObserver(image);
+        testToolkitMultiResolutionImage(image, false);
+        testToolkitMultiResolutionImage(image, true);
+    }
+
+    static void testToolkitMultiResolutionImageChache(String fileName, URL url) {
+
+        Image img1 = Toolkit.getDefaultToolkit().getImage(fileName);
+        if (!(img1 instanceof MultiResolutionImage)) {
+            throw new RuntimeException("Not a MultiResolutionImage");
+        }
+
+        Image img2 = Toolkit.getDefaultToolkit().getImage(fileName);
+        if (img1 != img2) {
+            throw new RuntimeException("Image is not cached");
+        }
+
+        img1 = Toolkit.getDefaultToolkit().getImage(url);
+        if (!(img1 instanceof MultiResolutionImage)) {
+            throw new RuntimeException("Not a MultiResolutionImage");
+        }
+
+        img2 = Toolkit.getDefaultToolkit().getImage(url);
+        if (img1 != img2) {
+            throw new RuntimeException("Image is not cached");
+        }
+    }
+
+    static void testToolkitMultiResolutionImage(Image image, boolean enableImageScaling)
+            throws Exception {
+
+        MediaTracker tracker = new MediaTracker(new JPanel());
+        tracker.addImage(image, 0);
+        tracker.waitForID(0);
+        if (tracker.isErrorAny()) {
+            throw new RuntimeException("Error during image loading");
+        }
+
+        final BufferedImage bufferedImage1x = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT,
+                BufferedImage.TYPE_INT_RGB);
+        Graphics2D g1x = (Graphics2D) bufferedImage1x.getGraphics();
+        setImageScalingHint(g1x, false);
+        g1x.drawImage(image, 0, 0, null);
+        checkColor(bufferedImage1x.getRGB(3 * IMAGE_WIDTH / 4, 3 * IMAGE_HEIGHT / 4), false);
+
+        Image resolutionVariant = ((MultiResolutionImage) image).
+                getResolutionVariant(2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT);
+
+        if (resolutionVariant == null) {
+            throw new RuntimeException("Resolution variant is null");
+        }
+
+        MediaTracker tracker2x = new MediaTracker(new JPanel());
+        tracker2x.addImage(resolutionVariant, 0);
+        tracker2x.waitForID(0);
+        if (tracker2x.isErrorAny()) {
+            throw new RuntimeException("Error during scalable image loading");
+        }
+
+        final BufferedImage bufferedImage2x = new BufferedImage(2 * IMAGE_WIDTH,
+                2 * IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
+        Graphics2D g2x = (Graphics2D) bufferedImage2x.getGraphics();
+        setImageScalingHint(g2x, enableImageScaling);
+        g2x.drawImage(image, 0, 0, 2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, null);
+        checkColor(bufferedImage2x.getRGB(3 * IMAGE_WIDTH / 2, 3 * IMAGE_HEIGHT / 2), enableImageScaling);
+
+        if (!(image instanceof MultiResolutionImage)) {
+            throw new RuntimeException("Not a MultiResolutionImage");
+        }
+
+        MultiResolutionImage multiResolutionImage = (MultiResolutionImage) image;
+
+        Image image1x = multiResolutionImage.getResolutionVariant(IMAGE_WIDTH, IMAGE_HEIGHT);
+        Image image2x = multiResolutionImage.getResolutionVariant(2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT);
+
+        if (image1x.getWidth(null) * 2 != image2x.getWidth(null)
+                || image1x.getHeight(null) * 2 != image2x.getHeight(null)) {
+            throw new RuntimeException("Wrong resolution variant size");
+        }
+    }
+
+    static void testToolkitImageObserver(final Image image) {
+
+        ImageObserver observer = new ImageObserver() {
+
+            @Override
+            public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
+
+                if (img != image) {
+                    throw new RuntimeException("Wrong image in observer");
+                }
+
+                if ((infoflags & (ImageObserver.ERROR | ImageObserver.ABORT)) != 0) {
+                    throw new RuntimeException("Error during image loading");
+                }
+
+                return (infoflags & ImageObserver.ALLBITS) == 0;
+
+            }
+        };
+
+        final BufferedImage bufferedImage2x = new BufferedImage(2 * IMAGE_WIDTH,
+                2 * IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
+        Graphics2D g2x = (Graphics2D) bufferedImage2x.getGraphics();
+        setImageScalingHint(g2x, true);
+
+        g2x.drawImage(image, 0, 0, 2 * IMAGE_WIDTH, 2 * IMAGE_HEIGHT, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, observer);
+
+    }
+
+    static void setImageScalingHint(Graphics2D g2d, boolean enableImageScaling) {
+        g2d.setRenderingHint(SunHints.KEY_RESOLUTION_VARIANT, enableImageScaling
+                ? SunHints.VALUE_RESOLUTION_VARIANT_ON
+                : SunHints.VALUE_RESOLUTION_VARIANT_OFF);
+    }
+
+    static void checkColor(int rgb, boolean isImageScaled) {
+
+        if (!isImageScaled && COLOR_1X.getRGB() != rgb) {
+            throw new RuntimeException("Wrong 1x color: " + new Color(rgb));
+        }
+
+        if (isImageScaled && COLOR_2X.getRGB() != rgb) {
+            throw new RuntimeException("Wrong 2x color" + new Color(rgb));
+        }
+    }
+
+    static void generateImages() throws Exception {
+        if (!new File(IMAGE_NAME_1X).exists()) {
+            generateImage(1);
+        }
+
+        if (!new File(IMAGE_NAME_2X).exists()) {
+            generateImage(2);
+        }
+    }
+
+    static void generateImage(int scale) throws Exception {
+        BufferedImage image = new BufferedImage(scale * IMAGE_WIDTH, scale * IMAGE_HEIGHT,
+                BufferedImage.TYPE_INT_RGB);
+        Graphics g = image.getGraphics();
+        g.setColor(scale == 1 ? COLOR_1X : COLOR_2X);
+        g.fillRect(0, 0, scale * IMAGE_WIDTH, scale * IMAGE_HEIGHT);
+        File file = new File(scale == 1 ? IMAGE_NAME_1X : IMAGE_NAME_2X);
+        ImageIO.write(image, "png", file);
+    }
+
+    static void testImageNameTo2xParsing() throws Exception {
+
+        for (String[] testNames : TEST_FILE_NAMES) {
+            String testName = testNames[0];
+            String goldenName = testNames[1];
+            String resultName = getTestScaledImageName(testName);
+
+            if (!isValidPath(testName) && resultName == null) {
+                continue;
+            }
+
+            if (goldenName.equals(resultName)) {
+                continue;
+            }
+
+            throw new RuntimeException("Test name " + testName
+                    + ", result name: " + resultName);
+        }
+
+        for (URL[] testURLs : TEST_URLS) {
+            URL testURL = testURLs[0];
+            URL goldenURL = testURLs[1];
+            URL resultURL = getTestScaledImageURL(testURL);
+
+            if (!isValidPath(testURL.getPath()) && resultURL == null) {
+                continue;
+            }
+
+            if (goldenURL.equals(resultURL)) {
+                continue;
+            }
+
+            throw new RuntimeException("Test url: " + testURL
+                    + ", result url: " + resultURL);
+        }
+
+    }
+
+    static URL getTestScaledImageURL(URL url) throws Exception {
+        Method method = getScalableImageMethod("getScaledImageURL", URL.class);
+        return (URL) method.invoke(null, url);
+    }
+
+    static String getTestScaledImageName(String name) throws Exception {
+        Method method = getScalableImageMethod("getScaledImageName", String.class);
+        return (String) method.invoke(null, name);
+    }
+
+    private static boolean isValidPath(String path) {
+        return !path.isEmpty() && !path.endsWith("/") && !path.endsWith(".")
+                && !path.contains("@2x");
+    }
+
+    private static Method getScalableImageMethod(String name,
+            Class... parameterTypes) throws Exception {
+        Toolkit toolkit = Toolkit.getDefaultToolkit();
+        Method method = toolkit.getClass().getDeclaredMethod(name, parameterTypes);
+        method.setAccessible(true);
+        return method;
+    }
+    private static final String[][] TEST_FILE_NAMES;
+    private static final URL[][] TEST_URLS;
+
+    static {
+        TEST_FILE_NAMES = new String[][]{
+            {"", null},
+            {".", null},
+            {"..", null},
+            {"/", null},
+            {"/.", null},
+            {"dir/", null},
+            {"dir/.", null},
+            {"aaa@2x.png", null},
+            {"/dir/aaa@2x.png", null},
+            {"image", "image@2x"},
+            {"image.ext", "image@2x.ext"},
+            {"image.aaa.ext", "image.aaa@2x.ext"},
+            {"dir/image", "dir/image@2x"},
+            {"dir/image.ext", "dir/image@2x.ext"},
+            {"dir/image.aaa.ext", "dir/image.aaa@2x.ext"},
+            {"dir/aaa.bbb/image", "dir/aaa.bbb/image@2x"},
+            {"dir/aaa.bbb/image.ext", "dir/aaa.bbb/image@2x.ext"},
+            {"dir/aaa.bbb/image.ccc.ext", "dir/aaa.bbb/image.ccc@2x.ext"},
+            {"/dir/image", "/dir/image@2x"},
+            {"/dir/image.ext", "/dir/image@2x.ext"},
+            {"/dir/image.aaa.ext", "/dir/image.aaa@2x.ext"},
+            {"/dir/aaa.bbb/image", "/dir/aaa.bbb/image@2x"},
+            {"/dir/aaa.bbb/image.ext", "/dir/aaa.bbb/image@2x.ext"},
+            {"/dir/aaa.bbb/image.ccc.ext", "/dir/aaa.bbb/image.ccc@2x.ext"}
+        };
+        try {
+            TEST_URLS = new URL[][]{
+                // file
+                {new URL("file:/aaa"), new URL("file:/aaa@2x")},
+                {new URL("file:/aaa.ext"), new URL("file:/aaa@2x.ext")},
+                {new URL("file:/aaa.bbb.ext"), new URL("file:/aaa.bbb@2x.ext")},
+                {new URL("file:/ccc/aaa.bbb.ext"),
+                    new URL("file:/ccc/aaa.bbb@2x.ext")},
+                {new URL("file:/ccc.ddd/aaa.bbb.ext"),
+                    new URL("file:/ccc.ddd/aaa.bbb@2x.ext")},
+                {new URL("file:///~/image"), new URL("file:///~/image@2x")},
+                {new URL("file:///~/image.ext"),
+                    new URL("file:///~/image@2x.ext")},
+                // http
+                {new URL("http://www.test.com"), null},
+                {new URL("http://www.test.com/"), null},
+                {new URL("http://www.test.com///"), null},
+                {new URL("http://www.test.com/image"),
+                    new URL("http://www.test.com/image@2x")},
+                {new URL("http://www.test.com/image.ext"),
+                    new URL("http://www.test.com/image@2x.ext")},
+                {new URL("http://www.test.com/dir/image"),
+                    new URL("http://www.test.com/dir/image@2x")},
+                {new URL("http://www.test.com:80/dir/image.aaa.ext"),
+                    new URL("http://www.test.com:80/dir/image.aaa@2x.ext")},
+                {new URL("http://www.test.com:8080/dir/image.aaa.ext"),
+                    new URL("http://www.test.com:8080/dir/image.aaa@2x.ext")},
+                // jar
+                {new URL("jar:file:/dir/Java2D.jar!/image"),
+                    new URL("jar:file:/dir/Java2D.jar!/image@2x")},
+                {new URL("jar:file:/dir/Java2D.jar!/image.aaa.ext"),
+                    new URL("jar:file:/dir/Java2D.jar!/image.aaa@2x.ext")},
+                {new URL("jar:file:/dir/Java2D.jar!/images/image"),
+                    new URL("jar:file:/dir/Java2D.jar!/images/image@2x")},
+                {new URL("jar:file:/dir/Java2D.jar!/images/image.ext"),
+                    new URL("jar:file:/dir/Java2D.jar!/images/image@2x.ext")},
+                {new URL("jar:file:/aaa.bbb/Java2D.jar!/images/image.ext"),
+                    new URL("jar:file:/aaa.bbb/Java2D.jar!/images/image@2x.ext")},
+                {new URL("jar:file:/dir/Java2D.jar!/aaa.bbb/image.ext"),
+                    new URL("jar:file:/dir/Java2D.jar!/aaa.bbb/image@2x.ext")},};
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static class PreloadedImageObserver implements ImageObserver {
+
+        @Override
+        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
+            throw new RuntimeException("Image should be already preloaded");
+        }
+    }
+}