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=" width=200
height=200></img>
Conversely, an absolute path would work (assuming that the given path and image really existed):
<html><img src=" 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.
JEditorPane()
creates a newJEditorPane
.JEditorPane(String url)
creates aJEditorPane
based on a string containing a URL specification.JEditorPane(String type, String text)
creates aJEditorPane
that has been initialized to the given text.JEditorPane(URL initialPage)
creates aJEditorPane
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. JEditorPane
s 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 =
" + 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 <a href="#resources">MyImageView</a>
.
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="
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!