Sam Barnum
Sam Barnum

Reputation: 10734

Using JEditorPane from non-EventDispatchThread

I'm using a JEditorPane as a "rubber stamp" to render HTML text to a PDF. I need the text to wrap at specific widths, and need to apply a white "highlight" behind the text. As such, I'm creating a JEditorPane in the PDF rendering thread, setting the text and stylesheet, and then painting it to the PDF graphics. However, I'm getting an intermittent NullPointerException deep in the bowels of the HTML Editor Kit. This is reproducible with this SSCCE:

import javax.swing.*;
import javax.swing.text.View;
import javax.swing.text.html.HTMLDocument;
import java.awt.*;
import java.awt.image.BufferedImage;

/**
 * @author sbarnum
 */
public class TextMarkerUtilsTest {
    public static void main(String[] args) throws Exception {
        Rectangle bounds = new Rectangle(255, 255);
        BufferedImage image = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_INT_RGB);
        Graphics2D d = image.createGraphics();
        d.setClip(bounds);
        for (int i=0; i<1000; i++) {
            JEditorPane renderHelper = new JEditorPane("text/html", "<html><body>This is my text.</body></html>");
            HTMLDocument document = (HTMLDocument) renderHelper.getDocument();
            document.getStyleSheet().addRule("foo{color:black;}");
            View rootView = renderHelper.getUI().getRootView(renderHelper);
            rootView.paint(d, bounds);
        }
    }

}

Running the above throws the following exception, usually after just a few times through the loop:

java.lang.NullPointerException
    at sun.font.FontDesignMetrics$MetricsKey.init(FontDesignMetrics.java:199)
    at sun.font.FontDesignMetrics.getMetrics(FontDesignMetrics.java:267)
    at sun.swing.SwingUtilities2.getFontMetrics(SwingUtilities2.java:949)
    at javax.swing.JComponent.getFontMetrics(JComponent.java:1599)
    at javax.swing.text.LabelView.getFontMetrics(LabelView.java:154)
    at javax.swing.text.html.InlineView.calculateLongestWordSpanUseWhitespace(InlineView.java:246)
    at javax.swing.text.html.InlineView.calculateLongestWordSpan(InlineView.java:191)
    at javax.swing.text.html.InlineView.getLongestWordSpan(InlineView.java:177)
    at javax.swing.text.html.ParagraphView.calculateMinorAxisRequirements(ParagraphView.java:140)
    at javax.swing.text.BoxView.checkRequests(BoxView.java:918)
    at javax.swing.text.BoxView.getMinimumSpan(BoxView.java:551)
    at javax.swing.text.html.ParagraphView.getMinimumSpan(ParagraphView.java:261)
    at javax.swing.text.BoxView.calculateMinorAxisRequirements(BoxView.java:886)
    at javax.swing.text.html.BlockView.calculateMinorAxisRequirements(BlockView.java:129)
    at javax.swing.text.BoxView.checkRequests(BoxView.java:918)
    at javax.swing.text.BoxView.getMinimumSpan(BoxView.java:551)
    at javax.swing.text.html.BlockView.getMinimumSpan(BlockView.java:361)
    at javax.swing.text.BoxView.calculateMinorAxisRequirements(BoxView.java:886)
    at javax.swing.text.html.BlockView.calculateMinorAxisRequirements(BlockView.java:129)
    at javax.swing.text.BoxView.checkRequests(BoxView.java:918)
    at javax.swing.text.BoxView.setSpanOnAxis(BoxView.java:326)
    at javax.swing.text.BoxView.layout(BoxView.java:691)
    at javax.swing.text.BoxView.setSize(BoxView.java:380)
    at javax.swing.plaf.basic.BasicTextUI$RootView.setSize(BasicTextUI.java:1703)
    at javax.swing.plaf.basic.BasicTextUI$RootView.paint(BasicTextUI.java:1422)
    at com.prosc.msi.model.editor.TextMarkerUtilsTest$1.run(TextMarkerUtilsTest.java:40)
    at java.lang.Thread.run(Thread.java:680)

Some interesting findings:

The problem is in javax.swing.text.GlyphPainter1.sync(), where javax.swing.text.GlyphView.getFont() is returning null. By setting a conditional breakpoint, I see that the GlyphView in this case is a javax.swing.text.html.InlineView. Calling getFont() after the breakpoint has stopped returns a non-null font, so something is not being initialized in time.

I realize that the swing components are not thread-safe, but shouldn't I be able to instantiate a JEditorPane in a background thread and manipulate it safely in that background thread, as long as only the one thread is making calls to the component?

Upvotes: 1

Views: 466

Answers (2)

Sam Barnum
Sam Barnum

Reputation: 10734

Thanks to Marko for the suggestion to look for callbacks to the Event Dispatch Thread, I ended up finding one in HTMLDocument.styleChanged(). My subclass:

public class ThreadFriendlyHTMLDocument extends HTMLDocument {
    @Override
    protected void styleChanged(final Style style) {
        // to fix GlyphPainter1.sync NullPointerException, we call this in the current thread, instead of the EDT
        DefaultDocumentEvent dde = new DefaultDocumentEvent(0,
                          this.getLength(),
                          DocumentEvent.EventType.CHANGE);
        dde.end();
        fireChangedUpdate(dde);
    }
}

Upvotes: 0

trashgod
trashgod

Reputation: 205875

As you are using only lightweight components, headless mode may be an option. You can keep the work out of your GUI's EDT using ProcessBuilder, illustrated here.

public static void main(String[] args) throws Exception {
    System.setProperty("java.awt.headless", "true"); 
    EventQueue.invokeLater(new Runnable() {

        @Override
        public void run() {
            Rectangle bounds = new Rectangle(255, 255);
            BufferedImage image = new BufferedImage(
                bounds.width, bounds.height, BufferedImage.TYPE_INT_RGB);
            Graphics2D d = image.createGraphics();
            d.setClip(bounds);
            for (int i = 0; i < 1000; i++) {
                JEditorPane renderHelper = new JEditorPane(
                    "text/html", "<html><body>This is my text.</body></html>");
                HTMLDocument document = (HTMLDocument) renderHelper.getDocument();
                document.getStyleSheet().addRule("foo{color:black;}");
                View rootView = renderHelper.getUI().getRootView(renderHelper);
                rootView.paint(d, bounds);
            }
        }
    });
}

Upvotes: 2

Related Questions