Extending the PDF Viewer - Part 2

Creating a new Stamp feature and bundling it for an applet

Quite some time ago we started this series with Extending the PDF Viewer - Part 1, which covered a lot of the basics of the viewer API. However the example we gave then - a memory monitor widget - was a bit limited, and we didn't really cover deployment at all. We aim to fix that here.

Custom stamps

Custom stamps are quite a common reason for extending the viewer, and while it's possible to integrate them with our AnnotationStampFactory class, that's a little complex for our purposes today.

One of my fervent wishes is that I could stamp my opinions on some of the specifications we have to implement, so that seems like a good starting point for today.

First, download the example files. This Zip contains the source code, a build script and a few other pieces you'll need to get rolling. Also make sure you've read through part 1 of this series, as we're going to skim over ground we've already coverered there. Let's start with the code.

The Code

We're going to develop our new widget as mypackage.MyStamp, and we want to be able to click on the document to add a stamp at that point. There are other reasons to click on the document (selecting text, dragging the viewport around) and we don't want to intefere with those, so we're going to make our new class extend ToggleViewerWidget; a subclass of the regular ViewerWidget with a sort of "radio button" functionality - you can add several of these to a group, and only one can be active at once.

This feature is going to have a button on the toolbar, so the first thing we need is an icon. This needs to be 16x16, and we're going to put it in the mypackage/resources folder. We reference this in the constructor:

public MyStamp() {
  super("MyStamp", GROUP);
  setButton(GROUP, "resources/exclamation.png", "My Stamp");
}   

Configuration

A ViewerFeature usually needs some configuration to make it useful, and there's a standard way to retrieve configuration parameters - the getFeatureProperty method. Our feature is called mypackage.MyStamp so we could specify properties for this in an applet like so
<applet ...>
  <param name="mypackage.MyStamp.name" value="Jim" />
</applet>
or for applications, by setting the System property "mypackage.MyStamp.name"
java -Dmypackage.MyStamp.name=Jim org.faceless.pdf2.viewer2.PDFViewer
and we'd read the values in the widget's initialize method:
public void initialize(PDFViewer viewer) {
  super.initialize(viewer);
  name = getFeatureProperty(viewer, "name");
  if (name == null) {
    name = "Anonymous";
  }
}

Listeners

We covered a lot of ground for listeners in the previous article, so we'll skim this here - you can look at the code to see how it's done. One aspect of the ToggleViewerWidget is that the updateViewport method is called whenever the feature is selected or deselected. We use this to add a PagePanelInteractionListener to the viewport, which will be called whenever the mouse moves or is clicked on the PagePanel. From there adding the stamp is relatively simple (we're not covering the creation of the stamp image here, but it's very easy - see the source code for details):

public void pageAction(PagePanelInteractionEvent event) {
  if (isSelected() && event.getType() == "mouseClicked") { 
    AnnotationStamp stamp = new AnnotationStamp();
    PDFCanvas canvas = createCanvas();
    stamp.setCanvas("mystamp", canvas);

    // Center stamp on the click and add to page.
    Point2D point = event.getPoint();
    PDFPage page = event.getPage();
    float x = (float)point.getX();
    float y = (float)point.getY();
    float w2 = canvas.getWidth() / 2;
    float h2 = canvas.getHeight() / 2;
    stamp.setRectangle(x-w2, y-h2, x+w2, y+h2);
    stamp.setPage(page);
  }
}

Undo

We added the Undo feature to the PDF Viewer in 2.11.19, and if we want our new feature to interact with this we need to notify the DocumentPanel of our new edit, which we do by firing a new UndoableEdit event on it:
docpanel.fireUndoableEditEvent(new UndoableEditEvent(docpanel, new AbstractUndoableEdit() { 
  public String getPresentationName() { 
    return "Custom Stamp";
  } 
  public void undo() { 
    super.undo();  // Don't forget this!
    stamp.setPage(null);
  } 
  public void redo() { 
    super.redo();  // Don't forget this!
    stamp.setPage(page);
  } 
}));

This will cause an "Undo Custom Stamp" menu item to appear in the viewer's Edit menu. Selecting it will remove the stamp from the page, and a subsequent "Redo" will put the stamp back.

Deployment (as a Signed Applet)

We've covered the META-INF/services file which lists your new feature in the previous article, and if you're deploying to an application this is all that's required. Applets and Java Web Start applications are a more complex beast, so that's what we're going to cover here.

Since Java 1.6u10 there have been actually been two types of Applet Class loader (we've previously covered some of the new features in our New Features For Applets article). Which loader you get depends on the way you deploy:

  • Old-style loader. This is the original loader dating back from Java 1.1. Jar files are specified as a comma-separated list in the archive attribute. If the applet is to be trusted the the Jars that make it up must be digitally signed, and they must all be signed by the same digital signature. All Jars will be downloaded by the browser before the applet is started.

    Originally it was possible to mix signed Jars with unsigned Jars in the same applet - code that originated from a signed Jar was trusted, code that didn't was not. In Java 1.6u19 this changed, and mixing signed and unsigned Jars would result in a warning unless the Trusted-Library attribute was set in all the signed Jars. This has been the case in our PDF Library since 2.11.19.

  • New-style loader. This loader is used when the jnlp_href parameter is specified with the applet - it's the same loader that's used with Java Web Start applications. The rules when using the new loader are slightly stricter: either all Jars are unsigned and the applet is untrusted, or all Jars must be signed by the same digital signature. The big advantage of the new-style loader is that the applet can be broken into several Jars, with non-essential Jars loaded later on demand. This can make the applet load much faster.

Both of these designs lead to problems. You can only apply one digital signature to a Jar, and we deliver our Jars signed with our own key - so packaging your feature into it's own Jar and digitally signing it won't work. One approach is to re-sign our bfopdf.jar Jar with your own key, but if you're going to go to that much effort, we think the best approach is to bundle all the the classes that are required to initialize your applet into one Jar. This will work with either loader.

Ant

This leads us to the final step: Ant, the standard Java build tool which neatly replaced all the problems inherent in make with different problems. Let's consider the steps we would want to package our new feature:

  1. Compile the source code for our feature
  2. Add the compiled code and any resources it requires to our Jar
  3. Merge in the classes and resources from the PDF Library Jar to our Jar
  4. Ensure the list of features in META-INF/services/org.faceless.pdf2.viewer2.ViewerFeature contains the original list of features plus our new features
  5. Sign our Jar and create a pack200 version of it for speedy downloading

Of these steps, the merging of the list of features is a little tricky. In recent versions of ant you can do this to concatenate the META-INF/services/org.faceless.pdf2.viewer2.ViewerFeature file from the original Jar with the new one from the src directory.

<concat destfile="build/META-INF/services/org.faceless.pdf2.viewer2.ViewerFeature">
 <zipfileset src="bfopdf.jar">
  <include name="META-INF/services/org.faceless.pdf2.viewer2.ViewerFeature">
 </zipfileset>
 <fileset dir="src">
  <include name="META-INF/services/org.faceless.pdf2.viewer2.ViewerFeature">
 </fileset>
</concat>

The other part of the battle is pack200 - we've said it before but we'll say it again, if you're deploying as an applet outside of your local LAN then pack200 is the best way to speed up deployment for the user. Don't forget about it. The linked article has details on how to do it, and the build.xml includes a slightly simpler version of this as well.

The whole process is covered in the build.xml file we include with the Zip File.

The final step is to try it yourself. Download the Zip and modify build.xml so that it has the correct keystore path, alias and password for your digital siganture. Then run ant and the try out the resulting Jar file in an applet. Happy stamping!