FOLLOW ME Twitter Facebook Вконтакте LinkedIn RSS Feed

Retina support in Oracle JDK 1.7.0_40 and above

Category: Java
Jun 23, 2013
Konstantin Bulenkov

Hi everyone!

A few days ago I installed Oracle JDK (version 1.7.0_40) built especially for early access purposes. So, it’s not the final release. The build contains tons of bug fixes and backports from JDK 8 for AWT/Swing and that was the main point why I started to play with it. As you might have heard, Retina support was one of the major issues in all 1.7.* releases. I’d say there was no Retina support before the build 1.7.0_40. I’m going to show you some differences of Retina support in Apple and Oracle JDKs.
 

1. isRetina() method

That’s funny, but there is no public API for it in Oracle JDK 7. Here is how we (IntelliJ IDEA team) do it under Apple JDK:

class IsRetina {
  public static final boolean isRetina = isRetina();

  private static boolean isRetina() {
    try {
      final boolean[] isRetina = new boolean[1];
      new apple.awt.CImage.HiDPIScaledImage(1,1,BufferedImage.TYPE_INT_ARGB) {
        @Override
        public void drawIntoImage(BufferedImage image, float v) {
          isRetina[0] = v > 1;
        }
      };
      return isRetina[0];
    } catch (Throwable e) { 
      e.printStackTrace();
      return false;
    }
  }
}


and to avoid ClassNotFoundException we call IsRetina if and only if we are on Mac OS X.

public class UIUtil {
//...
  public static boolean isRetina() {
    if (SystemInfo.isJavaVersionAtLeast("1.6.0_33") && SystemInfo.isAppleJvm) {
      return IsRetina.isRetina;
    }
    //...
    return false; 
  }

These are the links to SystemInfo.java and UIUtil.java
 
What to do under Oracle JDK? I spent some time debugging IntelliJ and found a solution. The main idea is that different platforms have different implementations of java.awt.GraphicsDevice abstract class. On Mac OS X it’s apple.awt.CGraphicsDevice and it contains what we need – the scale factor.

public static boolean isRetina() {
  //other OS and JVM specific checks...
  if (SystemInfo.isJavaVersionAtLeast("1.7.0_40") && SystemInfo.isOracleJvm) {
    GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
    final GraphicsDevice device = env.getDefaultScreenDevice();

    try {
      Field field = device.getClass().getDeclaredField("scale");

      if (field != null) {
        field.setAccessible(true);
        Object scale = field.get(device);

        if (scale instanceof Integer && ((Integer)scale).intValue() == 2) {
          return true;
        }
      }
    } catch (Exception ignore) {}
  }
  //...
  return false;
}

2. Using retina quality images

If you don’t use double sized images in your java application they will be automatically resized on Retina. Here is an example (no retina icons at left and retina quality at right)
Icons
How to avoid your application looking so bad on Retina devices? This is one of possible solutions:
2.1 To avoid automatic resizing on retina devices you should provide two images for every icon: regular and double sized. For example, 16×16 and 32×32 for Retina. According Apple’s guidelines it’s better to name them say “myicon.png” and “myicon@2x.png” (with “@2x” at the end of double sized ones).

2.2 Handle image loading from resources and image painting. Write a simple class IconLoader with a static method getIcon(String path).

public static Icon getIcon(URL url) {
  if (isRetina()) {
    url = add2xAtTheEndOfPath(url);
    return new RetinaIcon(Toolkit.getDefaultToolkit().createImage(url));
  }

  return new ImageIcon(Toolkit.getDefaultToolkit().createImage(url));
}

private static final class RetinaIcon extends ImageIcon {

  public RetinaIcon(final Image image) {
    super(image);
  }

  public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
    ImageObserver observer = getImageObserver();

    if (observer == null) {
      observer = c;
    }

    Image image = getImage();
    int width = image.getWidth(observer);
    int height = image.getHeight(observer);
    final Graphics2D g2d = (Graphics2D)g.create(x, y, width, height);

    g2d.scale(0.5, 0.5);
    g2d.drawImage(image, 0, 0, observer);
    g2d.scale(1, 1);
    g2d.dispose();
  }
}

3. Custom painting using images as a buffer

When you have a component that requires difficult custom painting it’s better and cheaper to cache it once and then do paint a cached image. This pattern is mainly used when component repainting involves difficult model calculations and takes time on every repaint.
To understand the problem deeply I’ll give you an example. Let’s consider we have a QR-Code generator component that is a combination of JTextArea and a custom drawing component which paintComponent() method takes text from the text area, builds a data model, and paints the result.
QR-code generator
The paint() method here can take some time depending on model building complexity. Anyway, the paint() method will be called every time we activate/deactivate Window, or when we move mouse over it, etc. It could be called hundreds of times with the same data for model generation. So, why don’t just cache it and clear the cache on every text change?

private Image myCache;

public void paintComponent(Graphics g) {
  if (myCache == null) {
    myCache = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2d = myCache.createGraphics();
    oldPaintComponent(g2d); //how paintComponent() worked before
    g2d.dispose();
  }
  
  g.drawImage(myCache, 0, 0, null);
}

//called from listeners of the text area
public void clearCache() {
  myCache = null;
  repaint();
}

What’s the problem with Retina here? Right! Images get twice bigger automatically.

Oracle JDK 1.7.0_40ea:

Custom painting with Oracle JDK

Apple JDK 1.6.0_51:

Custom painting with Apple JDK

Apple JDK contains a special class apple.awt.CImage.HiDPIScaledImage to avoid situations like this.

public class AppleHiDPIScaledImage {
  public static BufferedImage create(int width, int height, int imageType) {
    return new CImage.HiDPIScaledImage(width, height, imageType) {
      @Override
      protected void drawIntoImage(BufferedImage image, float scale) {
      }
    };
  }
}

You can draw text, lines, shapes, etc into this image (technically speaking into its Graphics object) and everything will be OK.

Unfortunately, Oracle JDK doesn’t have such utility classes for Retina devices and I’m get stuck trying to find a simple solution here. I’ve played with double sized BufferedImage. No luck. Any ideas?

Update: I finally found a way to do it by implementing Graphics2D interface and using a special HiDPIScaledImage object. You can look at the sources here Look for classes: HiDPIScaledGraphics, JBHiDPIScaledImage, RetinaImage, and UIUtil.

14 Comments

  • Thank you for the very informative article!

  • Today, I tried build 40 and somehow text still looks blurry, I was wondering if High Resolution Capable .plist was needed as in Apple version, but still not working, any clues?

    • Hello Manuel, it sounds weird. Have you tried to run your java app with java -jar ? This should be in your plist file:

      <key>NSHighResolutionCapable</key>
      <true/>
  • How can I get the Intellij EAP13 to use the Oracle 1.7 JDK on my machine?
    After upgrading to Mavericks I no longer have the Java 6 installed.

    But if I use the idea executable I get an error:

    frj@frj-mbp-3:/Applications/IntelliJ IDEA 13 EAP.app $ Contents/MacOS/idea
    No Java runtime present, requesting install.

    I have tried editing JVMVersion in Info.plist but that did not change anything:
    JVMVersion
    1.7*

    Any tips will be much appreciated – otherwise I will have to install the Apple JDK 6 just for Intellij

    • Check that JDK is installed correctly

      localhost:~ kb$ /usr/libexec/java_home -v 1.7
      /Library/Java/JavaVirtualMachines/jdk1.7.0_40.jdk/Contents/Home
      localhost:~ kb$
      • My Oracle JDK 1.7u45 was working ok, and /usr/libexec/java_home -v1.7 returned a working path to it. But Intellij did not try and use this version – even with the Info.plist containing JVMVersion 1.7*.

        Before I saw your reply I ended up installing Apples JDK6 – my boss was getting impatient with me :-/
        Apple JDK 6 installation made it work again.

        About the same time I wrote to you I had also asked the question in the EAP support forum – http://devnet.jetbrains.com/message/5501767#5501767 – I got a reply from one of your supporters redirecting me to:
        https://intellij-support.jetbrains.com/entries/27854363
        Dated 23rd of October stating that Apple JDK6 is in fact needed to launch Intellij.

        When I have time later today I will attempt uninstalling the JDK6 and trying your Info.plist.

        I diff’ed it with mine and the only difference (apart from version numbers) is the VMOptions section.
        In mine I have

        -agentlib:yjpagent=disablej2ee,disablealloc,disabletracing,onlylocal,builtinprobes=none,disableexceptiontelemetry,delay=10000,sessionname=IntelliJIdea13

        In yours you don’t have the agent lib argument at all – but you have a setting I do not have:
        -Didea.is.internal=true

        I’m guessing it is only the agentlib part I need to strip from my plist to test if jdk 1.7 can be used?

        • Well, you don’t need idea.is.internal=true It just makes some idea developers actions available. And you don’t need the agent too. We use it to let users make CPU/memory snapshots from toolbar.

    • Btw, I’m on Mavericks now and everything works fine. I have not reinstalled my JDKs. Try to use my Info.plist http://pastebin.com/fk1t5zdB

  • It is somewhat difficult to remove Apple JDK 6 once it has been installed. So I’m afraid that I cannot test how the behaviour is on a system without the Apple JDK.

    But I can confirm that Idea is in fact using my Oracle JDK 1.7 when I have JVMOptions 1.7* in the Info.plist (I did not need to remove the -agentlib part from it).

    But before I installed the Apple JDK 6 – I was unable to launch Intellij at all.

    So maybe it is just the binary that is ‘hard-coded’ to use the Apple JDK and once it is launched, reads the Info.plist configuration and uses the Oracle JDK 1.7 for launching Intellij.

    • I have a fresh installation of OS X Mavericks + Java 7u45 (Apple’s Java SE 6 is NOT installed) and I can confirm, that IntelliJ IDEA cannot launch (throwing: No Java runtime present, requesting install.).
      Changing the Info.plist’s entry to 1.7* doesn’t work either.
      So IntelliJ IDEA must be relying on some Apple’s Java 6 components…

      • Thanks, we’ve fixed it in the final release of IntelliJ IDEA 13. We’re going to announce it tomorrow or the day after tomorrow.

  • Hi, thanks for your post, I am using HiDpi 3200*1800 windows machine, IntelliJ is not supporting HiDpi in 13.1.4 … I tried everything… it is not working … What will be your solution?

    • I’m working on some solution right now. At the moment there is no any.

      • That would be great to have IntelliJ works nice in Windows or Linux

Leave a comment