Agile Ajax

How to REALLY do Page Preview in Java with Embedded HTML Rendering

Note: the screenshots in the post are all generated by the JRex solution, not print screen.

So I had a reader call me out -- correctly -- that a HOWTO article should really contain some code, not just pointers and handwaving on how things might be done. So I've decided to make amends by following through on my article from two weeks ago on how to do page preview. Here are my experiences, some code, and -- once I've cleaned up the code -- a zip file so you can try your own hand at embedded HTML rendering.

So I tried each of the HTML renderers from my article in turn. The results were less than promising.

  1. Flying Saucer: forget it. This thing barfs on anything but the most basic HTML. Putting Tidy or TagSoup in front of it didn't help matters much.
  2. Cobra HTML Toolkit: slightly better. I could get www.google.com to render, but that was about it. Real world Javascript, CSS and XHTML all caused this puppy to tuck its tail and run for home.
  3. JRex: since it's based on Mozilla, it actually does work and renders just about any page. But getting it to work is anything but easy. The end result I achieved gets me what I want -- images of rendered pages -- but there are some things about the solution that may make it unreliable and hard to scale.

slashdot.png

The rest of this post is about my experiences with JRex. A few preliminaries: first, I developed and deployed this solution on Windows (XP and Advanced Server). It should be possible to do so on Linux (JRex runs there same there as on Windows). Next, the code is a bit of a hack. This was definitely a keep-hacking-until-it-works kind of effort. My hope is that someone else can come along and take this to the next level.

Hello World with JRex

The documentation for JRex leaves something to be desired, so the first challenge is installing the thing and getting it to run. I went with the binary download of JRex 1.0b1_dom3 from September 8th, 2005. You need the binary WithOutLog download and the jrex_gre dowload. Unzip the zip file into C:/jrex, then unjar the second download to a scratch directory. In the unjared material, find the file org\mozilla\jrex\jrex_gre.zip. Unzip this to C:/jrex/jrex_gre. Finally, move the file jrex.dll from C:/jrex to C:/jrex/jrex_gre.

Now we are ready to write a little "Hello World!" program, just to test if everything is working. The following is a minimal program that launches a browser window:

package test;

import org.mozilla.jrex.JRexFactory;
import org.mozilla.jrex.ui.JRexCanvas;
import org.mozilla.jrex.window.JRexWindowManager;
import javax.swing.*;

public class JRexTest {
public static void main(String[] args) {
try {
JRexFactory.getInstance().startEngine();
} catch (Exception e) {
System.err.println("Unable to start up JRex Engine.");
e.printStackTrace();
System.exit(1);
}
JRexWindowManager winManager=(JRexWindowManager)
JRexFactory.getInstance().getImplInstance(JRexFactory.WINDOW_MANAGER);
winManager.create(JRexWindowManager.SINGLE_WINDOW_MODE);
JPanel inner = new JPanel();
JFrame frame = new JFrame();
frame.getContentPane().add(inner);
winManager.init(inner);
frame.setSize(640, 480);
frame.setVisible(true);
}
}

Compile and then run with a JRE (otherwise under certain circumstances JRex may not link to the awt DLL):

C:\Java\jdk1.5.0_08\jre\bin\java.exe -Djrex.gre.path=C:/jrex/jrex_gre text.JRexTest

Once we've come this far we're ready to move to the web side of things.

Preview Webapp

I decided to build a webapp that produces PNG image screenshots. My first thought was to simply build a servlet and use the headless trick to grab the output of the JRex components. There are a couple of issues that complicate matters, however:

  1. JRex just won't run in headless mode. You have to have a display. On Linux you could use VNC.
  2. With the complication of classloaders, etc., JRex won't run easily or cleanly in Tomcat, Jetty, etc. Therefore I decided to use a simple embedded web server. This of course has its downsides, as a good servlet container provides you with a number of advantages.
  3. JRex is not a pure HTML renderer -- it is an application, that pops up dialogs, responds to user events, etc. There may be a way to just use it as renderer, but that documentation thing gets in the way again. As it is now, there is no way to tell whether the rendering was successful or whether the JRex component is stuck with a modal dialog telling the nonexistent user that the server was not responding.
  4. JRex is not a real Swing/AWT component. You can't use a Graphics object to paint it to an offscreen image; all you will get is a blank page. We need to capture the part of the screen that corresponds to the browser window. That means that the browser window needs to be on top and of the size you want to image. It also means we need one display per preview server if we want to scale. (On Linux you could scale with multiple servers and multiple X displays such as VNC.)
  5. We can only handle one request at a time per preview server as we only have one screen.

To solve the problem of the application server, I used the dead simple embedded web server NanoHTTPD. If you need something dead simple, this is a handy way to go.

On to the actual image creation. Our web page will be in a JFrame on the display. We need a way to capture that JFrame to an image. We do this in SwingImageCreator using screen capture:

public static BufferedImage snapShot(Rectangle rect) throws AWTException {
return new Robot().createScreenCapture(rect);
}

public static BufferedImage snapShot(JFrame frame) throws AWTException {
Point pt = frame.getLocation();
return snapShot(new Rectangle(pt.x, pt.y, frame.getWidth()+pt.x, frame.getHeight() + pt.y));
}

The screen capture requires that our frame be on top. We do this in the PageRendererImpl class by calling frame.setAlwaysOnTop(true):

public synchronized BufferedImage renderPage(String url) {
try {
createBrowser();
navigation.loadURI(url, WebNavigationConstants.LOAD_FLAGS_NONE, null, null, null);
frame.setAlwaysOnTop(true);
try {
Thread.sleep(THIRTY_SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
BufferedImage img = SwingImageCreator.snapShot(frame);
frame.setAlwaysOnTop(false);
return img;
} catch (Exception e) {
e.printStackTrace();
return new BufferedImage(DEFAULT_IMAGE_DIM, DEFAULT_IMAGE_DIM, BufferedImage.TYPE_INT_ARGB);
}
}

The method is synchronized to prevent more than one thread for accessing the screen (of which we have only one).

The first thing to notice here is that we use a hack, e.g. sleeping for 30 seconds instead of detecting via an event listener whether the page has rendered. The next is that we open a new browser window each time. Ideally we would know if there is a popup or some other issue with the JRex browser, but instead we just nuke it and create another browser. One other way we prevent unwanted popups is by creating the window manager in JRexWindowManager.SINGLE_WINDOW_MODE, in which mode the browser will not open other tabs or windows. Still, if you enter a URL that doesn't load, you get something like this:

error.png

The createBrowser method also creates the Navigator object that is responsible for loading the page:

private void createBrowser() {
if (frame != null) {
// destroy and create anew
frame.dispose();
}
frame = new JFrame();
JPanel inner = new JPanel();
frame.getContentPane().add(inner);
mgr.init(inner);
canvas = (JRexCanvas) mgr.getBrowserForParent(inner);
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
frame.setSize((d.width> BROWSER_WIDTH ? BROWSER_WIDTH :d.width), d.height - BROWSER_HEIGHT_DELTA);
frame.setLocation(0,0);
navigation = canvas.getNavigator();
}

This solution certainly has plenty of warts, but it sort of works. It will take more time than I have to dig into the code of JRex and discover what can be done to make it a renderer instead of an app or to at least detect error conditions such as modal dialogs.

I'll clean up the code soon and post a link here to the zip file.


Technorati : , ,

Comments: 10 so far

  1. It’s certainly a hacky way to make it all work - maybe using a HTML Render isn’t the best solution afterall.

    The other day I found a company with similar solution but they tackle it by dealing with display memory directly (http://www.guangmingsoft.net/).

    It really makes you wonder how snap.com generates all those preview images…

    Comment by gogobu, Sunday, January 28, 2007 @ 12:32 am

  2. Hallo, please post link to the zip Ajax preview page…thx

    Comment by Oggy, Saturday, April 28, 2007 @ 1:57 pm

  3. I think there is an error at the command:
    “C:\Java\jdk1.5.0_08\jre\bin\java.exe -Djrex.gre.path=C:/jrex/jrex_gre text.JRexTest”

    your package is named “test” not “text”.
    I have a suggestion for use the code line: “System.setProperty(”jrex.gre.path”,”C:/jrex/jrex_gre”);”
    This semplify the command line.

    I hope this article is very useful.
    bye

    Comment by roli, Wednesday, October 24, 2007 @ 12:28 pm

  4. Thank you Dietrich for the tutorial, it is well explained.
    I have done all steps but I have the Exception:UnsatisfiedLinkError.
    Have you any idea what can I do ?

    Comment by Radhouane, Saturday, November 17, 2007 @ 5:04 pm

  5. Hi, this is a year old but perhaps you can still help. I’m not familiar with packages like this and how to use them. I’m still learning java really. I attempted to compile JRex with Eclipse and that was a disaster. This seems simpler since you have the already compiled binaries, yet i cannot get your example code to compile. Do I need to change my java classpath? Right now i have all default settings and i use textpad to compile on Windows Vista. Should i not be able to create “JRexTest.java” in C:/jrex/jrex_gre and compile it? it has errors that say package org.mozilla.jrex doesn’t exist

    I’m just trying to get a java browser i can modify that supports javascript, Thanks

    Comment by Harry, Tuesday, January 22, 2008 @ 5:33 pm

  6. Hi, this is a year old but perhaps you can still help. I’m not familiar with packages like this and how to use them. I’m still learning java really. I attempted to compile JRex with Eclipse and that was a disaster. This seems simpler since you have the already compiled binaries, yet i cannot get your example code to compile. Do I need to change my java classpath? Right now i have all default settings and i use textpad to compile on Windows Vista. Should i not be able to create “JRexTest.java” in C:/jrex/jrex_gre and compile it? it has errors that say package org.mozilla.jrex doesn’t exist

    I’m just trying to get a java browser i can modify that supports javascript, Thanks

    Comment by Harry, Tuesday, January 22, 2008 @ 5:35 pm

  7. sorry for the double post, I figured out how to add jrex.jar to the classpath and it compiles, but i have an error when i run it(in textpad)

    ***************** JRexL inited *****************
    ***************** JREX-Logging disabled *****************
    Exception in thread “main” org.mozilla.jrex.exception.JRexException
    at org.mozilla.jrex.xpcom.JRexXPCOMImpl.startXPCOM(JRexXPCOMImpl.java:143)
    at org.mozilla.jrex.JRexFactory.startEngine(JRexFactory.java:222)
    at test.JRexExample.main(JRexExample.java:50)
    Caused by: java.lang.Exception: is set to null!!!
    at org.mozilla.jrex.xpcom.JRexXPCOMImpl$1.run(JRexXPCOMImpl.java:149)

    Comment by Harry, Tuesday, January 22, 2008 @ 7:03 pm

  8. um… me again :)I added this >>> System.setProperty(”jrex.gre.path”,”C:/jrex/jrex_gre/”);
    still doesn’t work… sorry for posting repeatedly

    Exception in thread “main” org.mozilla.jrex.exception.JRexException
    at org.mozilla.jrex.xpcom.JRexXPCOMImpl.startXPCOM(JRexXPCOMImpl.java:143)
    at org.mozilla.jrex.JRexFactory.startEngine(JRexFactory.java:222)
    at test.JRexExample.main(JRexExample.java:52)
    Caused by: java.lang.UnsatisfiedLinkError: org.mozilla.jrex.xpcom.JRexXPCOMImpl.
    InitXPCOM(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
    at org.mozilla.jrex.xpcom.JRexXPCOMImpl.InitXPCOM(Native Method)
    at org.mozilla.jrex.xpcom.JRexXPCOMImpl.access$200(JRexXPCOMImpl.java:45)
    at org.mozilla.jrex.xpcom.JRexXPCOMImpl$1.run(JRexXPCOMImpl.java:173)

    Comment by Harry, Tuesday, January 22, 2008 @ 7:24 pm

  9. me again!
    i found out the cause of my previous error was not following directions
    i forgot to change the path from JDK to JRE… jawt.dll couldn’t be found and it threw me an error

    Comment by Harry, Wednesday, January 23, 2008 @ 4:02 pm

  10. Can u explain me wht changes did u make as im also getting the same error

    Comment by anu, Tuesday, June 17, 2008 @ 6:56 pm

Leave a comment

Powered by WP Hashcash

About Pathfinder

  • We design and build extraordinary applications for companies looking to make the next great idea a reality.
  • learn more

Topics

WordPress

Comments about this site: info@pathf.com