--- /dev/null 2013-11-13 19:50:26.000000000 +0400 +++ new/src/share/classes/com/sun/awt/MultiResolutionImage.java 2013-11-13 19:50:25.000000000 +0400 @@ -0,0 +1,66 @@ +/* + * 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 com.sun.awt; + +import java.awt.Image; + +/** + * This interface is designed to provide a set of images at various resolutions. + * + * WARNING: 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. + * + * The method should be overridden by any class whose instances are intended + * to provide image resolution variants according to the given image width + * and height. For example, + *
+     * {@code
+     *  public class ScaledImage extends BufferedImage
+     *         implements MultiResolutionImage {
+     *    @Override
+     *    public Image getResolutionVariant(int width, int height) {
+     *      return ((width <= getWidth() && height <= getHeight()))
+     *             ? this : highResolutionImage;
+     *    }
+     *  }
+     * }
+ * + * It is recomended to cache image variants for performance reasons. + * + * @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); +} --- old/src/share/classes/sun/awt/SunHints.java 2013-11-13 19:50:26.000000000 +0400 +++ new/src/share/classes/sun/awt/SunHints.java 2013-11-13 19:50:26.000000000 +0400 @@ -172,7 +172,7 @@ } } - private static final int NUM_KEYS = 9; + private static final int NUM_KEYS = 10; private static final int VALS_PER_KEY = 8; /** @@ -253,6 +253,13 @@ @Native public static final int INTVAL_STROKE_PURE = 2; /** + * Image scaling hint key and values + */ + @Native public static final int INTKEY_RESOLUTION_VARIANT = 9; + @Native public static final int INTVAL_RESOLUTION_VARIANT_DEFAULT = 0; + @Native public static final int INTVAL_RESOLUTION_VARIANT_OFF = 1; + @Native 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. @@ -450,6 +457,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, + "Default resolution variant usage"); + public static final Object VALUE_RESOLUTION_VARIANT_OFF = + new SunHints.Value(KEY_RESOLUTION_VARIANT, + SunHints.INTVAL_RESOLUTION_VARIANT_OFF, + "Use only default resolution variants of images"); + public static final Object VALUE_RESOLUTION_VARIANT_ON = + new SunHints.Value(KEY_RESOLUTION_VARIANT, + SunHints.INTVAL_RESOLUTION_VARIANT_ON, + "Use resolution variants of images"); public static class LCDContrastKey extends Key { --- old/src/share/classes/sun/java2d/SunGraphics2D.java 2013-11-13 19:50:27.000000000 +0400 +++ new/src/share/classes/sun/java2d/SunGraphics2D.java 2013-11-13 19:50:27.000000000 +0400 @@ -93,6 +93,7 @@ import sun.misc.PerformanceLogger; import java.lang.annotation.Native; +import com.sun.awt.MultiResolutionImage; /** * This is a the master Graphics2D superclass for all of the Sun @@ -237,6 +238,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; @@ -283,6 +285,9 @@ if (devScale != 1) { transform.setToScale(devScale, devScale); invalidateTransform(); + resolutionVariantHint = SunHints.INTVAL_RESOLUTION_VARIANT_ON; + } else { + resolutionVariantHint = SunHints.INTVAL_RESOLUTION_VARIANT_OFF; } font = f; @@ -1249,6 +1254,10 @@ stateChanged = (strokeHint != newHint); strokeHint = newHint; break; + case SunHints.INTKEY_RESOLUTION_VARIANT: + stateChanged = (resolutionVariantHint != newHint); + resolutionVariantHint = newHint; + break; default: recognized = false; stateChanged = false; @@ -1322,6 +1331,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; } @@ -3050,18 +3062,62 @@ } // 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 { // Image scaling hints are on + // get scaled destination image size + + double scaleX = devScale; + double scaleY = devScale; + + if (transformState == TRANSFORM_TRANSLATESCALE) { + scaleX = transform.getScaleX(); + scaleY = transform.getScaleY(); + } + + int scaledImageWidth = Math.abs(Region.clipScale(dx2 - dx1, scaleX)); + int scaledImageHeight = Math.abs(Region.clipScale(dy2 - dy1, scaleY)); + + Image scaledImage = ((MultiResolutionImage) img). + getResolutionVariant(scaledImageWidth, scaledImageHeight); + + if (scaledImage != null && scaledImage != img) { + // recalculate source region for the scaled image + int width = img.getWidth(null); + int height = img.getHeight(null); + + int scaledWidth = scaledImage.getWidth(null); + int scaledHeight = scaledImage.getHeight(null); + + if (0 < width && 0 < height + && 0 < scaledWidth && 0 < scaledHeight) { + + float widthScale = (float) scaledWidth / width; + float heightScale = (float) scaledHeight / height; + + sx1 = Region.clipScale(sx1, widthScale); + sy1 = Region.clipScale(sy1, heightScale); + sx2 = Region.clipScale(sx2, widthScale); + sy2 = Region.clipScale(sy2, heightScale); + img = scaledImage; + } + } + } + try { return imagepipe.scaleImage(this, img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); --- old/src/macosx/classes/sun/lwawt/macosx/LWCToolkit.java 2013-11-13 19:50:28.000000000 +0400 +++ new/src/macosx/classes/sun/lwawt/macosx/LWCToolkit.java 2013-11-13 19:50:28.000000000 +0400 @@ -35,14 +35,19 @@ import java.awt.im.InputMethodHighlight; import java.awt.peer.*; import java.lang.reflect.*; +import java.io.File; +import java.io.InputStream; +import java.net.URL; import java.security.*; import java.util.*; import java.util.concurrent.Callable; import sun.awt.*; +import sun.awt.image.ToolkitImage; import sun.lwawt.*; import sun.lwawt.LWWindowPeer.PeerType; import sun.security.action.GetBooleanAction; +import com.sun.awt.MultiResolutionImage; import sun.util.CoreResourceBundleControl; @@ -489,9 +494,37 @@ @Override public Image getImage(final String filename) { final Image nsImage = checkForNSImage(filename); - if (nsImage != null) return nsImage; + if (nsImage != null) { + return nsImage; + } + + Image image = super.getImage(filename); + Image scalableImage = ScalableToolkitImage.toScalableImage(image, filename); + if (image != scalableImage) { + putImageToCache(filename, (ToolkitImage) scalableImage); + } + return scalableImage; + } - return super.getImage(filename); + @Override + public Image getImage(URL url) { + Image image = super.getImage(url); + Image scalableImage = ScalableToolkitImage.toScalableImage(image, url); + if (image != scalableImage) { + putImageToCache(url, (ToolkitImage) scalableImage); + } + return scalableImage; + } + + @Override + public Image createImage(String filename) { + return ScalableToolkitImage.toScalableImage( + super.createImage(filename), filename); + } + + @Override + public Image createImage(URL url) { + return ScalableToolkitImage.toScalableImage(super.createImage(url), url); } static final String nsImagePrefix = "NSImage://"; @@ -781,4 +814,76 @@ public boolean enableInputMethodsForTextComponent() { return true; } + + public static final class ScalableToolkitImage extends ToolkitImage implements MultiResolutionImage { + + Image highResolutionImage; + + public ScalableToolkitImage(Image lowResolutionImage, Image highResolutionImage) { + super(lowResolutionImage.getSource()); + this.highResolutionImage = highResolutionImage; + } + + @Override + public Image getResolutionVariant(int width, int height) { + return ((width <= getWidth() && height <= getHeight())) + ? this : highResolutionImage; + } + + static Image toScalableImage(Image image, String fileName) { + + if (fileName != null && !fileName.contains("@2x") + && !(image instanceof ScalableToolkitImage)) { + String filename2x = getScaledImageName(fileName); + if (filename2x != null && new File(filename2x).exists()) { + return new ScalableToolkitImage(image, getDefaultToolkit().getImage(filename2x)); + } + } + return image; + } + + @SuppressWarnings("try") + static Image toScalableImage(Image image, URL url) { + + if (url != null && !url.toString().contains("@2x") + && !(image instanceof ScalableToolkitImage)) { + try { + URL url2x = getScaledImageURL(url); + + if (url2x != null) { + try (InputStream is = url2x.openStream()) { + return new ScalableToolkitImage(image, + getDefaultToolkit().getImage(url2x)); + } + } + } catch (Exception ignore) { + } + } + return image; + } + + private static URL getScaledImageURL(URL url) throws Exception { + String scaledImagePath = getScaledImageName(url.getPath()); + return scaledImagePath == null ? null : new URL(url.getProtocol(), + url.getHost(), url.getPort(), scaledImagePath); + } + + private static String getScaledImageName(String path) { + if (!isValidPath(path)) { + return null; + } + int slash = path.lastIndexOf('/'); + String name = (slash < 0) ? path : path.substring(slash + 1); + + 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("."); + } + + } } --- old/src/share/classes/sun/awt/SunToolkit.java 2013-11-13 19:50:29.000000000 +0400 +++ new/src/share/classes/sun/awt/SunToolkit.java 2013-11-13 19:50:29.000000000 +0400 @@ -774,6 +774,18 @@ } } + static protected void putImageToCache(String fileName, ToolkitImage image){ + synchronized (imgCache) { + imgCache.put(fileName, image); + } + } + + static protected void putImageToCache(URL url, ToolkitImage image){ + synchronized (imgCache) { + imgCache.put(url, image); + } + } + public Image getImage(String filename) { return getImageFromHash(this, filename); } --- /dev/null 2013-11-13 19:50:30.000000000 +0400 +++ new/test/java/awt/image/MultiResolutionImageTest.java 2013-11-13 19:50:30.000000000 +0400 @@ -0,0 +1,347 @@ +/* + * 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.MediaTracker; +import java.awt.RenderingHints; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.Objects; +import javax.imageio.ImageIO; +import javax.swing.JPanel; +import sun.awt.OSInfo; +import sun.awt.SunHints; +import com.sun.awt.MultiResolutionImage; + +/** + * @test @bug 8011059 + * @author Alexander Scherbatiy + * @summary [macosx] Make JDK demos look perfect on retina displays + * @run main MultiResolutionImageTest + */ +public class MultiResolutionImageTest { + + private static final int WIDTH = 300; + private static final int 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 { + testCustomScaledImage(); + + if (OSInfo.getOSType() == OSInfo.OSType.MACOSX) { + testToolkitScaledImage(); + testImageNameTo2xParsing(); + } + } + + public static void testCustomScaledImage() { + testCustomScaledImage(false); + testCustomScaledImage(true); + } + + public static void testCustomScaledImage(boolean enableImageScaling) { + + Image image = new MultiResolutionBufferedImage(); + + // Same image size + BufferedImage bufferedImage1x = new BufferedImage(WIDTH, HEIGHT, + BufferedImage.TYPE_INT_RGB); + Graphics2D g1x = (Graphics2D) bufferedImage1x.getGraphics(); + setImageScalingHint(g1x, enableImageScaling); + g1x.drawImage(image, 0, 0, null); + checkColor(bufferedImage1x.getRGB(3 * WIDTH / 4, 3 * HEIGHT / 4), false); + + // Twice image size + bufferedImage1x = new BufferedImage(2 * WIDTH, 2 * HEIGHT, + BufferedImage.TYPE_INT_RGB); + g1x = (Graphics2D) bufferedImage1x.getGraphics(); + setImageScalingHint(g1x, enableImageScaling); + g1x.drawImage(image, 0, 0, 2 * WIDTH, 2 * HEIGHT, 0, 0, WIDTH, HEIGHT, null); + checkColor(bufferedImage1x.getRGB(3 * WIDTH / 2, 3 * HEIGHT / 2), enableImageScaling); + } + + static class MultiResolutionBufferedImage extends BufferedImage + implements MultiResolutionImage { + + Image highResolutionImage; + + public MultiResolutionBufferedImage() { + super(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + highResolutionImage = new BufferedImage( + 2 * WIDTH, 2 * 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, WIDTH, HEIGHT); + } + + @Override + public Image getResolutionVariant(int width, int height) { + return ((width <= getWidth() && height <= getHeight())) + ? this : highResolutionImage; + } + + } + + static void testToolkitScaledImage() throws Exception { + + generateImage(1); + generateImage(2); + + File imageFile = new File(IMAGE_NAME_1X); + Image image = Toolkit.getDefaultToolkit().getImage(imageFile.getAbsolutePath()); + testToolkitScaledImage(image, false); + testToolkitScaledImage(image, true); + + image = Toolkit.getDefaultToolkit().getImage(imageFile.toURI().toURL()); + testToolkitScaledImage(image, false); + testToolkitScaledImage(image, true); + } + + static void testToolkitScaledImage(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(WIDTH, HEIGHT, + BufferedImage.TYPE_INT_RGB); + Graphics2D g1x = (Graphics2D) bufferedImage1x.getGraphics(); + setImageScalingHint(g1x, false); + g1x.drawImage(image, 0, 0, null); + checkColor(bufferedImage1x.getRGB(3 * WIDTH / 4, 3 * HEIGHT / 4), false); + + Image scaledImage = ((MultiResolutionImage) image). + getResolutionVariant(2 * WIDTH, 2 * HEIGHT); + + if (scaledImage == null) { + throw new RuntimeException("Scaled image is null"); + } + + MediaTracker tracker2x = new MediaTracker(new JPanel()); + tracker2x.addImage(scaledImage, 0); + tracker2x.waitForID(0); + if (tracker2x.isErrorAny()) { + throw new RuntimeException("Error during scalable image loading"); + } + + final BufferedImage bufferedImage2x = new BufferedImage(2 * WIDTH, + 2 * HEIGHT, BufferedImage.TYPE_INT_RGB); + Graphics2D g2x = (Graphics2D) bufferedImage2x.getGraphics(); + setImageScalingHint(g2x, enableImageScaling); + g2x.drawImage(image, 0, 0, 2 * WIDTH, 2 * HEIGHT, 0, 0, WIDTH, HEIGHT, null); + checkColor(bufferedImage2x.getRGB(3 * WIDTH / 2, 3 * HEIGHT / 2), enableImageScaling); + } + + static void setImageScalingHint(Graphics2D g2d, boolean enableImageScaling) { + if (enableImageScaling) { + g2d.setRenderingHint(SunHints.KEY_RESOLUTION_VARIANT, + SunHints.VALUE_RESOLUTION_VARIANT_ON); + } + } + + 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 generateImage(int scale) throws Exception { + BufferedImage image = new BufferedImage(scale * WIDTH, scale * HEIGHT, + BufferedImage.TYPE_INT_RGB); + Graphics g = image.getGraphics(); + g.setColor(scale == 1 ? COLOR_1X : COLOR_2X); + g.fillRect(0, 0, scale * WIDTH, scale * 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("."); + } + + private static Method getScalableImageMethod(String name, + Class... parameterTypes) throws Exception { + + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Class[] classes = toolkit.getClass().getClasses(); + Class scalableImageClass = null; + + for (Class cls : classes) { + if (cls.getName().endsWith("ScalableToolkitImage")) { + scalableImageClass = cls; + break; + } + } + + if (scalableImageClass == null) { + throw new RuntimeException("ScalableToolkitImage does not exist"); + } + Method method = scalableImageClass.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}, + {"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); + } + } +}