Wizard API updated!
Tim Boudreau has released a new version of the Swing Wizard library (version 0.997) that fixes the WizardException bug reported in JavaWorld's recent Open Source Java Project profile. The article's examples have been reworked to test out the new, improved WizardException. Thanks, Tim, for this helpful fix!
Open Source Java Projects: The Wizard API

Newsletter sign-up

Sign up for our technology specific newsletters.

Enterprise Java
View all newsletters

Email Address:

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.

1 | 2 |  Next >
Resources