Java Tip 109: Display images using JEditorPane

Modify the JEditorPane component to display images

You can use the current JEditorPane component to display HTML markup, but to perform more complicated tasks, JEditorPane needs some improvement. Recently, I had to build an XML form builder application. One necessary component was a WYSIWYG HTML editor that could edit the HTML markup content inside some of the XML tags. JEditorPane was the obvious Java component choice for displaying the HTML markup, because that functionality was already built into it. Unfortunately, when inserted into the HTML markup, JEditorPane could not display images with relative paths. For example, if the following image with a relative path was contained in an XML tag, it would not be displayed properly:

<html><img src="images\example.gif" width=200 
height=200></img>

Conversely, an absolute path would work (assuming that the given path and image really existed):

<html><img src="file:\\c:\images\example.gif" width=200 
height=200></img>

In my application, images were always stored in a subdirectory relative to the XML file's location. Hence, I always wanted to use a relative path. This article will explain why this problem exists and how to fix it.

Why does this happen?

Taking a closer look at the constructors for JEditorPane will help us understand why it cannot display images in relative paths.

  1. JEditorPane() creates a new JEditorPane.
  2. JEditorPane(String url) creates a JEditorPane based on a string containing a URL specification.
  3. JEditorPane(String type, String text) creates a JEditorPane that has been initialized to the given text.
  4. JEditorPane(URL initialPage) creates a JEditorPane based on a specified URL for input.

The second and fourth constructors initialize the object with a reference to a remote or local HTML file. An HTMLDocument is inside every JEditorPane, and its base is set to the base of the URL constructor parameter. JEditorPanes created using those constructors can handle relative paths, because the base of the HTMLDocument combines with the relative path to create an absolute path.

If the first constructor is used, displayed text must be inserted after the object is created. The third constructor accepts a String as content, but the base is not initialized. Because I wanted to obtain the HTML markup from an XML tag and not a file, I needed to use either the first or third constructor.

How do we fix the problem?

Before I continue, let's unveil and solve another smaller problem. The most obvious way to insert markup into the JEditorPane is to use the setText(String text). However, that method requires that you input the entire displayed markup every time you make a change. Ideally, the new tag(s) should be inserted into the existing text. You can use the following code to add the new markup:

private void insertHTML
  (JEditorPane editor, String html, int location)
                                 throws IOException {
  //assumes editor is already set to "text/html" type
  HTMLEditorKit kit =
    (HTMLEditorKit) editor.getEditorKit();
  Document doc = editor.getDocument();
  StringReader reader = new StringReader(html);
  kit.read(reader, doc, location);
}

Now, getting to the heart of the matter: How does JEditorPane render HTML? Each type of JEditorPane references both a Document and an EditorKit. When JEditorPane is set to type "text/html", it contains an HTMLDocument, which contains the markup and an HTMLEditorKit that determines which classes render each tag contained in the markup. Specifically, the HTMLEditorKit class contains an HTMLFactory inner class whose create(Element elem) method actually examines each separate tag. Here is the code from that factory class, which handles image tags:

            else if (kind==HTML.Tag.IMG)
              return new ImageView(elem);

As you can now see, the ImageView class actually loads the image. To establish the image's location, the getSourceURL() method is called:

    private URL getSourceURL( ) {
      String src = (String) fElement.getAttributes().
            getAttribute(HTML.Attribute.SRC);
      if( src==null ) return null;
      URL reference = ((HTMLDocument)getDocument()).
                      getBase();
        try {
          URL u = new URL(reference,src);
          return u;
        } catch (MalformedURLException e) {
          return null;
        }
    }

Here, the getSourceURL() method attempts to create a new URL to reference the image using the HTMLDocument base. If that base is null, null is returned and the image-loading operation is aborted. You want to override that behavior.

Ideally, you would subclass the ImageView class and override the initialize(Element elem) method, where the image-loading is done. Unfortunately, that class is package protected, so you must create an entirely new class. The easiest way to do that is to borrow, then modify, the code from the original ImageView class. Let's call it MyImageView.

First, look at the code that loaded the image. The following is taken from the initialize(Element elem) method:

          URL src = getSourceURL();
          if( src != null ) {
            Dictionary cache = (Dictionary)
getDocument().getProperty(IMAGE_CACHE_PROPERTY);
            if( cache != null )
                fImage = (Image) cache.get(src);
            else
                fImage = Toolkit.getDefaultToolkit().getImage(src);
          }

Here, you obtain the URL; if it's null, you skip the image loading. In MyImageView, you should only execute this code if your image reference is a URL. The following is a method you can add to test the image source:

    private boolean isURL() {
        String src =
          (String)
fElement.getAttributes().getAttribute(HTML.Attribute.SRC);
        return src.toLowerCase().startsWith("file") ||
               src.toLowerCase().startsWith("http");
    }

Basically, you obtain the reference to the image in the form of a String and test to see whether it begins with one of the two types of URL: file for local images and http for remote images. Jens Alfke, author of the original javax.swing.text.html.ImageView class, uses class global variables, so passing parameters to functions is unnecessary. Here, the global variable is fElement.

You can write code that says if (isURL()) {<execute URL code>}, but what do you put into the else statement for a relative path? It's quite simple -- just load the image as you normally would in an application:

           else {
             String src =
               (String) fElement.getAttributes().getAttribute
                  (HTML.Attribute.SRC);
             fImage = Toolkit.getDefaultToolkit().createImage(src);
           }

There is no real magic here, but there is one catch. The createImage(src) function can return before all the image's pixels have been populated. If that happens, a broken image will be displayed. To fix the problem, you can just wait until the image's pixels are completely populated. My first inclination was to use the MediaTracker to detect when the image was ready, but the MediaTracker's constructor requires the component rendering the image as a parameter. So once again, I borrowed some code from Jim Graham's java.awt.MediaTracker and wrote my own method to circumvent the problem:

    private void waitForImage() throws InterruptedException {
      int w = fImage.getWidth(this);
      int h = fImage.getHeight(this);
      while (true) {
        int flags = Toolkit.getDefaultToolkit().checkImage(fImage, w, h,
this);
        if ( ((flags & ERROR) != 0) || ((flags & ABORT) != 0 ) )
          throw new InterruptedException();
        else if ((flags & (ALLBITS | FRAMEBITS)) != 0)
          return;
        Thread.sleep(10);
        //System.out.println("rise and shine...");
      }
    }

This method basically does the same job as the MediaTracker's waitForID(int id) method, but does not require a parent component. A call to this method can be placed just after the image is created.

There is a small problem that I should mention before I continue. It was impossible to subclass ImageView from the javax.swing.text.html package, so I copied the entire file to create my own class, called MyImageView, which I have not put in a package. In the original ImageView code, if an image cannot be displayed because it does not exist or is delayed, it loads a default broken image from the javax.swing.text.html.icons package. To load the broken image, the class uses the getResourceAsStream(String name) method from the Class class. The actual code looks like this:

    InputStream resource =
HTMLEditorKit.class.getResourceAsStream(MISSING_IMAGE_SRC);

where the MISSING_IMAGE_SRC parameter is a String with content:

    MISSING_IMAGE_SRC =
      "icons" + System.getProperty("file.separator", "/") +
"image-failed.gif";

The following excerpt from the ImageView source code explains Sun's reasoning for using the getResourceAsStream(String name) method for loading the broken image(s).

    /* Copy resource into a byte array.  This is
     * necessary because several browsers consider
     * Class.getResource a security risk because it
     * can be used to load additional classes.
     * Class.getResourceAsStream just returns raw
     * bytes, which we can convert to an image.
     */

If you haven't skipped through this section yet (I know, it's pretty nitty-gritty!), let me explain why I mention it. If you aren't aware of this behavior, you won't understand why broken images are not displayed correctly, and won't be able to fix the problem in your own code. To fix the problem, you must load your own images. I chose to continue using the same method, but it's not really necessary. The above warning is for browsers containing applets, which have security considerations that limit disk access (unless signed, of course). In any case, this article was intended for use with an application, so using an alternate image-loading method should not be a concern.

When a call to getResourceAsStream(String name) is made, you can include a relative path to the image, as illustrated above. In the above code, the broken image will always be loaded from the specified path relative to the HTMLEditorKit class. For example, since the HTMLEditorKit class is located in javax.swing.text.html, it will attempt to load the broken image image-failed.gif from javax.swing.text.html.icons. This also applies to simple directories; the classes do not have to be in packages. Lastly, since HTMLEditorKit is package protected, you do not have access to its getResourceAsStream(String name) method. Instead, you can use the MyImageView class and put your broken images in an icons subdirectory. The code line will look like this:

    InputStream resource =
MyImageView.class.getResourceAsStream(MISSING_IMAGE_SRC);

If you choose to use an implementation similar to mine, you will have to create your own icons. You can still use the icons bundled with Sun's JDK, but that requires changing the location of the resource to use an absolute path instead of a relative path. The absolute path is:

javax.swing.text.html.icons.imagename.gif

To learn about using getResourceStream(String name), see the Javadoc information for the Class class; a link is provided in Resources.

This article is almost entirely about accommodating relative paths -- but what are they relative to? So far, if you use the code I have supplied, you will only be able to use paths relative to where you started the application. This is great if all your images are always located in those paths, but that is not always the case. I won't go into great detail on how to fix this problem, because it can be fixed easily. You can either set an application global variable somewhere in your application or set a system variable. In MyImageView, before loading the image, you concatenate the relative path to the image and the absolute path obtained from the global variable. If that doesn't make sense, look for the processSrcPath() method in the final source code for MyImageView.

At last, MyImageView is complete. However, you must figure out how to tell JEditorPane to use MyImageView instead of javax.swing.text.html.ImageView. The JEditorPane can support three text formats: plain, RTF, and HTML. If JEditorPane is displaying HTML, BasicHTML -- a subclass of TextUI -- is used to render the HTML. BasicHTML uses JEditorPane's HTMLEditorKit to create the View. The HTMLEditorKit contains a method called getViewFactory(), which returns an instance of an inner class called HTMLFactory. The HTMLFactory contains a method called create(Element elem), which returns a View according to the tag type. Specifically, if the tag is an IMG tag, it returns an instance of ImageView. To return an instance of MyImageView, you can create your own EditorKit called MyHTMLEditorKit, which subclasses HTMLEditorKit. Inside your MyHTMLEditorKit, you create a new inner class called MyHTMLFactory, which subclasses HTMLFactory. In that inner class, you can make your own create(Element elem) method, which looks something like this:

    public View create(Element elem) {
      Object o =
        elem.getAttributes().getAttribute(StyleConstants.NameAttribute);
      if (o instanceof HTML.Tag) {
        HTML.Tag kind = (HTML.Tag) o;
        if (kind == HTML.Tag.IMG)
          return new MyImageView(elem);
      }
      return super.create( elem );
    }

The only thing left to do now is set the JEditorPane to use MyHTMLEditorKit. The code is quite simple:

JEditorPane editor = new JEditorPane();
editor.setEditorKit(new MyHTMLEditorKit());

Now, using the insertHTML() method that you created earlier, you can write code like this:

int location = findLocation();
String html = "<img src=\"test.gif\">";
try {
  insertHTML(editor, html, location);
}
catch (IOException e) { e.printStackTrace(); }

I made up the findLocation() method to illustrate that you can insert the image at any valid location in the Document -- not just at the beginning.

Now you are on your way to writing your own WYSIWYG HTML editor!

Rob Kenworthy owns and operates an independent contracting company that primarily caters to clients who need Java-based solutions. He holds degrees in English literature and computer science, and has been developing in Java for the past two years. When not being a nerd, he enjoys snowboarding and rollerblading.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more