Friday, March 28, 2008

Instructions for CF + iText + PdfPageEventHelper Example (Festivus for the Rest of Us)

Now it is time for the airing of grievances..

Perhaps I should be flattered, but I dislike articles that assume I know everything there is to know about the topic at hand. Let us face it, if I were omniscient I would not be would be reading the article in the first place ;) So for people like me, here are some detailed instructions to accompany the previous entry on using the PdfPageEventHelper.

For those that hate detailed instructions, well .. I am still airing my grievances. Wait your turn ;)

What you will need for the example:

1. Eclipse. Yes, you could use another IDE. However, since I am using Eclipse, you are on your own for instructions ;-)

2. Download the iText-2.0.7.jar (or newer) available at sourceforge.net

Example: My iText jar is located in the same directory as my CFM script. You can use a different location if you prefer.
C:\ColdFusionMX7\wwwroot\iTextExamples\iText-2.0.7.jar

3. Download and install the JavaLoader.cfc available at riaforge.org

Example: I installed the entire javaloader folder under the webroot
C:\ColdFusionMX7\wwwroot\javaloader


Step-by-Step Instructions



Step 1: Create the java jar that will be used from ColdFusion

A. Create a java project
  • Open Eclipse > File > New Java Project

  • Enter the name: iTextUtilities

  • Click "Finish"

B. Add the iText jar to the project
  • Project > Properties > Java Build Path

  • Select "Add External Jars" and navigate to wherever you saved the iText jar and select it.

  • Click "OK"

C. Create a package
  • Right click the "src" folder > New Package

  • Enter the name: itextutil

  • Click "Finish"

D. Create the java class
  • Right click the "itextutil" package > New Class

  • Enter the Name: MyPageEvent

  • Enter the Superclass: com.lowagie.text.pdf.PdfPageEventHelper

  • Click "Finish"

  • Paste in the java code

  • Compile the class: Project > Clean

  • Warning: The package and class names are case sensitive. The instructions/example may not work if you change the case.

E. Create the java jar
Step 2: Run the ColdFusion example

A) Install the jar files
  • Place the jar you created in step 1 AND the newer iText jar in a directory accessible to ColdFusion

  • Example: My jars are located here
    C:\ColdFusionMX7\wwwroot\iTextExamples\itextutil.jar
    C:\ColdFusionMX7\wwwroot\iTextExamples\iText-2.0.7.jar

B) Add the jars to the javaLoader

Example: I instantiated the JavaLoader in OnApplicationStart.


<!---
Application.cfc
--->
<cfcomponent>
<cfset this.name = "iTextExamples">
<cfset this.Sessionmanagement = true>
<cfset this.loginstorage = "session">
<!---
... other settings
---->

<cffunction name="onApplicationStart">
<!--- note, this is actually a harcoded UUID value --->
<cfset MyUniqueKeyForJavaLoader = "xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx">

<!--- if we have not already created the javaLoader --->
<cfif NOT structKeyExists(server, MyUniqueKeyForJavaLoader)>
<!--- construct an array containing the full path to the jars you wish to load --->
<cfset pathArray = arrayNew(1)>
<cfset arrayAppend(pathArray, expandPath('/iTextExamples/iText-2.0.7.jar'))>
<cfset arrayAppend(pathArray, expandPath('/iTextExamples/itextUtil.jar'))>

<cflock scope="server" type="exclusive" timeout="10">
<!--- verify again the javaloader was not already created --->
<cfif NOT StructKeyExists(server, MyUniqueKeyForJavaLoader)>
<cfset server[MyUniqueKeyForJavaLoader] = createObject("component", "javaloader.JavaLoader").init(pathArray)>
</cfif>
</cflock>
</cfif>
</cffunction>

<!---
... other functions
---->
</cfcomponent>


You should now be ready run the ColdFusion example

...Read More

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>

...Read More

Tuesday, March 25, 2008

ImageDrawText - Negative font sizes give text a new spin

I discovered today (completely by accident) that using a negative font size gives image text a whole new spin. It rotates the text 180 degrees. Similar code in java produces the same results. So it must be related to the underlying java classes.


The following ColdFusion code produces the image below.

Negative Font Size Example


<cfset img = ImageNew("", 200, 100)>
<cfset attr = { font="Arial", style="bold", size="-20" }>
<cfset ImageDrawText(img, "Transform Me", 190, 10, attr)>
<cfimage action="writeToBrowser" source="#img#">




The one code gotcha is the x coordinate. Since the text is rotated 180 degrees, the x coordinate is now relative to the right hand side. So if the desired x position is 10, you must use x = ImageGetWidth(img)-10 instead of x = 10.

Now I have not seen this documented anywhere. So I would use ImageRotateDrawingAxis instead. But it was still an interesting find ;)

Update: I corrected a typo in example/function name.

ImageRotateDrawingAxis

<cfset img = ImageNew("", 200, 100)>
<cfset ImageRotateDrawingAxis(img, 180, 100, 10)>
<cfset attr = { font="Arial", style="bold", size="20" }>
<cfset ImageDrawText(img, "Transform Me", 10, 10, attr)>
<cfimage action="writeToBrowser" source="#img#">

...Read More

Sunday, March 23, 2008

CF8 + MS JDBC 1.2 Driver - Generated key issues. Curiouser and Curiouser

In a previous entry I wrote about seeing some bizarre results when using CF8 and the MS JDBC 1.2 driver to obtain identity values. Dan G. Switzer, II pointed out two important CF8 bugs that might possibly be related to the strange behavior. He was right. The weird results are related to the second issue, with a slight twist.

Nathan Mische wrote about the second issue in his entry problems with CF 8's Generated Keys Feature. In summary, he points out that CF8's new ability to return generated keys is achieved by appending a SELECT SCOPE_IDENTITY() statement onto the end of insert statements. This happens even if you do not use cfquery's result attribute. His entry shows how you can use the SQL Profiler to verify this.

Since my results were slightly different, I decided to use the SQL Profiler to see what sql was actually sent to the database by the three (3) different drivers: CF8 Driver, MS SQL JDBC 1.0, and MS SQL JDBC 1.2. My test sql was a basic insert statement.


<cfquery name="insert" datasource="#dsn#" result="result">
INSERT INTO SomeTable ( Name )
VALUES ( 'Test' )
</cfquery>


As you can see below, the results for the CF8 Driver show that select scope_identity() was appended to the sql, as Nathan Mische described.



The results for the JDBC 1.0 Driver show that nothing was appended to the query.



However, the results for the JDBC 1.2 driver show that select scope_identity() AS GENERATED_KEYS is added to the query. I am not certain why it adds the alias "GENERATED_KEYS".



But that does partially explain why the query below does not work as expected with JDBC 1.2.


<cfquery name="insert" datasource="#dsn#" result="result">
INSERT INTO SomeTable ( Name )
VALUES ( 'Test' )
SELECT SCOPE_IDENTITY() AS NewRecordID
</cfquery>


The SQL Profiler shows that the actual sql sent to the database is:


INSERT INTO SomeTable ( Name )
VALUES ( 'Test' );
SELECT SCOPE_IDENTITY() AS NewRecordID
select SCOPE_IDENTITY() AS GENERATED_KEYS


As a result, CF seems to ignore the first select/column alias, and uses the second one instead. So if you dump the query, you can see the column name becomes GENERATED_KEYS.



The original issue blogged by Nathan Mische also explains another problem with the ColdFusion 8 driver. This problem was mentioned by an adobe forum member named sws. He/she observed that when you comment out the select statement, result.IDENTITYCOL becomes undefined. Run the following code.


<cfquery name="insert" datasource="#dsn#" result="result">
INSERT INTO SomeTable ( Name )
VALUES ( 'Test' )
-- SELECT SCOPE_IDENTITY() AS NewRecordID
</cfquery>

<cfdump var="#result#">
<cfif IsDefined("insert")>
<cfdump var="#insert#">
<cfelse>
query not defined
</cfif>


The SQL Profiler shows that select SCOPE_IDENTITY() is appended to the end of the commented line. So that statement is never executed.



Consequently, result.IDENTITYCOL is not defined.



I hate to say it, but I am calling "bug" on all these behaviors.

...Read More

Friday, March 21, 2008

CF8 + MS JDBC 1.2 Driver - .. And for my next trick, I will make this query disappear!

A few weeks ago I mentioned a possible bug I found working with CF8's built in MS SQL driver. Now a recent post, by an adobe forum member named sws, mentioned another strange behavior. This time with the MS JDBC 1.2 driver. So I decided to run a few tests with the three main drivers (CF8, MS SQL JDBC 1.0 and JDBC 1.2) and compare the results.

Now, I expected a few variances here and there. But the results were a bit bizarre from the start. So I just continued running more tests, reviewing the results and scratching my head. After a while I started to wonder if maybe I had fallen down a rabbit hole. A few tests later, I swear the song They're coming to take me away popped into my head. (If you have never heard the song, it is worth a listen. Once. It is .. an odd ... song. Yet, strangely appropriate for situations like this ;)

Anyway, if you take a look at a basic INSERT/VALUES statement everything seems okay until you get to the JDBC 1.2 driver. This was the issue mentioned by sws. CF does return a query, but the column name is GENERATED_KEYS. Huh? What happened to the column alias? Even more confusing is that jdbc has a method called getGeneratedKeys() and GENERATED_KEYS is also the result name used for ids from MySQL databases. So it is difficult to identify the real culprit here.



















































SQL:
INSERT INTO SomeTable ( Name )

VALUES ( 'Test' )

SELECT SCOPE_IDENTITY() AS NewRecordID

#dsncolumnListrecord Countdata
1.CF8 (SQL2000)NEWRECORDID 1row[1] = 8
2.CF8 (SQL2005)NEWRECORDID 1row[1] = 8
3.JDBC 1.0 (SQL2000)NEWRECORDID 1row[1] = 9
4.JDBC 1.0 (SQL2005)NEWRECORDID 1row[1] = 9
5.JDBC 1.2 (SQL2000)GENERATED_KEYS 1row[1] = 10
6.JDBC 1.2 (SQL2005)GENERATED_KEYS 1row[1] = 10




So I tried adding a SET NOCOUNT statement to see if that helped. Well, not only did it not help, it made the query disappear. Pretty neat trick ;) Perhaps I should have been more specific about what kind of help I wanted.

Now it has been a long day, so at this point I started dreaming up bad infomercials in my head: "Developers, are you bothered by an overabundance of pesky query objects? Well worry no more. The 'MS JDBC Query Remover 2005' is the solution to your problem! Plus, it is only $9.95 and comes with a free set of ginsu knives!"




















































SQL:
SET NOCOUNT ON

INSERT INTO SomeTable ( Name )

VALUES ( 'Test' )

SELECT SCOPE_IDENTITY() AS NewRecordID

SET NOCOUNT OFF


#dsncolumnListrecord countdata
1.CF8 (SQL2000)NEWRECORDID 1row[1] = 11
2.CF8 (SQL2005)NEWRECORDID 1row[1] = 11
3.JDBC 1.0 (SQL2000)NEWRECORDID 1row[1] = 12
4.JDBC 1.0 (SQL2005)NEWRECORDID 1row[1] = 12
5.JDBC 1.2 (SQL2000)-- 1(query not defined)
6.JDBC 1.2 (SQL2005)-- 1(query not defined)




But wait, it gets better. As I continued to try different things, I decided to try using SET NOCOUNT OFF before running the insert. Well, it did not produce a query object. But it did somehow add the column alias to the "result" structure. So if this were actually a documented behavior, I could access the value as #result.NewRecordID# ;) Of course it is not, so I would not seriously use it. Just another neat driver trick I discovered today ;)

















































SQL:

SET NOCOUNT OFF
INSERT INTO SomeTable ( Name )
VALUES ( 'Test' );
SELECT SCOPE_IDENTITY() AS NewRecordID;

# dsn columnList record count result aliasName
1. CF8 (SQL2000) NEWRECORDID 1 (not defined)
2. CF8 (SQL2005) NEWRECORDID 1 (not defined)
3. JDBC 1.0 (SQL2000) NEWRECORDID 1 (not defined)
4. JDBC 1.0 (SQL2005) NEWRECORDID 1 (not defined)
5. JDBC 1.2 (SQL2000) -- 1 NewRecordID = 207
6. JDBC 1.2 (SQL2005) -- 1 NewRecordID = 240






Are you starting to see why the song I mentioned popped into my head? ;) Now, I am really not certain whether the problem is with the MS driver, with CF's communication with the driver, or both. For kicks, I may run some tests with the jTDS driver. Just to see what tricks ColdFusion + jTDS has up its sleeve. But I will leave that for another day. I think I have reached my limit on query magic for today.

...Read More

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep