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: , ,

Sunday, April 21, 2013

beaTunes 3.5.12 - on Java7

beaTunes2 logoEven though Java 7 was originally released in 2011 (yes, pretty much two years ago!), there still isn't a version available for OS X that works as well as Apple's Java 6. However, we are getting very close to going prime time. As far as I know, only a couple of things are still missing. Most notable are Retina support and a flawlessly working full screen mode. But neither are really showstoppers. So, in addition to the regular beaTunes update, which can be found at the usual location, I decided to post another version of the update which comes with a bundled Java 7.
Why? Well, there really are two reasons:
  1. Feedback
  2. Speed!

Seriously, when playing around with it, it just left a really good impression. It feels a lot faster, more responsive. Neat. I figured, some of you might appreciate this :-)
So, take it for a spin and if you encounter any issues, please let me know in the support forum.
Thanks!
Requirement: OS X 10.7.3 or later
Download link: beaTunes-3-5-12-jre-osx.dmg (98mb)

Update

Due to this bug in Java 7, you cannot create new folder-based libraries with this release. beaTunes simply won't let you choose a folder (see also here). To work around this issue, you can first download the regular release, create the library and then install the Java 7 release.

Update 2

Unfortunately, the current Java 7 (1.7.0_21) for OS X still can't deal with Umlauts in file names. For beaTunes this means a lot of FileNotFoundExceptions. I was assured by the Java dev team, that the fix, which is already included in Java 8 EA versions, will be available soon.

Labels: ,