Friday, March 28, 2008

Using iText's PdfPageEventHelper with ColdFusion



A poster named Lemonhead recently mentioned using an iText helper class called PdfPageEventHelper from ColdFusion. If you are not familiar with it, PdfPageEventHelper is a java class that you can extend, and use to perform tasks like adding headers and footers to a PDF file.

I had never used this class with ColdFusion before, so I created a very simple example to show how it works. You can find a full java example on the iText site.

Due to the way PdfPageEventHelper is designed, I could not just call it from ColdFusion. I had to delve into some java and create my own class that extends it. The same way you extend a cfcomponent. So I opened Eclipse and created a new java project. (I love the fact that I can use Eclipse for both ColdFusion and Java projects!). Then I added the iText jar (version 2.0.7) to my project. Other versions should work as well, as long as they are new enough to contain the PdfPageEventHelper class. The built in version that ships with ColdFusion 8 does not.

Next I created a java class that extends PdfPageEventHelper. Then I overwrote the onEndPage method. The onEndPage method is trigged just before iText adds a new page. So it is the recommended spot for placing code that adds headers, footers, etcetera. I placed a few lines of code inside onEndPage that will add a given string (and page number), to the footer of each page. Nothing fancy. Finally, I compiled the class and exported it as a jar file that I could use from ColdFusion. That was it for the java side of things.

Since the PdfPageEventHelper class is not included in the ColdFusion 8 iText jars, I had to use the JavaLoader.cfc to load a newer version of iText, and the jar I created in Eclipse. If you run the CF example below, it will generate a simple PDF file with a silly reddish-pink footer on each page ;)



Well, that is all she wrote. A special thanks to Lemonhead for introducing me to something new! As this is my first foray into using the PdfPageEventHelper with ColdFusion, comments / corrections / suggestions are welcome!

See also Prerequisites / Detailed instructions

Java Code

package itextutil;

import java.awt.Color;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.ExceptionConverter;
import com.lowagie.text.Phrase;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfPageEventHelper;
import com.lowagie.text.pdf.PdfWriter;

public class MyPageEvent extends PdfPageEventHelper {
private String footerText;
private Color textColor;
private float textSize;
protected BaseFont textFont;

public static void main(String[] args) {
String outFile = args[0];
Document document = new Document();
try {
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(outFile));
writer.setPageEvent(new MyPageEvent("Testing Footer stuff", 6, new Color(255, 80, 180)));

document.open();
for (int i = 0; i < 10; i++) {
document.add(new Phrase("The best way to be boring is to leave nothing out. "));
if (i < 10) {
document.newPage();
}
}
}
catch (FileNotFoundException e) {
e.printStackTrace();
}
catch (DocumentException e) {
e.printStackTrace();
}
document.close();
System.out.println("Done creating file " + outFile);
}

public MyPageEvent() {
this("Page ", 6, new Color(0, 0, 0));
}

public MyPageEvent(String footerText, float textSize, Color textColor) {
this.footerText = footerText;
this.textSize = textSize;
this.textColor = textColor;
}

public void onOpenDocument(PdfWriter writer, Document document) {
try {
this.textFont = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.EMBEDDED);
}
catch (DocumentException e) {
throw new ExceptionConverter(e);
}
catch (IOException e) {
throw new ExceptionConverter(e);
}
}

public void onEndPage(PdfWriter writer, Document document) {
PdfContentByte cb = writer.getDirectContent();
cb.saveState();
String text = footerText + " " + writer.getPageNumber();
cb.beginText();
cb.setColorFill(textColor);
cb.setFontAndSize(textFont, textSize);
cb.setTextMatrix(document.left(), document.bottom() - 10);
cb.showText(text);
cb.endText();
cb.restoreState();
}
}


ColdFusion Code

<h1>PdfPageEventHelper example</h1>
<cfscript>
savedErrorMessage = "";

// all file paths are relative to the current directory
fullPathToOutputFile = ExpandPath("./PageEventHelperResult.pdf");

// settings used for dynamic footer text
Color = createObject("java", "java.awt.Color");
footerText = "BOREDOM ALERT! BOREDOM ALERT! Page number ";
footerTextColor = Color.decode("##cc0000");
footerFontSize = 12;

// get instance of javaLoader stored in the server scope
javaLoader = server[MyUniqueKeyForJavaLoader];

// step 1: create a new document-object. uses U.S. letter size pages
document = javaLoader.create("com.lowagie.text.Document").init();

try {
// step 2: create a writer that listens to the document
// and directs a PDF-stream to a file
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
writer = javaLoader.create("com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);

// step 3: create an instance of our custom page event
// class and add it to the PdfWriter
pageEvent = javaLoader.create("itextutil.MyPageEvent").init(
footerText,
javacast("float", footerFontSize),
footerTextColor
);
writer.setPageEvent( pageEvent );

// step 4: open the document and add a few sample pages
phrase = javaLoader.create("com.lowagie.text.Phrase");
totalPages = 10;
document.open();
for (i = 1; i LTE totalPages; i = i + 1) {
document.add( phrase.init("The best way to be boring is to leave nothing out. ") );
document.add( phrase.init("The best way to be boring is to leave nothing out. ") );
document.add( phrase.init("The best way to be boring is to leave nothing out. ") );
if (i LT totalPages) {
document.newPage();
}
}

}
catch (Exception e) {
savedErrorMessage = e;
}

// close document and output stream objects
if ( structKeyExists(variables, "document") ) {
document.close();
}
if ( structKeyExists(variables, "outStream") ) {
outStream.close();
}

WriteOutput("Done!");
</cfscript>

<!--- show any errors --->
<cfif len(savedErrorMessage) gt 0>
Error. Unable to create file
<cfdump var="#savedErrorMessage#">
</cfif>

7 comments:

Anonymous,  September 18, 2008 at 1:47 PM  

Hi,

Thanks again for your blog - I have found lots of useful stuff here.

I have been experimenting with the page helper technique outlined in this post and have that working well.

I can get a reasonable amount of flexibility by passing the style info in a similar way to your example.

However, it occurs to me that an improvement would be if you could get the onPageEnd listener in the java to call a CF function that you plugged in, and then do all the end page stuff in CF rather than java. For example, you could define what your header and footer looks like dynamically and on a pdf by pdf case instead of having to recompile the java whenever you wanted a different type of header / footer that wasnt covered by the style data you pass in.

Something like:
pageEvent = javaLoader.create("itextutil.MyPageEvent").init( myCF_onEndPageFunc);

I am only a java dabbler at this stage so I dont know if that could be done. Do you know if you can pass cf functions to java so that you can tell the onEndPage method in the java class to call the cf function?

Thanks,
Murray

cfSearching September 19, 2008 at 9:06 AM  

@Murray,

Interesting idea. Since CF functions are classes themselves it is probably possible.

I have not thought through all of the ramifications. But the first one that comes to mind is that the "function" class is internal to CF (ie undocumented). So its usage incurs the usual risk of the application breaking if anything changes in a future version.

cfSearching September 19, 2008 at 9:10 AM  

@Murray,

Since you peaked my interest, I will put together an example later. Just to see how the idea plays out ;-)

Anonymous,  September 19, 2008 at 2:07 PM  

Cool, I look forward to what you discover.

Thanks,
Murray

Anonymous,  September 19, 2008 at 2:33 PM  

because if that works, it opens the way to creating a generic pageEvent jar for onStartPage, onCloseDocument etc. That would be cool!

cfSearching September 19, 2008 at 9:54 PM  

@Murray,

What do you know? It works. I still do not know if there are other issues, aside from the undocumented part. But it is a pretty cool concept.

I will clean up the code an post it tomorrow. (It needs a little explanation).

(Now if only I could spell "piqued" ;)

cfSearching September 21, 2008 at 11:29 PM  

@Murray,

I posted Part 1. I will post the second half tomorrow.

Thanks again for the idea!

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep