beaTunes News

Monday, April 29, 2013

Creating Retina-capable Images with Apple's Java 6

beaTunes2 logoFor a while now, beaTunes supports Retina displays. It does this mostly through the NSImage hack. Any images loaded using Toolkit.getDefaultToolkit().getImage("NSImage://your_image_name_without_extension") that conform to the Apple @2x-naming scheme will be drawn in high resolution, if possible. Another trick to draw in high resolution is to apply an AffineTransform of (0.5, 0.5) to your Graphics2D object before drawing. But what do you do, when you cannot access the drawing code and your image is dynamically generated? For example a custom image for a JButton. Or something you loaded from the Internet. Then you somehow have to mimic what's happening when you are using the NSImage-hack. Unfortunately, Apple's JDK does not offer a public API for what it does. But the implementing classes are nevertheless accessible. So with a little bit of reflection magic, we can take advantage of the same mechanism.

Let's see... First we need to be able to figure out, what the current scale factor is. The scale factor is the factor between logical and physical pixels. Retina displays have a factor of 2.0f, regular displays have 1.0f. Luckily the factor is stored in a desktop property, so it's easily accessible:

private static final String APPLE_AWT_CONTENT_SCALE_FACTOR = "apple.awt.contentScaleFactor";

public static float getHiDPIScaleFactor() {
    final Float scaleFactor = (Float) Toolkit.getDefaultToolkit()
        .getDesktopProperty(APPLE_AWT_CONTENT_SCALE_FACTOR);
    return scaleFactor == null ? 1.0f : scaleFactor;
}

public static boolean useHiDPIScaling() {
    return getHiDPIScaleFactor() != 1f;
}

And now the real trickery (for brevity I skipped imports and proper error handling):

// Creates Retina version of the given image.
public static Image toHiDPIResolution(final Image image) {
    if (!useHiDPIScaling()) {
        return image;
    }

    Image hiDPICapableImage = image;
    try {
        final Class cImageClass = Class.forName("apple.awt.CImage");
        final Method getCreator = cImageClass.getDeclaredMethod("getCreator");
        getCreator.setAccessible(true);
        final Object creator = getCreator.invoke(null);
        final Image nativeImage = (Image)creator.getClass()
            .getMethod("createImage", Image.class).invoke(creator, image);

        final Method getNSImage = nativeImage.getClass()
            .getDeclaredMethod("getNSImage");
        getNSImage.setAccessible(true);
        final Object pointer = getNSImage.invoke(nativeImage);
        final float scaleFactor = getHiDPIScaleFactor();
        final int scaledWidth = (int) (image.getWidth(null) / scaleFactor);
        final int scaledHeight = (int) (image.getHeight(null) / scaleFactor);
        hiDPICapableImage = (BufferedImage)creator.getClass()
            .getMethod("createImageWithSize", Long.TYPE, Integer.TYPE, Integer.TYPE)
            .invoke(creator, pointer, scaledWidth, scaledHeight);

        // dereference first image, to avoid double release
        final Field fNSImage = nativeImage.getClass().getDeclaredField("fNSImage");
        fNSImage.setAccessible(true);
        fNSImage.setLong(nativeImage, 0L);
    } catch (Exception e) {
        // in real code - throw something!
        e.printStackTrace();
    }
    return hiDPICapableImage;
}

When called on a Retina capable Mac with Apple's Java 6 (no, it won't work on Java 7/8), this code generates an Image that will be drawn in high resolution without having to apply an AffineTransform on the graphics object. Naturally, logical pixel width and height are halved, i.e. a 200x200 pixel image becomes a 100x100 image, but on a Retina display all 200x200 pixel are still drawn.

Be warned: Since this hack relies on unpublished APIs, it may become unusable at any time due to changes in Apple's JVM.

Enjoy.

Labels: , ,

0 Comments:

Post a Comment

<< Home