Monday, December 31, 2007

Flash from the past - Web safe color palette

This entry is more of reminder for myself. I needed to generate an array of the old hexadecimal web safe colors. (Do not ask me why I had to do this. I would not tell you under threat of torture). Anyway, I am posting this entry as a reference for myself, in case I ever need to do something similar in the future.

Is that it?
About the only interesting thing I can say about the function is it uses the FormatBaseN function to convert the decimal RGB values (0,51,102,153,204,255) to hexadecimal format (00,33,66,99,cc,ff). Then uses nested loops to construct the array of 216 web safe colors.

So without any fanfare, here it is in all its lame glory. Or would that be a contradiction?

Code


<h1>Old Web Safe Palette Example</h1>
<cfset palette = createWebSafePalette()>
<!--- create web safety palette --->
<cfset palette = createWebSafePalette()>
<!--- show web safety palette --->
<table border="1">
<tr>
<cfoutput>
<cfloop from="1" to="#arrayLen(palette)#" index="x">
<td style="width: 75px; background-color: ###palette[x]#">#palette[x]#</td>
<cfif x MOD 9 EQ 0></tr><tr></cfif>
</cfloop>
</tr>
</cfoutput>
</table>

<cffunction name="createWebSafePalette" returntype="array" access="public" output="false"
hint="Returns an array of hexidecimal color values in web safety palette">
<cfset var palette = ArrayNew(1)>
<cfset var hexValues = ArrayNew(1)>
<!--- cfSearching: use old web safe rgb decimal values --->
<cfset var decValues = listToArray("0,51,102,153,204,255")>
<cfset var x = 0>
<cfset var red = 0>
<cfset var green = 0>
<cfset var blue = 0>
<cfset var totalSize = 0>

<cfscript>
// cfSearching: convert decimal values to hex format
for (x = 1; x LTE arrayLen(decValues); x = x + 1) {
// cfSearching: must pad "0" value
if (decValues[x] EQ 0) {
arrayAppend( hexValues, "0"& FormatBaseN( decValues[x], 16) );
}
else {
arrayAppend( hexValues, FormatBaseN( decValues[x], 16) );
}
}

totalSize = arrayLen(hexValues);

// cfSearching: create safety palette colors from hex values (R-G-B)
for (red = 1; red LTE totalSize; red = red + 1) {
for (green = 1; green LTE totalSize; green = green + 1) {
for (blue = 1; blue LTE totalSize; blue = blue + 1) {
arrayAppend( palette, hexValues[red] & hexValues[green] & hexValues[blue] );
}
}
}
</cfscript>

<cfreturn palette>
</cffunction>

...Read More

Create and Resize Completely Transparent GIF's

While working on the last entry about images with rounded corners I happened to do some searching on transparent GIF's. A google search turned up an entry on Ben Nadel's blog that mentioned a possible bug involving completely transparent GIF's. The bug occurs when you resize the totally transparent gif with with ColdFusion and then try to write the image back to the file system. I tested it and sure enough the code threw an exception.


An exception occured while trying to write the image.
Ensure that the destination directory exists and that Coldfusion has permission to write to the given path or file. cause : java.lang.NullPointerException

java.lang.NullPointerException
at com.sun.media.imageioimpl.common.PaletteBuilder.findPaletteEntry(PaletteBuilder.java:349)
...


It does appear to be a bug, but apparently one with the JDK, not ColdFusion: Writing an empty ARGB BufferedImage using GIF Writer throws NullPointerException.


Curious, I decided to investigate whether there was any way to resize and write a completely transparent gif to disk. I did not have much luck with ColdFusion functions or ARGB BufferedImages. But I did find a way to create a 100% transparent gif that would work in a pinch. It seems to work well, but I have not done extensive testing, so use it at your own risk ;)

Where to begin


I first created a transparent color model. To keep it simple the color model supports 'black' only. This was achieved by creating three byte arrays. The byte arrays represent the RGB values for the color black: ie Color(0,0,0) or #000000 in hex.


<!--- cfSearching: note, these color arrays support only 'black' --->
<cfset Local.redArray = javacast("byte[]", listToArray("0,0"))>
<cfset Local.greenArray = javacast("byte[]", listToArray("0,0"))>
<cfset Local.blueArray = javacast("byte[]", listToArray("0,0"))>


Now its worth noting that using a 'black' only color model means all text and shapes added to this image would appear in black only. But this is fine since we only want to create a completely clear gif, not draw on a clear background.

Next the arrays are passed into one of the IndexColorModel constructors.
The first parameter represents the number of bits each pixel occupies. The second is the size of the rbg color arrays. Their sizes should be equal. The last parameter represents the transparency: 0.


<!--- cfSearching: create transparent color model --->
<cfset Local.IndexColorModel = createObject("java", "java.awt.image.IndexColorModel")>
<cfset Local.colorModel = Local.IndexColorModel.init( javacast("int", 1),
arrayLen(Local.redArray),
Local.redArray,
Local.greenArray,
Local.blueArray, 0)>


The color model is then used to create a byte indexed BufferedImage. Finally the BufferedImage is passed to the ImageNew function to return a CF compatible image object.


<cfset Local.img = Local.BufferedImage.init(
javacast("int", arguments.width),
javacast("int", arguments.height),
Local.BufferedImage.TYPE_BYTE_INDEXED,
Local.colorModel)>
<cfreturn ImageNew(Local.img)>


Voila! We now have a completely transparent GIF that can be written to disk by ColdFusion. Bear in mind I am still learning about color models, so any corrections, comments or suggestions are welcome.

Complete Code

<h1>Create Completely Transparent GIF Example </h1>
<!--- cfSearching: initialize image settings --->
<cfset width = javacast("int", 200)>
<cfset height = javacast("int", 200)>
<cfset fileNameOfSavedImage = "newTransparentGif.gif">

<!--- cfSearching: create a totally transparent gif and save it to disk --->
<cfset clear = createClearGif(width, height)>
<cfset ImageWrite(clear, ExpandPath(fileNameOfSavedImage))>

<!--- cfSearching: display the saved gif --->
<cfoutput>
<div style="background: url(#fileNameOfSavedImage#);">
<div style="font-family: verdana,arial;">
<b>Is this image really transparent?</b>
</div>
</div>
</cfoutput>


<cffunction name="createClearGif" returntype="any" access="public" output="false">
<cfargument name="width" type="numeric" required="true">
<cfargument name="height" type="numeric" required="true">

<cfset var Local = structNew()>

<!--- cfSearching: note, these color arrays support only 'black' --->
<cfset Local.redArray = javacast("byte[]", listToArray("0,0"))>
<cfset Local.greenArray = javacast("byte[]", listToArray("0,0"))>
<cfset Local.blueArray = javacast("byte[]", listToArray("0,0"))>

<!--- cfSearching: create transparent color model --->
<cfset Local.IndexColorModel = createObject("java", "java.awt.image.IndexColorModel")>
<cfset Local.colorModel = Local.IndexColorModel.init( javacast("int", 1),
javacast("int", arrayLen(Local.redArray)),
Local.redArray,
Local.greenArray,
Local.blueArray,
javacast("int", 0))>

<!--- cfSearching: create new image using the color model --->
<cfset Local.BufferedImage = createObject("java", "java.awt.image.BufferedImage")>
<cfset Local.img = Local.BufferedImage.init(
javacast("int", arguments.width),
javacast("int", arguments.height),
Local.BufferedImage.TYPE_BYTE_INDEXED,
Local.colorModel)>

<!--- cfSearching: return the image in CF compatible format --->
<cfreturn ImageNew(Local.img)>
</cffunction>

...Read More

Saturday, December 29, 2007

Creating PNG Images with Rounded Corners

I have been working with the image functions in ColdFusion 8 and trying to figure out how to create rounded corners using an existing image.

First let me point out there are some impressive professional products available that can perform this feat, such as Image Effects by Foundeo Inc. Obviously it is more robust and better suited to handling high resolution, high quality images, than the technique described here ;) As my image needs were more basic, I decided to investigate if this were possible using only ColdFusion's built in image functions.

Searching the documentation I found the ImageDrawRoundRect function. It looked promising but it appears to work only for drawing new shapes. What I needed was a way to clip the drawing area of an existing shape. Not having found a suitable combination of CF image functions yet, I decided to try my hand at creating a function that used java directly.

Clipping


My first inclination was to try clipping. Using the same dimensions as the original image, I created a BufferedImage that supports transparency.

<cfscript>
Local.BufferedImage = createObject("java", "java.awt.image.BufferedImage");
Local.destImage = Local.BufferedImage.init(
javacast("int", Local.width),
javacast("int", Local.height),
Local.BufferedImage.TYPE_INT_ARGB
);
</cfscript>


I then used a rounded rectangle to define the clipping area, so anything outside the bounds of the rectangle would appear transparent.
<cfscript>
Local.graphics = Local.destImage.createGraphics();
Local.RoundRectangle2D = createObject("java", "java.awt.geom.RoundRectangle2D$Double");
Local.rounded = Local.RoundRectangle2D.init( javacast("double", 0),
javacast("double", 0),
javacast("double", Local.width),
javacast("double", Local.height),
javacast("double", arguments.arcWidth),
javacast("double", arguments.arcHeight)
);
Local.graphics.setClip( Local.rounded );
</cfscript>


Finally I drew the original image onto the new BufferedImage. Then passed the BufferedImage into the ImageNew() function to return a CF compatible image object.

<cfscript>
Local.graphics.drawImage( Local.sourceImage,
javacast("int", 0),
javacast("int", 0),
javacast("null", "")
);
Local.graphics.dispose();
</cfscript>

<cfreturn ImageNew( Local.destImage )>


As you can see it did work. But if you look closely at the image on the right, the rounded edges appear ragged. At least on the windows o/s.



If at first you don't succeed


Next I tried a tip from the sun.com forums. It suggested using a mask to do the equivalent of clipping, but with better blending of the edges. So this time I used anti-aliasing to draw the rounded rectangle shape first.

Update: This article gives a good explanation of why the mask technique works, as it involves more than just using the anti-aliasing hint.
http://weblogs.java.net/blog/campbell/archive/2006/07/java_2d_tricker.html

<cfscript>
// cfSearching: use anti-aliasing for smoother edges
Local.RenderingHints = createObject("java", "java.awt.RenderingHints");
Local.graphics.setRenderingHint( Local.RenderingHints.KEY_ANTIALIASING,
Local.RenderingHints.VALUE_ANTIALIAS_ON
);
// cfSearching: create a rounded rectangle mask
Local.graphics.setColor( Local.Color.WHITE );
Local.graphics.fillRoundRect(javacast("double", 0),
javacast("double", 0),
javacast("double", Local.width),
javacast("double", Local.height),
javacast("double", arguments.arcWidth),
javacast("double", arguments.arcHeight)
);
</cfscript>


Then used a Source-in AlphaComposite to draw the original image over the rectangle mask. Again the end result being everything outside the rectangle area appears transparent. If you are not familiar with compositing rules, take a peek at one of the sun.com tutorials. The image examples clearly demonstrate the effect of the different AlphaComposite rules.

<cfscript>
// cfSeaching: draw the source image onto the mask
Local.graphics.setComposite( Local.AlphaComposite.SrcIn );
Local.graphics.drawImage( Local.sourceImage,
javacast("int", 0),
javacast("int", 0),
javacast("null", "")
);
Local.graphics.dispose();
</cfscript>


As you can see the resulting corners are much smoother this time.



Conclusion


Now there is obviously much more to handling graphics than I covered here. But hopefully this simple technique demonstrates a few of the things you can do with BufferedImages. As always, comments/corrections/suggestions are welcome. Enjoy!


Clipping Example (Ragged Edges)
<h1>Rounded Corners (Clip) Example</h1>

<cfset fullPathToSourceImage = ExpandPath("testImage3.jpg")>
<!--- display original image --->
<cfoutput>
<img src="#GetFileFromPath(fullPathToSourceImage)#"><br><br>
</cfoutput>

<!--- display new image with rounded corners (arc 50 x 50) --->
<cfset roundedImage = roundedCornersClip( fullPathToSourceImage, 50, 50 )>
<cfimage source="#roundedImage#" action="WriteToBrowser" format="png">


<cffunction name="roundedCornersClip" returntype="any" access="public" output="false">
<cfargument name="source" type="string" required="true" hint="full path to source image">
<cfargument name="arcWidth" type="numeric" required="true" hint="horizontal diameter of arc">
<cfargument name="arcHeight" type="numeric" required="true" hint="vertical diameter of arc">
<cfset var Local = structNew()>

<cfscript>
Local.sourceImage = ImageGetBufferedImage(ImageNew(arguments.source));
Local.width  = Local.sourceImage.getWidth();
Local.height = Local.sourceImage.getHeight();

Local.BufferedImage = createObject("java", "java.awt.image.BufferedImage");
Local.destImage = Local.BufferedImage.init(
javacast("int", Local.width),
javacast("int", Local.height),
Local.BufferedImage.TYPE_INT_ARGB
);

Local.graphics = Local.destImage.createGraphics();
Local.RoundRectangle2D = createObject("java", "java.awt.geom.RoundRectangle2D$Double");
Local.rounded = Local.RoundRectangle2D.init( javacast("double", 0),
javacast("double", 0),
javacast("double", Local.width),
javacast("double", Local.height),
javacast("double", arguments.arcWidth),
javacast("double", arguments.arcHeight)
);

Local.graphics.setClip( Local.rounded );  
Local.graphics.drawImage( Local.sourceImage,
javacast("int", 0),
javacast("int", 0),
javacast("null", "")
);
Local.graphics.dispose();
</cfscript>

<cfreturn ImageNew( Local.destImage )>
</cffunction>


Mask Example (Smoother Edges)
<h1>Rounded Corners (Mask) Example</h1>

<cfset fullPathToSourceImage = ExpandPath("testImage3.jpg")>

<!--- display original image --->
<cfoutput>
<img src="#GetFileFromPath(fullPathToSourceImage)#"><br><br>
</cfoutput>

<!--- display new image with rounded corners (arc 50 x 50) --->
<cfset roundedImage = roundedCornersMask( fullPathToSourceImage, 50, 50 )>
<cfimage source="#roundedImage#" action="WriteToBrowser" format="png">

<cffunction name="roundedCornersMask" returntype="any" access="public" output="false">
<cfargument name="source" type="string" required="true" hint="full path to source image">
<cfargument name="arcWidth" type="numeric" required="true" hint="horizontal diameter of the rounded corners">
<cfargument name="arcHeight" type="numeric" required="true" hint="vertical diameter of the rounded corners">

<cfscript>
var Local = structNew();

// cfSearching: create required java objects
Local.Color = createObject("java", "java.awt.Color");
Local.AlphaComposite = createObject("java", "java.awt.AlphaComposite");

Local.sourceImage = ImageGetBufferedImage(ImageNew(arguments.source));
Local.width  = Local.sourceImage.getWidth();
Local.height = Local.sourceImage.getHeight();

// cfSearching: create a bufferedImage to hold the mask
Local.BufferedImage = createObject("java", "java.awt.image.BufferedImage");
Local.Mask = Local.BufferedImage.init( javacast("int", Local.width),
javacast("int", Local.height),
Local.BufferedImage.TYPE_INT_ARGB
);
Local.graphics = Local.Mask.createGraphics();

// cfSearching: use anti-aliasing for smoother edges
Local.RenderingHints = createObject("java", "java.awt.RenderingHints");
Local.graphics.setRenderingHint( Local.RenderingHints.KEY_ANTIALIASING,
Local.RenderingHints.VALUE_ANTIALIAS_ON
);
// cfSearching: create a rounded rectangle mask
Local.graphics.setColor( Local.Color.WHITE );
Local.graphics.fillRoundRect(javacast("double", 0),
javacast("double", 0),
javacast("double", Local.width),
javacast("double", Local.height),
javacast("double", arguments.arcWidth),
javacast("double", arguments.arcHeight)
);

// cfSeaching: draw the source image onto the mask
Local.graphics.setComposite( Local.AlphaComposite.SrcIn );
Local.graphics.drawImage( Local.sourceImage,
javacast("int", 0),
javacast("int", 0),
javacast("null", "")
);
Local.graphics.dispose();
</cfscript>

<cfreturn ImageNew(Local.Mask)>
</cffunction>


...Read More

Thursday, December 27, 2007

iText FAQ's - Measurements

The iText FAQ's contain a simple but useful entry on measurements. The FAQ's explain the default measurement system roughly corresponds to a typographic or PostScript point. This means if you want to create a rectangle, custom page size, etcetera, you must first convert your measurement units (inches, centimeters, millimeters, etcetera) into points. It is a simple concept, but important if you wish to do any sort of positioning or sizing with iText.

Converting measurement units into points is a common task. So you may wish to create a utility component that contains the common conversion functions like: converting inches to points, centimeters to points, etcetera. You can find the basis for such a utility component below (see MeasurementUtil.cfc).

Now onto a few examples that demonstrate the iText measurement system. The first is a direct translation from the iText tutorial. The second example is similar but it uses one of the U.S. standards PageSize.LETTER.

Documentation: Frequently Asked Questions
Source: Measurements.java

Measurements Example


<h1>Measurements Example</h1>

<cfscript>
savedErrorMessage = "";

fullPathToOutputFile = ExpandPath("./Measurements.pdf");

// step 1: creation of a document-object
pageSize = createObject("java", "com.lowagie.text.Rectangle").init(javacast("float", 288), javacast("float", 720));
document = createObject("java", "com.lowagie.text.Document").init( pageSize,
javacast("float", 36),
javacast("float", 18),
javacast("float", 72),
javacast("float", 72) );

try {
// step 2:
// we 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 = createObject("java", "com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);

// step 3: we open the document
document.open();

// step 4:
//cfSearching: create a single paragraph object and reuse it
paragraph = createObject("java", "com.lowagie.text.Paragraph");

document.add(paragraph.init("The size of this page is 288x720 points."));
document.add(paragraph.init("288pt / 72 points per inch = 4 inch"));
document.add(paragraph.init("720pt / 72 points per inch = 10 inch"));
document.add(paragraph.init("The size of this page is 4x10 inch."));
document.add(paragraph.init("4 inch x 2.54 = 10.16 cm"));
document.add(paragraph.init("10 inch x 2.54 = 25.4 cm"));
document.add(paragraph.init("The size of this page is 10.16x25.4 cm."));
document.add(paragraph.init("The left border is 36pt or 0.5 inch or 1.27 cm"));
document.add(paragraph.init("The right border is 18pt or 0.25 inch or 0.63 cm."));
document.add(paragraph.init("The top and bottom border are 72pt or 1 inch or 2.54 cm."));

//cfSearching:
WriteOutput("Finished!");
}
catch (com.lowagie.text.DocumentException de) {
savedErrorMessage = de;
}
catch (java.io.IOException ioe) {
savedErrorMessage = ioe;
}

// step 5: we close the document
document.close();
</cfscript>


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


Measurements LETTER Page Size Example

<h1>Measurements PageSize.LETTER Example</h1>
** Note, centimeter values are rounded<br><br>

<cfscript>
savedErrorMessage = "";

fullPathToOutputFile = ExpandPath("./MeasurementsLetterSize.pdf");

// cfSearching: margins in points. using 1/2 inch margin on left-right, 1 inch margin on top-bottom
util = createObject("component", "MeasurementUtil");
marginLeft = util.inchesToPoints(0.5);
marginRight = util.inchesToPoints(0.5);
marginTop = util.inchesToPoints(1);
marginBottom = util.inchesToPoints(1);

// cfSearching: create document using pre-defined LETTER size (8.5 x 11 inches)
PageSize = createObject("java", "com.lowagie.text.PageSize");
document = createObject("java", "com.lowagie.text.Document").init( PageSize.LETTER,
marginLeft, marginRight, marginTop, marginBottom );

try {
// step 2:
// we 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 = createObject("java", "com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);

// step 3: we open the document
document.open();

// step 4:
// cfSearching: highlight the document content area
directContent = writer.getDirectContent();

color = createObject("java", "java.awt.Color");
yellow = color.init( javacast("int", 255), javacast("int", 255), javacast("int", 0) );

directContent.setColorFill(yellow);
directContent.rectangle( util.inchesToPoints(0.5),
util.inchesToPoints(1),
util.inchesToPoints(7.5),
util.inchesToPoints(9));
directContent.fill();

// step 5:

PdfContentByte = createObject("java", "com.lowagie.text.pdf.PdfContentByte");
BaseFont = createObject("java", "com.lowagie.text.pdf.BaseFont");
textColor = color.init(javacast("int", 0), javacast("int", 0), javacast("int", 0));
textFont = createObject("java", "com.lowagie.text.FontFactory").getFont(BaseFont.HELVETICA,
BaseFont.WINANSI, BaseFont.NOT_EMBEDDED).getBaseFont();

directContent.beginText();
directContent.setFontAndSize(textFont, javacast("float", 14) );
directContent.setColorFill(textColor);

// cfSearching: show top and bottom margins
inBottom = util.pointsToInches(marginBottom);
cmBottom = decimalFormat(util.pointsToCM(marginBottom));
directContent.moveText( util.inchesToPoints(1.5), util.inchesToPoints(0.5));
directContent.showText( "Top and bottom margins are #marginBottom# pt -> #inBottom# in -> #cmBottom# cm ");

// cfSearching: show left and right margins
inLeft = util.pointsToInches(marginLeft);
cmLeft = decimalFormat(util.pointsToCM(marginLeft));
directContent.moveText( -util.inchesToPoints(1.5) + 15, util.inchesToPoints(5));
directContent.showText( "Left and right margins are "& marginLeft &" pt -> #inLeft# in -> #cmLeft# cm");

inWidth = 8.5;
inHeight = 11;
ptWidth = util.inchesToPoints(inWidth);
ptHeight = util.inchesToPoints(inHeight);
cmWidth = decimalFormat(util.pointsToCM(ptWidth));
cmHeight = decimalFormat(util.pointsToCM(ptHeight));
directContent.moveText( util.inchesToPoints(0.25), util.inchesToPoints(5));
directContent.showText( "The size of this page is #ptWidth#x#ptHeight# points ->"
&" #inWidth#x#inHeight# inches -> #cmWidth#x#cmHeight# centimeters ");

directContent.endText();

WriteOutput("Finished!");
}
catch (com.lowagie.text.DocumentException de) {
savedErrorMessage = de;
}
catch (java.io.IOException ioe) {
savedErrorMessage = ioe;
}

// step 5: we close the document
document.close();
</cfscript>


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


MeasurementUtil.cfc

<cfcomponent>
<cffunction name="inchesToPoints" returntype="numeric" access="public" output="false"
hint="Converts inches to PostScript points">

<cfargument name="inches" type="numeric" required="true">
<cfreturn javacast("float", arguments.inches) * javacast("float", 72)>
</cffunction>

<cffunction name="pointsToInches" returntype="numeric" access="public" output="false"
hint="Converts PostScript points to inches">

<cfargument name="points" type="numeric" required="true">
<cfreturn javacast("float", arguments.points) / javacast("float", 72)>
</cffunction>

<cffunction name="inchesToCM" returntype="numeric" access="public" output="false"
hint="Converts inches to centimeters" >

<cfargument name="inches" type="numeric" required="true">
<cfreturn javacast("float", arguments.inches) * javacast("float", 2.54)>
</cffunction>

<!---
Function pointsToCM based on http://itext.ugent.be/wiki/examples/src/com/lowagie/util/Measurements.java
--->
<cffunction name="pointsToCM" returntype="numeric" access="public" output="false"
hint="Converts PostScript points to centimeters.">

<cfargument name="points" type="numeric" required="true">
<cfreturn (javacast("float", arguments.points) * javacast("float", 2.54)) / javacast("float", 72)>
</cffunction>

<!---
Source http://itext.ugent.be/wiki/examples/src/com/lowagie/util/Measurements.java
--->
<cffunction name="mmToPoints" returntype="numeric" access="public" output="false"
hint="Converts millimeters to PostScript points">

<cfargument name="millimeters" type="numeric" required="true">
<cfreturn (javacast("float", arguments.millimeters) * javacast("float", 7.2)) / javacast("float", 2.54)>
</cffunction>

<!---
Function cmToPoints based on http://itext.ugent.be/wiki/examples/src/com/lowagie/util/Measurements.java
--->
<cffunction name="cmToPoints" returntype="numeric" access="public" output="false"
hint="Converts centimeters to PostScript points">

<cfargument name="centimeters" type="numeric" required="true">
<cfreturn (javacast("float", arguments.centimeters) * javacast("float", 72)) / javacast("float", 2.54)>
</cffunction>
</cfcomponent>

...Read More

Tuesday, December 25, 2007

Using FFMPEG to convert video files to FLV format

While trying to figure out how to convert video clips to FLV format I found a blog entry that mentions you can use FFMPEG and the java Runtime library to do this. If you are not familiar with FFMPEG their documentation describes it as a "very fast video and audio converter".

The blog entry function worked well with small files, but attempting to convert larger files seemed to cause the process to hang. I ran the code several times, waiting a few minutes in between each attempt. But each time the processes went out into never-never-land and .. well, never came back. I checked the Task Manager afterward and it showed several orphaned ffmpeg.exe processes. Finally I resorted to bouncing the CF Server. That did kill the orphaned processes. But I was still puzzled about why I could successfully convert the same files using ffmpeg.exe on the command line but not using Runtime.exec().

Finally I found two articles that helped explain the problem. The first entry on http://blog.taragana.com/ explains that failure to drain the input/output streams of the subprocess "may cause the subprocess to block, and even deadlock." The source of this information is an old but excellent article on www.javaworld.com . It provides a more in-depth explanation of the issue and describes how to properly use Runtime.exec().

Armed with these three references, I combined all of the suggestions and it seems to have resolved the problem. Obviously this code belongs in a function, but the sample below should give the general idea. Disclaimer: Bear in mind this is the first time I have used ffmpeg.exe and Runtime.exec() so use it at your own risk ;)


UPDATE: In my testing ffmpeg.exe appears to write to a single stream. For programs that write to both the output and error stream you need to process the streams in seperate threads to prevent blocking or deadlocks. See Runtime.exec(), mencoder and cfthread for an example.



<!--- test file paths --->
<cfset ffmpegPath = "c:\bin\ffmpeg.exe">
<cfset inputFilePath = "c:\bin\testInput.mp4">
<cfset ouputFilePath = "c:\bin\testOuput.flv">
<cfset resultLog = "c:\bin\testOuput_result.log">
<cfset errorLog = "c:\bin\testOuput_error.log">

<!--- convert the file --->
<cfset results = structNew()>
<cfscript>
try {
runtime = createObject("java", "java.lang.Runtime").getRuntime();
command = '#ffmpegPath# -i "#inputFilePath#" -g 300 -y -s 300x200 -f flv -ar 44100 "#ouputFilePath#"';
process = runtime.exec(#command#);
results.errorLogSuccess = processStream(process.getErrorStream(), errorLog);
results.resultLogSuccess = processStream(process.getInputStream(), resultLog);
results.exitCode = process.waitFor();
}
catch(exception e) {
results.status = e;
}
</cfscript>

<!--- display the results --->
<cfdump var="#results#">


<!--- function used to drain the input/output streams. Optionally write the stream to a file --->
<cffunction name="processStream" access="public" output="false" returntype="boolean" hint="Returns true if stream was successfully processed">
<cfargument name="in" type="any" required="true" hint="java.io.InputStream object">
<cfargument name="logPath" type="string" required="false" default="" hint="Full path to LogFile">
<cfset var out = "">
<cfset var writer = "">
<cfset var reader = "">
<cfset var buffered = "">
<cfset var line = "">
<cfset var sendToFile = false>
<cfset var errorFound = false>

<cfscript>
if ( len(trim(arguments.logPath)) ) {
out = createObject("java", "java.io.FileOutputStream").init(arguments.logPath);
writer = createObject("java", "java.io.PrintWriter").init(out);
sendToFile = true;
}

reader = createObject("java", "java.io.InputStreamReader").init(arguments.in);
buffered = createObject("java", "java.io.BufferedReader").init(reader);
line = buffered.readLine();
while ( IsDefined("line") ) {
if (sendToFile) {
writer.println(line);
}
line = buffered.readLine();
}
if (sendToFile) {
errorFound = writer.checkError();
writer.flush();
writer.close();
}
</cfscript>
<!--- return true if no errors found. --->
<cfreturn (NOT errorFound)>
</cffunction>


If you are interested in testing this example, all you should need is

1. The ffmpeg binary. You can download a version at sourceforge.net

2. A sample video file

In my travels I read a tip from techrepulic.com that mentions a simple program called FLV player. The program allows you to view .flv files on a hard drive. While not the only way to view .flv files, it is a handy program.

As always, comments/corrections/suggestions are welcome. Enjoy!

...Read More

Sunday, December 23, 2007

Getting started with iText - Part 18 (Concatenating PDF Forms)

In Part 18 we translate the ConcatenateForms.java example. It is an extremely simple example that concatenates multiple PDF forms. You can run this example with MX7 or CF8 and theoretically MX6 (if you have installed iText). Of course if you are running CF8 you should use the new cfpdf tag for merging PDF files and forms instead. A CF8 only example is included at the end for comparison purposes.

*Groan* Do I have to RTM?


Truth be told some of us have an unfortunate tendency to skim articles, and ignore the referenced links, so we can jump to the interesting part: the code. (Now when I say "us" .. I do not mean myself of course.. ;) This entry is a good example of how not reading the documentation can burn you.

Now the iText tutorial is a bit old, but it does mention two important things about the PdfCopyFields object used in this example. First it states the PdfCopyFields class keeps all documents in memory unlike PdfCopy. That is an important distinction. Second it notes that if you have fields with the same name they will be merged so, it's probably a good idea to rename them if that's the case.

As a test, I created two copies of the SimpleRegistrationForm.pdf file. Then used the sample code to concatenate the two files. Because fields with the same name are merged, typing a value into one of the fields, such as "name", effects all of the fields with that same name. Since the process takes the separate forms and merges them into one AcroForm, this behavior makes sense. But I think it is worth emphasizing so this behavior does not take you by surprise. Note, using CF8's cfpdf tag to merge forms produces the same results.

Now for the good part ..



What you will need for this example


Download the following (2) files from the iText site and place them in the same directory as your .cfm script


Code


Documentation: Manipulating existing PDF documents
Source: ConcatenateForms.java

iText Example

<h1>Concatenate PDF Forms example</h1>
Concatenates 2 PDF files with forms. The resulting PDF has 1 merged AcroForm.<br>
<cfscript>
savedErrorMessage = "";

// cfSearching: All file paths are relative to the current directory
fullPathToOutputFile = ExpandPath("./ConcatenatedForms.pdf");
// cfSearching: Using an array of paths here instead of separate variables for each file
arrayOfInputFiles = arrayNew(1);
arrayAppend(arrayOfInputFiles, ExpandPath("./SimpleRegistrationForm.pdf"));
arrayAppend(arrayOfInputFiles, ExpandPath("./TextFields.pdf"));

try {

outStream = createObject("java", "java.io.FileOutputStream").init( fullPathToOutputFile );
copy = createObject("java", "com.lowagie.text.pdf.PdfCopyFields").init(outStream);
PdfReader = createObject("java", "com.lowagie.text.pdf.PdfReader");

for (fileIndex = 1; fileIndex LTE arrayLen(arrayOfInputFiles); fileIndex = fileIndex + 1) {
reader = PdfReader.init( arrayOfInputFiles[fileIndex] );
copy.addDocument(reader);
}

WriteOutput("Finished!");
}
catch (java.language.Exception de) {
savedErrorMessage = de;
}
// cfSearching: close iText and output stream objects
if (IsDefined("copy")) {
copy.close();
}
if (IsDefined("outputStream")) {
outputStream.close();
}
</cfscript>


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


ColdFusion 8 example

<h1>Concatenate PDF Forms example (ColdFusion 8)</h1>

<cfset listOfInputFiles = ExpandPath("./SimpleRegistrationForm.pdf")>
<cfset listOfInputFiles = listAppend(listOfInputFiles, ExpandPath("./TextFields.pdf"))>
<cfset fullPathToOutputFile = ExpandPath("./ConcatenatedForms.pdf")>

<cfpdf action="merge"
source="#listOfInputFiles#"
destination="#fullPathToOutputFile#"
keepbookmark="true"
overwrite="true">

...Read More

Saturday, December 22, 2007

Getting started with iText - Part 17 (Concatenating PDF Files)

In Part 17, we translate the Concatenate.java example. As usual the title is a spoiler: the example demonstrates how to concatenate (or merge) multiple PDF files.


You can run this example with MX7 or CF8. Theoretically it should work with MX6 as well if you have installed an iText jar in the CF classpath. It is worth noting here that ColdFusion 8 provides the cfpdf tag which can merge PDF files. I have included a ColdFusion 8 example for comparison purposes.

What you will need for this example


Download the following (3) files from the iText site and place them in the same directory as your .cfm script


Code


Documentation: Manipulating existing PDF documents
Source: Concatenate.java

iText Example

<h1>Concatenate (Merge) PDF files example</h1>
<cfscript>
savedErrorMessage = "";

// cfSearching: All file paths are relative to the current directory
fullPathToOutputFile = ExpandPath("./Concatenated.pdf");
arrayOfInputFiles = arrayNew(1);
arrayAppend(arrayOfInputFiles, ExpandPath("./ChapterSection.pdf"));
arrayAppend(arrayOfInputFiles, ExpandPath("./Destinations.pdf"));
arrayAppend(arrayOfInputFiles, ExpandPath("./SimpleAnnotations1.pdf"));

try {

pageOffset = 0;
PdfReader = createObject("java", "com.lowagie.text.pdf.PdfReader");
SimpleBookmark = createObject("java", "com.lowagie.text.pdf.SimpleBookmark");
// cfSearching: Internally CF stores arrays as Vectors. So I chose to use an explict vector
// cfSearching: here, but you could use an array and CF array functions instead
allBookmarks = createObject("java", "java.util.Vector");

for ( fileIndex = 1; fileIndex LTE arrayLen(arrayOfInputFiles); fileIndex = fileIndex + 1) {
// we create a reader for a certain document
reader = pdfReader.init( arrayOfInputFiles[fileIndex] );
reader.consolidateNamedDestinations();
// we retrieve the total number of pages
totalPages = reader.getNumberOfPages();
bookmarks = SimpleBookmark.getBookmark(reader);
if (IsDefined("bookmarks")) {
if (pageOffset neq 0) {
SimpleBookmark.shiftPageNumbers(bookmarks, javacast("int", pageOffset), javacast("null", 0));
}
allBookmarks.addAll(bookmarks);
}
pageOffset = pageOffset + totalPages;

if (fileIndex EQ 1) {
// step 1: creation of a document-object
document = createObject("java", "com.lowagie.text.Document");
document = document.init( reader.getPageSizeWithRotation( javacast("int", 1)) );
// step 2: we create a writer that listens to the document
outStream = createObject("java", "java.io.FileOutputStream").init( fullPathToOutputFile );
pdfWriter = createObject("java", "com.lowagie.text.pdf.PdfCopy").init(document, outStream);
// step 3: we open the document
document.open();
}
// step 4: we add content
for (pageIndex = 1; pageIndex LTE totalPages; pageIndex = pageIndex + 1) {
page = pdfWriter.getImportedPage(reader, javacast("int", pageIndex) );
pdfWriter.addPage(page);
}

formFields = reader.getAcroForm();
if (IsDefined("formFields")) {
pdfWriter.copyAcroForm(reader);
}
}

if (NOT allBookmarks.isEmpty()) {
pdfWriter.setOutlines( allBookmarks );
}
// step 5: we close the document
document.close();

WriteOutput("Finished!");
}
catch (java.language.Exception de) {
savedErrorMessage = de;
}
// cfSearching: close document and output stream objects
if (IsDefined("document")) {
document.close();
}
if (IsDefined("outputStream")) {
outputStream.close();
}
</cfscript>


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


ColdFusion 8 example

<h1>ColdFusion 8 Merge PDF files example</h1>

<cfset listOfInputFiles = ExpandPath("./ChapterSection.pdf")>
<cfset listOfInputFiles = listAppend(listOfInputFiles, ExpandPath("./Destinations.pdf"))>
<cfset listOfInputFiles = listAppend(listOfInputFiles, ExpandPath("./SimpleAnnotations1.pdf"))>
<cfset fullPathToOutputFile = ExpandPath("./Concatenated.pdf")>

<cfpdf action="merge"
source="#listOfInputFiles#"
destination="#fullPathToOutputFile#"
keepbookmark="true"
overwrite="true">

Finished!

...Read More

Getting started with iText - Part 16 (EncryptorExample.java)

In Part 16 of Getting Started with iText we will translate the EncryptorExample.java example. This example demonstrates how to encrypt an existing PDF file.

The java example calls a single method in the PdfEncryptor helper class to perform the encryption. You can read more about the parameters for this function (specifically permissions) in the iText online tutorial.

First a few words about deprecation and encryption


1. While you can run this example "as is" under MX7 or CF8, it does use two deprecated constants when setting permissions. According to the API PdfWriter.AllowCopy and PdfWriter.AllowPrinting are deprecated and "scheduled for removal at or after 2.2.0." For that reason you may choose to use a newer version of iText like 2.0.7+ and use ALLOW_COPY and ALLOW_PRINTING instead.

2. The tutorial states that an additional jar is required to run the example: bcprov-jdk14-138.jar. The sample code below seems to work with MX7 and CF8 without the use of any additional jars. Since MX7 uses iText for the cfdocument tag, which provides a 40-bit and 128-bit encryption option, I can only assume the necessary encryption classes are built-into CF. Though I do not know which classes are used to perform the encryption. If anyone has any insights on this topic, feel free to leave a comment.


What you will need for this example


A sample PDF from the iText site: ChapterSection.pdf. Download the file and place it in the same directory as your .cfm script.


Code


Documentation: Manipulating existing PDF documents
Source: EncryptorExample.java

MX7/CF8 Compatible Code

<h1>Encrypt an existing PDF Example</h1>
<cfscript>
savedErrorMessage = "";

// cfSearching: All file paths are relative to the current directory
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToOutputFile = ExpandPath("./Encrypted.pdf");
userPassword = "Hello";
ownerPassword = "World";
//cfSearching: If true, use 128-bit key length. Otherwise, use 40-bit key
useStrength128Bits = true;

try {
string = createObject("java", "java.lang.String");
pdfReader = createObject("java", "com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
pdfWriter = createObject("java", "com.lowagie.text.pdf.PdfWriter");
pdfEncryptor = createObject("java", "com.lowagie.text.pdf.PdfEncryptor");
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
//cfSearching: This example uses deprecated constants. See the API for more information
//cfSearching: http://itext.ugent.be/library/api/com/lowagie/text/pdf/PdfWriter.html#AllowCopy
pdfEncryptor.encrypt( pdfReader,
outStream,
javacast("string", userPassword).getBytes(),
javacast("string", ownerPassword).getBytes(),
BitOr(PdfWriter.AllowCopy , PdfWriter.AllowPrinting),
useStrength128Bits);

WriteOutput("Finished!");
}
catch (java.language.Exception de) {
savedErrorMessage = de;
}
// cfSearching: close output stream object
if (IsDefined("outputStream")) {
outputStream.close();
}
</cfscript>


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


Using JavaLoader and iText 2.0.7+ Code
Note, the code below uses an instance of the javaLoader stored in server scope as explained here

<h1>Encrypt an existing PDF Example</h1>
<cfscript>
savedErrorMessage = "";

// cfSearching: All file paths are relative to the current directory
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToOutputFile = ExpandPath("./Encrypted.pdf");
userPassword = "Hello";
ownerPassword = "World";
//cfSearching: If true, uses 128-bit key length. Otherwise, uses 40-bit key
useStrength128Bits = true;

//cfSearching: uses an instance of javaloader stored in server scope
javaLoader = server[MyUniqueKeyForJavaLoader];

try {
string = createObject("java", "java.lang.String");
pdfReader = javaLoader.create("com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
pdfWriter = javaLoader.create("com.lowagie.text.pdf.PdfWriter");
pdfEncryptor = javaLoader.create("com.lowagie.text.pdf.PdfEncryptor");
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
//cfSearching: This example uses new constants available in iText 2.0.7+
pdfEncryptor.encrypt( pdfReader,
outStream,
javacast("string", userPassword).getBytes(),
javacast("string", ownerPassword).getBytes(),
BitOr(PdfWriter.AllowCopy , PdfWriter.AllowPrinting),
useStrength128Bits);

WriteOutput("Finished!");
}
catch (java.language.Exception de) {
savedErrorMessage = de;
}
// cfSearching: close output stream object
if (IsDefined("outputStream")) {
outputStream.close();
}
</cfscript>


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

...Read More

Getting started with iText - Part 15 (Register.java)

The next installation in Getting started with iText translates the Register example. It is a simple example that demonstrates how to fill in form fields with iText.

You can run the example "as is" with either MX7 or CF8. Though if you are using CF8 you could use the new cfpdf and cfpdfform tags instead. From what I understand CF8 does make use of iText behind the scenes for some of the PDF functionality. Though I do not know to what extent. For comparison purposes I have also included a CF8 example below.

The iText example fills in a form and creates two output files. The only difference between them is that one of the output files is flattened. Meaning the form field values can no longer be modified.

What you will need for this example


A sample form from the iText site: SimpleRegistrationForm.pdf. Download the file and place it in the same directory as your .cfm script.

Code


Documentation: Manipulating existing PDF documents
Source: Register.java

iText Example

<h1>Fill in Registration Form Fields Example</h1>
<cfscript>
savedErrorMessage = "";

// cfSearching: All file paths are relative to the current directory
fullPathToInputFile = ExpandPath("./SimpleRegistrationForm.pdf");
fullPathToOutputFile1 = ExpandPath("./Registered.pdf");
fullPathToOutputFile2 = ExpandPath("./Registered_Flat.pdf");

try {
// we create a reader for a certain document
pdfReader1 = createObject("java", "com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
//cfSearching: fill in the form fields but do not flatten the form
outputStream1 = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile1);
pdfStamper1 = createObject("java", "com.lowagie.text.pdf.PdfStamper").init(pdfReader1, outputStream1);
form1 = pdfStamper1.getAcroFields();
form1.setField("name", "Bruno Lowagie");
form1.setField("address", "Baeyensstraat 121, Sint-Amandsberg");
form1.setField("postal_code", "BE-9040");
form1.setField("email", "bruno@lowagie.com");

//cfSearching: fill in the form fields AND flatten the form
pdfReader2 = createObject("java", "com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
outputStream2 = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile2);
pdfStamper2 = createObject("java", "com.lowagie.text.pdf.PdfStamper").init(pdfReader2, outputStream2);
form2 = pdfStamper2.getAcroFields();
form2.setField("name", "Bruno Lowagie");
form2.setField("address", "Baeyensstraat 121, Sint-Amandsberg");
form2.setField("postal_code", "BE-9040");
form2.setField("email", "bruno@lowagie.com");
pdfStamper2.setFormFlattening(true);

WriteOutput("Finished!");

}
catch (java.language.Exception de) {
savedErrorMessage = de;
}
// cfSearching: close the stamper and output stream objects
pdfStamper1.close();
outputStream1.close();
pdfStamper2.close();
outputStream2.close();
</cfscript>


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


ColdFusion 8 Example

<h1>Fill in Registration Form Fields CF8 Example</h1>

<cfset fullPathToInputFile = ExpandPath("./SimpleRegistrationForm.pdf")>
<cfset fullPathToOutputFile1 = ExpandPath("./Registered.pdf")>
<cfset fullPathToOutputFile2 = ExpandPath("./Registered_Flat.pdf")>

<!--- fill in the form fields. will NOT overwrite existing file ---->
<cfpdfform action="populate" source="#fullPathToInputFile#" destination="#fullPathToOutputFile1#">
<cfpdfformparam name="name" value="Bruno Lowagie">
<cfpdfformparam name="address" value="Baeyensstraat 121, Sint-Amandsberg">
<cfpdfformparam name="postal_code" value="BE-9040">
<cfpdfformparam name="email" value="bruno@lowagie.com">
</cfpdfform>

<!--- use completed form created in previous step and flatten it ---->
<cfpdf action="write" source="#fullPathToOutputFile1#"
destination="#fullPathToOutputFile2#"
flatten="true" >
Finished!

...Read More

Thursday, December 20, 2007

Understanding the iText BarcodeInter25 method

One of the cooler aspects of the Creating a wicked Film Festival VIP Pass with iText example was the use of bar codes. Not being familiar with them, or the BarcodeInter25 method used in the example, I did some searching. Thanks to wikipedia and barcode-1.net I discovered that the iText method generates a bar code in a standard format known as Interleaved 2 of 5.

Inter-whaaat?


The Interleaved 2 of 5 format is used to encode pairs of numbers using a set of bars. Each number, 0 through 9, is represented by a set of 5 bars. The number zero is represented by NNWWN, where "N" is a narrow bar and "W" is a wide bar.


0 NNWWN
1 WNNNW
2 NWNNW
3 WWNNN
4 NNWNW
5 WNWNN
6 NWWNN
7 NNNWW
8 WNNWN
9 NWNWN

The term interleaved means that two numbers are encoded in a set of 5 bars and the spaces in between those bars. The first number is represented by the black bars. The second number is represented by the spaces in between the bars. This bar code format begins with a start code NNNN and ends with a stop code WNN.

Because I am curious *cough* geek *cough* , I created a simple bar code to test this information. As you can see this is the bar code I generated.



After counting the bars things seemed to match up, but then I remembered the descriptions said this format encodes pairs of numbers. My sample number contained 7 digits, so why did it work? Then I remembered the sample code adds a checksum digit. So my 7 digit number plus the 1 digit checksum produces an even number of digits to encode.

How is the checksum calculated


According to barcode-1.net this format uses the modulo 10 scheme to calculate the checksum. So I took my sample number and calculated what the checksum digit should be.

My base number is: 0012345

1. Add the digits in odd numbered positions: 0 + 1 + 3 + 5 == 9
2. Multiply the result by three: 9 x 3 == 27
3. Add the digits in even numbered positions: 0 + 2 + 4 == 6
4. Add the two results together: 6 + 27 == 33
5. Calculate modulo: 33 mod 10 == 3
6. Subtract the modulo from ten: 10 - 3 == 7

The checksum for this number is: 7

I checked it against my sample bar code and it matched.



There are additional rules about the dimensions of the bars, but I will leave you to read more about it on your own. If you would like to generate a sample bar code PDF in ColdFusion, see the code below.

As usual these ramblings are based on my own unofficial reading, testing and observations. So corrections/questions/suggestions are welcome ;)

ColdFusion Code

<h1>Simple Bar Code Example</h1>

<cfscript>
savedErrorMessage = "";

// cfSearching: All file paths are relative to the current directory
fullPathToOutputFile = ExpandPath("./SimpleBarCodeExample.pdf");
fullPathToITextJar = ExpandPath("./iText-2.0.7.jar");
dotNotationPathToJavaLoader = "javaloader.JavaLoader";
barCodeNumber = "0012345";

// cfSearching: get an instance of the javaloader
pathsForJavaLoader = arrayNew(1);
arrayAppend(pathsForJavaLoader, fullPathToITextJar);
javaLoader = createObject("component", dotNotationPathToJavaLoader).init(pathsForJavaLoader);

// cfSearching: create color and font used for drawing text
color = createObject("java", "java.awt.Color");
BLACK = color.init(javacast("int", 0), javacast("int", 0), javacast("int", 0) );
BaseFont = javaLoader.create("com.lowagie.text.pdf.BaseFont");
FONT = javaLoader.create("com.lowagie.text.FontFactory").getFont(BaseFont.HELVETICA,
BaseFont.WINANSI, BaseFont.NOT_EMBEDDED).getBaseFont();

try {
// cfSearching: create a document with a custom page size 4 inches x 3 inches
pageDimensions = javaLoader.create("com.lowagie.text.Rectangle").init( inchesToPoints(4), inchesToPoints(3));
document = javaLoader.create("com.lowagie.text.Document").init(pageDimensions);

// we 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
document.open();
// step 4
directContent = writer.getDirectContent();

directContent.beginText();
directContent.moveText(inchesToPoints(0.25), HEIGHT - inchesToPoints(0.75));
directContent.setFontAndSize(FONT, javacast("float", 16) );
directContent.setColorFill(BLACK);
directContent.showText("Barcode number: "& barCodeNumber);
directContent.endText();

Rectangle = javaLoader.create("com.lowagie.text.Rectangle");
PushbuttonField = javaLoader.create("com.lowagie.text.pdf.PushbuttonField");
barcode = PushbuttonField.init(writer, rectangle.init( inchesToPoints(0.25),
inchesToPoints(1),
WIDTH - inchesToPoints(0.25),
HEIGHT - inchesToPoints(1)),
"barcode");


// cfSearching: generate the barcode image. does nothing if code is invalid
try {
code = javaLoader.create("com.lowagie.text.pdf.BarcodeInter25").init();
code.setGenerateChecksum(true);
code.setBarHeight( inchesToPoints(1) );

code.setCode( javacast("string", barCodeNumber) );
code.setFont( javacast("null", "") );
cb = javaLoader.create("com.lowagie.text.pdf.PdfContentByte").init(writer);
template = code.createTemplateWithBarcode(cb, javacast("null", ""), javacast("null", ""));
barcode.setLayout(PushbuttonField.LAYOUT_ICON_ONLY);
barcode.setProportionalIcon(false);
barcode.setTemplate(template);
writer.addAnnotation(barcode.getField());

} catch (java.lang.Exception e) {
// not a valid code, do nothing
}

}
catch (com.lowagie.text.DocumentException de) {
savedErrorMessage = de;
}
catch (java.io.IOException ioe) {
savedErrorMessage = ioe;
}

// step 4
document.close();
outStream.close();
</cfscript>

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

<cffunction name="inchesToPoints" returntype="numeric" access="public" output="false"
hint="Converts inches to Postscript points">
<cfargument name="inches" type="numeric" required="true">
<cfreturn javacast("float", (arguments.inches * 72)) >
</cffunction>

...Read More

Getting started with iText - Part 14 (AddWatermarkPageNumbers.java)

The next installation in Getting started with iText translates the AddWatermarkPageNumbers example. As it's name implies, it demonstrates how to add watermarks and page numbers to an existing PDF file.

This example uses newer methods that are not available in the MX7/CF8 iText jar. So once again we will use Mark Mandel's JavaLoader.cfc to load the iText 2.0.7 jar.

What you will need for this example


1. A recent version of iText like 2.0.7. For instructions, see How to use a newer version of iText

2. Three (3) input files from the iText site. Download the sample files and place them in the same directory as your .cfm script: ChapterSection.pdf  SimpleAnnotations1.pdf  watermark.jpg

Getting started



As usual we start by initializing a few variables with the file paths used in this example. Then we get an instance of the JavaLoader.cfc for creating our iText objects. Next we create a PdfReader object that will read in our original PDF file. We then create a PdfStamper object that will be used to copy the original into a new PDF file.


UPDATE: To avoid confusion the attached code sample was updated to use an instance of the javaLoader stored in the server scope. This is necessary to prevent memory leaks as mentioned in the instructions.



<cfscript>
// cfSearching: by default all files are in current directory. change as needed
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToWatermark = ExpandPath("./watermark.jpg");
fullPathToAnnotations = ExpandPath("./SimpleAnnotations1.pdf");
fullPathToOutputFile = ExpandPath("./Watermark_Pagenumbers.pdf");

/*
cfSearching: This example was updated to use an instance of
the javaLoader that is stored in the server scope. This prevents
memory leaks due to a bug in ColdFusion.

"Using a Java URLClassLoader in CFMX Can Cause a Memory Leak"
http://www.compoundtheory.com/?action=displayPost&ID=212
*/
javaLoader = server[YourUniqueKeyForTheJavaLoader];

// we create a reader for a certain document
pdfReader = javaLoader.create("com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
totalPages = pdfReader.getNumberOfPages();

// we create a stamper that will copy the document to a new file
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
pdfStamper = javaLoader.create("com.lowagie.text.pdf.PdfStamper").init(pdfReader, outStream);
</cfscript>


To make things interesting we add a bit of metadata to the new PDF file. We then load the watermark image and center it within the page using the setAbsolutePosition method. Next we create a BaseFont object that will be used for drawing the page numbers and watermark text.


<cfscript>
// adding some metadata
moreInfo = createObject("java", "java.util.HashMap").init();
moreInfo.put("Author", "Bruno Lowagie");
pdfStamper.setMoreInfo(moreInfo);

// cfSearching: create objects that will be used to in our loop to add content to each page
img = javaLoader.create("com.lowagie.text.Image").getInstance(fullPathToWatermark);
img.setAbsolutePosition( javacast("float", 200), javacast("float", 400) );
BaseFont = javaLoader.create("com.lowagie.text.pdf.BaseFont");
bf = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.EMBEDDED);
Element = javaLoader.create("com.lowagie.text.Element");
PageSize = javaLoader.create("com.lowagie.text.PageSize");
</cfscript>


Using a while loop we stamp each page with the watermark image. You may notice the watermark is added to the underContent layer or beneath the existing text. We then stamp the word "DUPLICATE" diagonally across the watermark image. But this time using the overContent layer, or over the existing text layer. Still using the overContent layer we draw the page number at the bottom of the current page. You can read more about layers in the iText documentation.


<cfscript>
// adding content to each page
i = 0;
while (i LT totalPages) {
i = i + 1;

// watermark under the existing page
under = pdfStamper.getUnderContent( javacast("int", i) );
under.addImage(img);

// cfSearching: draw page number text over the existing page
over = pdfStamper.getOverContent( javacast("int", i) );
over.beginText();
over.setFontAndSize(bf, javacast("float", 18) );
over.setTextMatrix( javacast("float", 30), javacast("float", 30) );
over.showText("page " & i);
// cfSearching: draw the word "DUPLICATE" across the watermark image
// cfSearching: over the existing page content
over.setFontAndSize(bf, javacast("float", 32) );
over.showTextAligned(Element.ALIGN_LEFT, javacast("string", "DUPLICATE"),
javacast("float", 230),
javacast("float", 430),
javacast("float", 45) );
over.endText();
}
</cfscript>


Next we insert a new page at the beginning of our new PDF. Think of it as a cover page. The words "Duplicate of Existing PDF Document" are then written across the overContent. Finally we copy the content from another document and add it to the underContent layer. Why? To demonstrate we have merged the contents from two PDF files I suppose.


<cfscript>
// adding an extra page
pdfStamper.insertPage( javacast("int", 1), PageSize.A4);
over = pdfStamper.getOverContent(javacast("int", 1));
over.beginText();
over.setFontAndSize(bf, javacast("float", 18) );
over.showTextAligned(Element.ALIGN_LEFT, javacast("string", "DUPLICATE OF AN EXISTING PDF DOCUMENT"),
javacast("float", 30),
javacast("float", 600),
javacast("float", 0) );
over.endText();
// adding a page from another document
reader2 = javaLoader.create("com.lowagie.text.pdf.PdfReader").init( fullPathToAnnotations );
under = pdfStamper.getUnderContent( javacast("int", 1));
under.addTemplate(pdfStamper.getImportedPage(reader2, javacast("int", 3)),
javacast("float", 1),
javacast("float", 0),
javacast("float", 0),
javacast("float", 1),
javacast("float", 0),
javacast("float", 0) );
</cfscript>


All that is left is to close the PdfStamper object and our new PDF will be created. Well that is it for now. It was interesting learning how to draw an image or text on the different layers. Until next time.

Documentation: Manipulating existing PDF documents
Source: AddWatermarkPageNumbers.java


Complete Code

<h1>Add Watermark and Page Numbers Example</h1>
<cfscript>
savedErrorMessage = "";

// cfSearching: by default all files are in current directory. change as needed
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToWatermark = ExpandPath("./watermark.jpg");
fullPathToAnnotations = ExpandPath("./SimpleAnnotations1.pdf");
fullPathToOutputFile = ExpandPath("./Watermark_Pagenumbers.pdf");

/*
cfSearching: This example was updated to use an instance of
the javaLoader that is stored in the server scope. This prevents
memory leaks due to a bug in ColdFusion.

"Using a Java URLClassLoader in CFMX Can Cause a Memory Leak"
http://www.compoundtheory.com/?action=displayPost&ID=212
*/
javaLoader = server[YourUniqueKeyForTheJavaLoader];

try {
// we create a reader for a certain document
pdfReader = javaLoader.create("com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
totalPages = pdfReader.getNumberOfPages();

// we create a stamper that will copy the document to a new file
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
pdfStamper = javaLoader.create("com.lowagie.text.pdf.PdfStamper").init(pdfReader, outStream);

// adding some metadata
moreInfo = createObject("java", "java.util.HashMap").init();
moreInfo.put("Author", "Bruno Lowagie");
pdfStamper.setMoreInfo(moreInfo);

// cfSearching: create objects that will be used to in our loop to add content to each page
img = javaLoader.create("com.lowagie.text.Image").getInstance(fullPathToWatermark);
img.setAbsolutePosition( javacast("float", 200), javacast("float", 400) );
BaseFont = javaLoader.create("com.lowagie.text.pdf.BaseFont");
bf = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.EMBEDDED);
Element = javaLoader.create("com.lowagie.text.Element");
PageSize = javaLoader.create("com.lowagie.text.PageSize");

// adding content to each page
i = 0;
while (i LT totalPages) {
i = i + 1;

// watermark under the existing page
under = pdfStamper.getUnderContent( javacast("int", i) );
under.addImage(img);
// text over the existing page
over = pdfStamper.getOverContent( javacast("int", i) );
over.beginText();
over.setFontAndSize(bf, javacast("float", 18) );
over.setTextMatrix( javacast("float", 30), javacast("float", 30) );
over.showText("page " & i);
over.setFontAndSize(bf, javacast("float", 32) );
over.showTextAligned(Element.ALIGN_LEFT, javacast("string", "DUPLICATE"),
javacast("float", 230),
javacast("float", 430),
javacast("float", 45) );
over.endText();
}

// adding an extra page
pdfStamper.insertPage( javacast("int", 1), PageSize.A4);
over = pdfStamper.getOverContent(javacast("int", 1));
over.beginText();
over.setFontAndSize(bf, javacast("float", 18) );
over.showTextAligned(Element.ALIGN_LEFT, javacast("string", "DUPLICATE OF AN EXISTING PDF DOCUMENT"),
javacast("float", 30),
javacast("float", 600),
javacast("float", 0) );
over.endText();
// adding a page from another document
reader2 = javaLoader.create("com.lowagie.text.pdf.PdfReader").init( fullPathToAnnotations );
under = pdfStamper.getUnderContent( javacast("int", 1));
under.addTemplate(pdfStamper.getImportedPage(reader2, javacast("int", 3)),
javacast("float", 1),
javacast("float", 0),
javacast("float", 0),
javacast("float", 1),
javacast("float", 0),
javacast("float", 0) );

WriteOutput("Finished!");
}
catch (java.lang.Exception e) {
savedErrorMessage = e;
}
// closing PdfStamper will generate the new PDF file
if (IsDefined("pdfStamper")) {
pdfStamper.close();
}
if (IsDefined("outStream")) {
outStream.close();
}
</cfscript>

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

...Read More

Wednesday, December 19, 2007

Getting started with custom styles and CFCHART

While working with some basic cfcharts, I came across an entry on Ray Camden's site that made me realize I had overlooked a powerful tool for manipulating chart styles.

Now if you have heavy duty charting needs, you are probably using something other than cfchart already. So this entry may be "one big yawn" for you. But if you are doing basic to moderate charting and would like a little more control over the chart styles, and are not familiar with WebCharts3D, read on.

Enter WebCharts3D


The real jewel in Camden's entry was the mention of the WebCharts3D utility. It turns out the ColdFusion documentation, of all places, contains instructions on how to use this fantastic utility to create chart styles.

Now there are some limitations. Cfchart does not support all of the different styles WebCharts3D is capable of producing. Though from what I have heard there are other ways to produce these charts in ColdFusion besides using cfchart. Barney Boisvert's site has what appears to be an interesting entry about charting with svg and batik. I came across it while searching for information about svg, but confess I have yet to finish it. Same old story, too many interesting topics to research, not enough time.

Anyway, the Webcharts3D utility is quite flexible allows you to modify many of the default styles like data series labels, tool tips, color palettes, etcetera. You can find a complete description of the various properties on the WebCharts site. Again, not all of the properties will be supported by cfchart. But within the the basic chart styles there is a surprising amount of flexibility. Open up the WebCharts3D utility and take it for spin.



And now a few rudimentary style examples from Charts 101 ...



Doughnut Chart 1

<!--- basic doughnut chart with: custom paint palette (DawnTransluent) --->
<cfchart format="png" style="doughnutChart1.xml">
<cfchartseries type="pie">
<cfchartdata item="2000" value="100.0">
<cfchartdata item="2001" value="200.0">
<cfchartdata item="2002" value="100.0">
<cfchartdata item="2003" value="180.0">
<cfchartdata item="2004" value="200.0">
</cfchartseries>
</cfchart>

<!--- doughnutChart1.xml --->
<?xml version="1.0" encoding="UTF-8"?>
<pieChart depth="Double" style="sliced" type="SmallNut" angle="314">
<dataLabels style="Value" placement="Outside"/>
<legend>
<decoration style="None"/>
</legend>
<popup background="#C8FFFFFF" foreground="#333333"/>
<paint palette="DawnTransluent" paint="Plain"/>
<insets left="5" top="5" right="5" bottom="5"/>
</pieChart>


Doughnut Chart 2

<!--- basic doughnut chart with: custom data labels, tooltips (RoundShadow), paint palette (Dawn) --->
<cfchart format="flash" style="doughnutChart2.xml" chartwidth="500" chartheight="300">
<cfchartseries type="pie">
<cfchartdata item="2000" value="100.0">
<cfchartdata item="2001" value="200.0">
<cfchartdata item="2002" value="100.0">
<cfchartdata item="2003" value="180.0">
<cfchartdata item="2004" value="200.0">
</cfchartseries>
</cfchart>

<!--- doughnutChart2.xml --->
<?xml version="1.0" encoding="UTF-8"?>
<pieChart depth="Double" style="Sliced" type="SmallNut" angle="314">
<dataLabels style="Pattern" placement="Inside" font="Verdana-10">
<![CDATA[
$(colLabel) - ${colPercent}
]]>
</dataLabels>
<elements place="Default" drawOutline="false">
<morph morph="Grow" stage="None"/>
<![CDATA[
Year (Amount): ${colLabel} (${value})\n Overall: ${colPercent} of ${colTotal}
]]>
</elements>
<legend placement="Left" />
<popup decoration="RoundShadow" background="#C8FFFFCC" isAntialiased="true"
foreground="black"
isMultiline="true"/>
<paint paint="Dawn"/>
<insets left="5" top="5" right="5" bottom="5"/>
</pieChart>


Combination Chart


<!--- basic combination chart with: custom line, bar and toolips --->
<cfchart format="flash" style="comboChart.xml">
<cfchartseries type="line" serieslabel="Sample 0">
<cfchartdata item="2000" value="100.0" >
<cfchartdata item="2001" value="700.0">
<cfchartdata item="2002" value="100.0">
<cfchartdata item="2003" value="180.0">
<cfchartdata item="2004" value="200.0">
<cfchartdata item="2005" value="400.0">
</cfchartseries>
<cfchartseries type="line" serieslabel="Sample 1">
<cfchartdata item="2000" value="150.0">
<cfchartdata item="2001" value="300.0">
<cfchartdata item="2002" value="250.0">
<cfchartdata item="2003" value="230.0">
<cfchartdata item="2004" value="250.0">
<cfchartdata item="2005" value="450.0">
</cfchartseries>
<cfchartseries type="bar" serieslabel="Sample 2">
<cfchartdata item="2000" value="200.0">
<cfchartdata item="2001" value="200.0">
<cfchartdata item="2002" value="250.0">
<cfchartdata item="2003" value="130.0">
<cfchartdata item="2004" value="450.0">
<cfchartdata item="2005" value="250.0">
</cfchartseries>
</cfchart>

<!--- comboChart.xml --->
<?xml version="1.0" encoding="UTF-8"?>
<frameChart is3D="false">
<frame xDepth="6" yDepth="6" outline="black" leftAxisPlacement="Front"/>
<xAxis>
<labelStyle isMultilevel="true"/>
</xAxis>
<dataLabels foreground="black"/>
<elements place="Default" shape="Curve" outline="black" lineWidth="6"
drawShadow="true" showMarkers="false">
<morph morph="Grow"/>
<series index="0" shape="Line"/>
<series index="2" shape="Bar">
<paint color="#CC99FF"/>
</series>
</elements>
<popup decoration="Shadow" background="#C8FFFFCC" isAntialiased="true"
foreground="black"
isMultiline="true"/>
<paint palette="Pastel" isVertical="true" max="100"/>
<insets left="5" top="5" right="5" bottom="5"/>
</frameChart>

...Read More

Tuesday, December 18, 2007

Getting started with iText - Part 13 (TwoOnOne.java)

The next installation in Getting started with iText translates the TwoOnOne example. It demonstrates a simple but useful technique for manipulating an existing PDF file. The original PDF is condensed by merging every two pages into one. If the original PDF file contained 32 pages, the new file would contain only 16 pages.




First a word about deprecation


Technically this example does not require a newer version of iText. It is possible to run this code using only the older iText jar that is built into MX7 and CF8. But if you view the API it states that some of the methods required like height() are deprecated and scheduled to be removed entirely in a later version. So for that reason you might prefer to use a newer iText jar. For those of you that like to live dangerously I have included a MX7/CF8 compatible version at the end.

What you will need for this example


1. A recent version of iText like 2.0.7. For instructions, see How to use a newer version of iText


UPDATE: I wanted to re-emphasize an issue that is mentioned in the instructions link above. MX users that are running the JavaLoader.cfc must read the article Using a Java URLClassLoader in CFMX Can Cause a Memory Leak.

The code for this example creates a new instance of the JavaLoader on each request. This was intended only to demonstrate how to use the JavaLoader. To avoid any confusion, my future code examples will use a server scoped object instead.


2. A sample PDF file. For this example download the ChapterSection.pdf file from the iText site. Place the file in the same directory as your .cfm script.

Now onto the code


First we initialize a few variables with the paths of the files we will be using. Then we get an instance of the JavaLoader.cfc that we will use to create our iText objects.


<cfscript>
// by default outputs to current directory. change as needed
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToOutputFile = ExpandPath("./2On1.pdf");
fullPathToITextJar = ExpandPath("./iText-2.0.7.jar");
dotNotationPathToJavaLoader = "javaloader.JavaLoader";

// cfSearching: create an instance of the javaLoader
pathsForJavaLoader = arrayNew(1);
arrayAppend(pathsForJavaLoader, fullPathToITextJar);
javaLoader = createObject('component', dotNotationPathToJavaLoader).init(pathsForJavaLoader);
</cfscript>


Next we create an instance of PdfReader to read in the input file and get the total number of pages. Then we obtain the size of the first page, and use it to create a new Document and PdfWriter object. These objects will be used to generate our new PDF file.


<cfscript>
// we create a reader for a certain document
pdfReader = javaLoader.create("com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
// we retrieve the total number of pages
totalPages = pdfReader.getNumberOfPages();

// we retrieve the size of the first page
pageSize = pdfReader.getPageSize(1);
// cfSearching: In earlier jars (MX7/CF8), the Rectangle methods are height()/width() not getHeight()/getWidth()
width = pageSize.getHeight();
height = pageSize.getWidth();

// step 1: creation of a document-object
rectangle = javaLoader.create("com.lowagie.text.Rectangle").init(width, height);
document = javaLoader.create("com.lowagie.text.Document").init(rectangle);

// step 2: we create a writer that listens to the document
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
pdfWriter = javaLoader.create("com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);
</cfscript>


Finally we use a while loop to essentially copy all pages from the original file onto the new PDF file, in groups of two. Note, I am using the term "copy" loosely here to conceptually describe the final product.

Since we are starting with a blank PDF, we first add a new page inside each iteration of the loop. Next we copy the current set of pages from the original document. The first page will be added to the left side of the current Document page using a transformation.



<cfscript>
document.newPage();
p = p + 1;
i = i + 1;

page1 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page1,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", "60"),
javacast("float", "120") );
</cfscript>


The second page, if one exists, will be added to the right side.


<cfscript>
if (i LT totalPages) {
i = i + 1;
page2 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page2,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", ((width / 2) + 60) ),
javacast("float", "120") );
}
</cfscript>


Finally we add a footer with the page numbers: page x of y.


<cfscript>
pdfContentByte.beginText();
pdfContentByte.setFontAndSize(bf, javacast("float", 14));
pdfContentByte.showTextAligned(PdfContentByte.ALIGN_CENTER,
"page "& p &" of "& ( INT(totalPages / 2) + IIF(totalPages MOD 2 GT 0, 1, 0)),
javacast("float", width / 2),
javacast("float", 40),
javacast("float", 0) );
pdfContentByte.endText();
</cfscript>


The loop will repeat these steps until all of the pages in the original document are written to the new PDF.

So there you have it. As always comments/suggestions/corrections are welcome!

Documentation: Manipulating existing PDF documents
Source: TwoOnOne.java

Complete Code (Requires JavaLoader.cfc and iText 2.0.7)

<h1>Two Pages on One Example (Using JavaLoader.cfc)</h1>

<cfscript>
savedErrorMessage = "";

// by default outputs to current directory. change as needed
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToOutputFile = ExpandPath("./2On1.pdf");
fullPathToITextJar = ExpandPath("./iText-2.0.7.jar");
dotNotationPathToJavaLoader = "javaloader.JavaLoader";

// cfSearching: create an instance of the javaLoader
pathsForJavaLoader = arrayNew(1);
arrayAppend(pathsForJavaLoader, fullPathToITextJar);
javaLoader = createObject('component', dotNotationPathToJavaLoader).init(pathsForJavaLoader);

try {
// we create a reader for a certain document
pdfReader = javaLoader.create("com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
// we retrieve the total number of pages
totalPages = pdfReader.getNumberOfPages();

// we retrieve the size of the first page
pageSize = pdfReader.getPageSize(1);
// cfSearching: In earlier jars (MX7/CF8), the Rectangle methods are height()/width() not getHeight()/getWidth()
width = pageSize.getHeight();
height = pageSize.getWidth();

// step 1: creation of a document-object
rectangle = javaLoader.create("com.lowagie.text.Rectangle").init(width, height);
document = javaLoader.create("com.lowagie.text.Document").init(rectangle);

// step 2: we create a writer that listens to the document
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
pdfWriter = javaLoader.create("com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);

// step 3: we open the document
document.open();

// step 4: we add content
baseFont = javaLoader.create("com.lowagie.text.pdf.BaseFont");
bf = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
pdfContentByte = pdfWriter.getDirectContent();

i = 0;
p = 0;
while (i LT totalPages) {
document.newPage();
p = p + 1;
i = i + 1;
// cfSearching: copy the next page
page1 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page1,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", "60"),
javacast("float", "120") );

if (i LT totalPages) {
i = i + 1;
page2 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page2,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", ((width / 2) + 60) ),
javacast("float", "120") );
}

// cfSearching: Add a "page x of y" footer
pdfContentByte.beginText();
pdfContentByte.setFontAndSize(bf, javacast("float", 14));
pdfContentByte.showTextAligned(PdfContentByte.ALIGN_CENTER,
"page "& p &" of "& ( INT(totalPages / 2) + IIF(totalPages MOD 2 GT 0, 1, 0)),
javacast("float", width / 2),
javacast("float", 40),
javacast("float", 0) );
pdfContentByte.endText();
}

WriteOutput("Finished!");
}
catch (Exception de) {
savedErrorMessage = de;
}

// step 5: we close the document
document.close();
// close the output stream
outStream.close();
</cfscript>

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


Complete Code (MX7/CF8 compatible version)

<h1>Two Pages on One Example (MX7/CF8 compatible)</h1>

<cfscript>
savedErrorMessage = "";

// cfSearching: by default all files are in the current directory. change as needed
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToOutputFile = ExpandPath("./2On1.pdf");

try {
// we create a reader for a certain document
pdfReader = createObject("java", "com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
// we retrieve the total number of pages
totalPages = pdfReader.getNumberOfPages();

// we retrieve the size of the first page
pageSize = pdfReader.getPageSize(1);
// cfSearching: height() and width() are deprecated in later iText versions
// cfSearching: The new methods are getHeight()/getWidth()
width = pageSize.height();
height = pageSize.width();

// step 1: creation of a document-object
rectangle = createObject("java", "com.lowagie.text.Rectangle").init(width, height);
document = createObject("java", "com.lowagie.text.Document").init(rectangle);

// step 2: we create a writer that listens to the document
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
pdfWriter = createObject("java", "com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);

// step 3: we open the document
document.open();

// step 4: we add content
baseFont = createObject("java", "com.lowagie.text.pdf.BaseFont");
pdfContentByte = pdfWriter.getDirectContent();
i = 0;
p = 0;
while (i LT totalPages) {
document.newPage();
p = p + 1;
i = i + 1;
page1 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page1,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", "60"),
javacast("float", "120") );

if (i LT totalPages) {
i = i + 1;
page2 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page2,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", ((width / 2) + 60) ),
javacast("float", "120") );
}

bf = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
pdfContentByte.beginText();
pdfContentByte.setFontAndSize(bf, javacast("float", 14));
pdfContentByte.showTextAligned(PdfContentByte.ALIGN_CENTER,
"page "& p &" of "& ( INT(totalPages / 2) + IIF(totalPages MOD 2 GT 0, 1, 0)),
javacast("float", width / 2),
javacast("float", 40),
javacast("float", 0) );
pdfContentByte.endText();
}

WriteOutput("Finished!");
}
catch (Exception de) {
savedErrorMessage = de;
}

// step 5: we close the document
document.close();
// close the output stream
outStream.close();
</cfscript>

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

...Read More

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep