1

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:

  • If the above runs in the Event Dispatch Thread, it works
  • If I take out the call to addRule("foo{color:black;}"), it works (I need to specify rules, but it doesn't seem to matter what the rule is, it fails if any rules are added)

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?

Sam Barnum
  • 10,559
  • 3
  • 54
  • 60
  • 1
    Are you positive that the Swing code you execute never registers any callbacks? Those callbacks will be executed on the EDT. I don't mean the code you wrote, but all the code that gets executed as a consequence of your Swing calls. – Marko Topolnik Jun 25 '12 at 19:11
  • @Marko, I cannot find any callbacks in the Swing code referenced above, but it's pretty complicated. I would have guessed something about the CSS parsing was happening in a thread, but as far as I can tell everything is all happening in the same thread. – Sam Barnum Jun 25 '12 at 19:25
  • As an alternate, any recommendation for how to render HTML text to a graphics object in a background thread would be welcome. I do need highlight support for the background, and the ability to specify a default font for the HTML, and specify a fixed width. – Sam Barnum Jun 25 '12 at 20:05
  • @Marko, I did find one occurrence: DefaultStyledDocument.styleChanged posts on the EDT, which then calls rootView.changedUpdate on the View. This could be the culprit. I don't see any easy way around this, however. Looking into alternatives. – Sam Barnum Jun 25 '12 at 20:36
  • But why don't you transfer control to the EDT for the relevant code segment? Why is that not an option? – Marko Topolnik Jun 25 '12 at 20:37
  • @Marko, This is a swing app which generates a PDF in the background, while a progress bar ticks off the page. Headless is not an option, unfortunately, nor is letting the EDT paint the PDF notes (this breaks the progress bar) – Sam Barnum Jun 26 '12 at 19:46

2 Answers2

1

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);
            }
        }
    });
}
Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • This is a swing app which generates a PDF in the background, while a progress bar ticks off the page. Headless is not an option, unfortunately, nor is letting the EDT paint the PDF notes (this breaks the progress bar) – Sam Barnum Jun 26 '12 at 19:05
  • Spawn the headless JVM from a `SwingWorker`; you'll have to pass progress through `stdout/stdin` and forward it to your `PropertyChangeListener`. – trashgod Jun 26 '12 at 19:22
  • Thanks for the suggestion, I need to share memory with the JVM process. Think I found a workaround. – Sam Barnum Jun 26 '12 at 21:14
0

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);
    }
}
Sam Barnum
  • 10,559
  • 3
  • 54
  • 60
  • Correct, but it works if all access to the component happens from a single (non-EDT) thread. Compared to the default implementation, which rebuilds the View in the EDT, even if the style sheet was changed from some other thread. Most likely with the assumption that eventually the component would be rendered in the EDT. – Sam Barnum Jun 27 '12 at 21:07