Monday, February 15, 2010

ColdFusion 9: CFPDF - Adding Headers and Footers (and a Few Quirks) - Part 2

In Part 1 I posted some examples of text and image headers. CF9 also introduced some new constants for working with page numbers and labels.


Page Numbers
Though "Page X of Y" headers/footers are not exactly earth-shattering, it is a little easier to add them to existing pdf's with the new constants _PAGENUMBER and _LASTPAGENUMBER.

<!--- Sample Document --->
<cfdocument format="pdf" name="pdfInput">
    <cfloop from="1" to="10" index="x">
        The Blank Page 
        <cfif x lt 10>
            <cfdocumentitem type="pagebreak" />
        </cfif>
    </cfloop>
</cfdocument>

<!--- Add Page X of Y Footers --->
<cfpdf action="addFooter"
   source="pdfInput"
   name="pdfOutput"
   text="Page _PAGENUMBER of _LASTPAGENUMBER" 
/>

<!--- Display results --->
<cfcontent type="application/pdf" variable="#ToBinary(pdfOutput)#" />

NumberFormat
With the help of the neat numberFormat attribute, you can change boring decimal page numbers into Roman numeral format (both upper and lower case). The supported values are: LOWERCASEROMAN, NUMERIC, UPPERCASEROMAN.

<!--- Sample Document --->
<cfdocument format="pdf" name="pdfInput">
    <cfloop from="1" to="10" index="x">
        The Blank Page 
        <cfif x lt 10>
            <cfdocumentitem type="pagebreak" />
        </cfif>
    </cfloop>
</cfdocument>

<!--- Add Page X of Y in Roman Numerals --->
<cfpdf action="addFooter"
    source="pdfInput"
    name="pdfOutput"
    numberFormat="UpperCaseRoman"
    text="Page _PAGENUMBER of _LASTPAGENUMBER" 
/>

<!--- Display results --->
<cfcontent type="application/pdf" variable="#ToBinary(pdfOutput)#" />

Page Labels
If your pdf contains custom Page Labels, you can also add that text to headers/footers using the _PAGELABEL and/or _LASTPAGELABEL constants. Obviously if your pdf doesn't have custom page labels, you will probably just see the default decimal page numbers instead. But I am pretty sure you can add custom labels with ddx (or alternately with iText).


<!--- Add Page Label to Header --->
<cfpdf action="addHeader"
    source="testDocumentWithLabels.pdf"
    name="pdfOutput"
    text="<b>Label For Page (_PAGENUMBER):</b> _PAGELABEL" 
/>

<!--- Display results --->
<cfcontent type="application/pdf" variable="#ToBinary(pdfOutput)#" />

Things You Cannot Do ...
Perhaps this is an obvious point, but these settings have no affect on information already embedded within the pdf. In other words, you cannot change existing pdfs with page numbers already embedded in the file. However, if you use cfpdf to generate the headers/footers you can certainly remove them and add new ones.

Printing Headers/Footers
For cases when you do not always want headers/footers to be printed, you can use the showOnPrint option to control visibility when printing. For example, you could tweak some of the previous snippets to display both the graphic and text headers on screen, but use showOnPrint="false" to make only the text header "printable". Obviously you can also use the pages attribute to apply that behavior only to select pages.

<!--- 
    Display the graphics header only on screen
--->
<cfpdf action="addHeader" 
    source="inputFile.pdf" 
    destination="pdfWithImageHeader.pdf" 
    topMargin="2.6"
    leftMargin="0"
    rightMargin="0"
    align="center"
    image="http://www.google.com/logos/olympics10-prsskating-hp.png"
    overwrite="true"
    showonprint = "false"
/>

....

<!--- 
    Display the text header both on screen and when printed
--->
<cfpdf action="addHeader" 
    source="pdfWithImageHeader.pdf" 
    name="pdfOutput" 
    topMargin="1.7"
    leftMargin="0"
    rightMargin="0"
    align="center"
    text="#headerText#"
    showonprint = "true"
/>

As you can see, it is not extremely complex. But hopefully these examples make a good addendum to the limited documentation on the addHeader / addFooters features.

...Read More

ColdFusion 9: CFPDF - Adding Headers and Footers (and a Few Quirks) - Part 1

With the explosion of new features in CFPDF, there is a lot to cover in the documentation. So it is understandably a little lacking on extra examples that go beyond the very basics. While belatedly exploring some of the extended features, I ran into a few quirks with addHeader / addFooter. Probably not news to some of you. But I thought I would share my examples and results in case it saves someone else a little time.


Text Headers and Footers
The basic concept of adding a textual headers and footers was simple enough. Just use the appropriate action, addHeader or addFooter, along with a simple text value.  Setting the text alignment is equally simple, just provide an align value (left,center, right) with the desired margins.  It is worth noting that if you omit the margin, CF uses its default.  While I would not swear to it, the default seems to be 1 inch. At least for letter sized pages.

Note: If there is too much text to display with the margin bounds, it may flow right off the page. So if working with a lot of text, you may need to adjust it with some html/css.

<!--- Align plain text header --->
<cfpdf  action="addHeader" 
    source="inputFile.pdf" 
    name="pdfOutput" 
    align="left"
    leftMargin="0.5"
    text="Left Aligned. One-half (0.5) inch from left side"
/>  

<!--- Display results --->
<cfcontent type="application/pdf" variable="#ToBinary(pdfOutput)#" />
Styling Text Headers and Footers
Applying html/css styles to the text is very similar to using a text watermark (added in CF 8.0.1). I did encounter one peculiarity with fonts. The names seem to be case sensitive. So on windows the font name "castellar" did nothing, but "Castellar" (with an uppercase "C") did produce the desired font.


<!--- 
Add Styled header 
--->
<cfsavecontent variable="headerText">
    <span style="font-size: 25pt; font-family: Castellar;">
       <b style="color: #0080ff;">Vancouver</b>
       <b style="color: #0000ff;">2010</b>
       <b style="color: #000080;">Olympics</b>
    </span>
</cfsavecontent>

<cfpdf action="addHeader" 
    source="inputFile.pdf" 
    name="pdfOutput" 
    topMargin="1"
    text="#headerText#"
/>

<!--- Display results --->
<cfcontent type="application/pdf" variable="#ToBinary(pdfOutput)#" />

HTML/CSS Limitations
Not all html tags are supported, such as <div>, <a>, etcetera. I am assuming the same limitations apply to action="addWatermark". So whatever quirks one has, the others probably do as well. 

Now in case you see an error message complaining about an "Invalid Document..", check the stack trace for the real error. Most likely it will say something about either an invalid tag or syntax error in your html/css.


An error occurred during the ADDHEADER operation in the cfpdf tag.  
Error: Invalid Document C:\ColdFusion9\wwwroot\test\pdf\inputFile.pdf specified for source or directory.  
...

Stack Trace:
com.adobe.internal.pdftoolkit.core.exceptions.PDFInvalidDocumentException: Error during text formatting, internalProcessElement: InvalidInput: Illegal element tag "div".
....

Differences between Watermarks and Headers/Footers
One difference I have noticed is how text is sized. CF 8.0.1 added the width and height attributes to support text watermarks. From what I have observed, the width and height values seem to control the size of the watermark text, rather than any explicit font sizes in html/css.  Since the width and height attributes are not allowed with actions addHeader and addFooter,  that text can be controlled by css font sizes.

Text and Image Opacity
As with watermarks, you can also use the opacity attribute to change transparency of the header/footer. Simply use a value between 0 (transparent) and 10 (fully opaque). This example creates a styled footer whose text is 50% transparent. Opacity applies to both text and images.



<cfsavecontent variable="text">
    <span style="font-size: 18pt; font-family: Century Gothic;">
       <b style="color: #0080ff;">Vancouver</b>
       <b style="color: #0000ff;">2010</b>
       <b style="color: #000080;">Olympics</b>
    </span>
</cfsavecontent>

<cfpdf action="addFooter" 
    source="inputFile.pdf" 
    name="pdfOutput" 
    bottomMargin="0.5"
    text="#text#"
    align="center"
    opacity="5"
/>    

<!--- Display results --->
<cfcontent type="application/pdf" variable="#ToBinary(pdfOutput)#" />

Image Headers and Footers
Using images for headers and footers is also supported. The image source can be a file path, url, CF image object, array of bytes, or a base64 string (using isBase64="true").

Figuring out image headers/footers took a little while. It is not that the syntax is particularly cryptic. But it was not really clear to me, from the documentation, what should happen in various cases, such as when using an over-sized or undersized image.

Based on my tests, if an image is too large to fit within the given margin dimensions, it is automatically re-sized. So if the margin is too small, you can end up with a skewed or squished image. I also observed some strange alignment issues in a few of my tests. Though I am not yet sure why. In any case, you may want to ensure your images are correctly sized beforehand, to avoid unexpected resizing behavior by cfpdf.

If you view the image url below, its size should be around 7.4 x 2.4 inches. To center align the image, I used the code below. The top margin is set to "2.6" (inches). Tall enough to fit the image without resizing, plus a small amount of padding at the top. Both the left and right margin are set to zero (0) so the image is centered across the entire page width.

Now you may notice that visually this image does not appear completely centered. But that is due to the layout of the underlying graphic. But you can adjust the position by changing the alignment and leftMargin.


<!---
    Sample Document
--->
<cfdocument format="pdf" fileName="inputFile.pdf" pageType="Letter" marginTop="2.6" overwrite="true">
    <style type="text/css" media="screen">
        div { font-family: Castellar; padding: 10px; color: 204a64; } 
    </style>
    <cfloop from="1" to="10" index="x">
        <div> 
            Alpine Skiing * Biathlon * BobSleigh * Cross-Country Skiing * Figure Skating * 
            Ice Hockey * Luge * Curling * Snowboard * Speed Skating
        </div>
        <cfif x lt 10>
            <cfdocumentitem type="pagebreak" />
        </cfif>
    </cfloop>
</cfdocument>

<!--- 
    Add image header (Note: Using image URL just for demonstration purposes)
--->
<cfpdf action="addHeader" 
    source="inputFile.pdf" 
    destination="pdfWithImageHeader.pdf" 
    topMargin="2.6"
    leftMargin="0"
    rightMargin="0"
    align="center"
    image="http://www.google.com/logos/olympics10-prsskating-hp.png"
    overwrite="true"
/>

Multiple Headers / Footers
It is also possible to layer headers/footers by adding text on top of an existing image. To demonstrate, read in the file created in the previous code snippet and adjust the margin size so the text is placed in the desired position. The results should look something like this



<!--- 
Add text header on top of existing image header
--->
<cfsavecontent variable="headerText">
    <span style="font-size: 25pt; font-family: Castellar;">
       <b style="color: #0080ff;">Vancouver</b>
       <b style="color: #0000ff;">2010</b>
       <b style="color: #000080;">Olympics</b>
    </span>
</cfsavecontent>

<cfpdf action="addHeader" 
    source="pdfWithImageHeader.pdf" 
    name="pdfOutput" 
    topMargin="1.7"
    leftMargin="0"
    rightMargin="0"
    align="center"
    text="#headerText#"
/>

<!--- Display results --->
<cfcontent type="application/pdf" variable="#ToBinary(pdfOutput)#" />

Remove Headers/Footers
In the process of running all of these tests, I discovered something worth noting about images.  Using an image obviously increases the file size. In some cases substantially (I have not gotten around to doing any size comparisons between cfpdf and iText yet). Since adding a new header/footer does not remove any existing ones, if you mistakenly add an image twice, it will increase the file size .. twice. But you can always use the action="removeHeaderFooter" first. (Yes, it does remove both headers and footers).


<!--- 
    Remove headers and footers
--->
<cfpdf action="removeHeaderFooter" 
        source="pdfWithImageHeader.pdf" 
        name="pdfOutput" 
    />

<!--- Display results --->
<cfcontent type="application/pdf" variable="#ToBinary(pdfOutput)#" />

On a side note, if you use cfdocument to generate header and footers, as opposed to the new cfpdf actions, be aware there is a "Known Issue" mentioned in the CF9 release notes.


Bug 76078:
Headers or footers added while generating PDF using cfdocument are not marked and are not recognized as headers or footers. Therefore, cfpdf action="removeheaderfooter" will not be able to remove them from source PDF.

ColdFusion 9: CFPDF - Adding Headers and Footers (and a Few Quirks) - Part 2

...Read More

Thursday, February 11, 2010

ColdFusion: Adding a Link to an Existing PDF with iText

A recent question on stackoverflow.com asked how to add a hyperlink to an existing pdf with iText. There is most definitely more than one way to do it, and quite possibly better methods than the one mentioned here. But as it uses a few interesting techniques I thought I would share it.  Of course any comments or improvements are always welcome.


As usual, first open the source pdf with a reader object, and use a stamper to prepare the output file for writing. With that out of the way, you can move on to creating the hyperlink.

Given that CF8 and CF9 use older versions of iText, I decided the simplest method would be to use a Chunk object.  If you are not familiar with Chunks, they are a low level object used to represent a bunch of characters all having the same properties (font, color, etcetera). So first initialize a Chunk with whatever text you want to use for the link. Then use setAnchor() to specify the link url.

<cfscript>
   pdfReader = createObject("java", "com.lowagie.text.pdf.PdfReader").init( inputPath );
   outStream = createObject("java", "java.io.FileOutputStream").init( outputPath );
   pdfStamper = createObject("java", "com.lowagie.text.pdf.PdfStamper").init( pdfReader, outStream );

   chunk = createObject("java", "com.lowagie.text.Chunk").init("Rage Against the Machine (on Wikipedia)");
   chunk.setAnchor("http://en.wikipedia.org/wiki/Rage_Against_the_Machine");
</cfscript>

Since the default font is pretty bland, you will probably want to select a different font for the link. There are several ways to work with fonts. But in this example I defined a BaseFont by passing in the path to the physical font file, the desired encoding and a flag to ensure the font is embedded. The BaseFont definition is then used to create a Font object with the desired settings (such as size, color and style) and applied to the Chunk object.


(On a side note, I was going to use the basic arial.ttf font. But while perusing the windows/font directory, I was amused to find an odd font named Rage and promptly decided I had use it for this entry instead)


<cfscript>
   // define an embedded font 
   BaseFont = createObject("java", "com.lowagie.text.pdf.BaseFont");
   Font = createObject("java", "com.lowagie.text.Font");
   bf = BaseFont.createFont("c:/windows/fonts/rage.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);
   // create the main font object
   textColor = createObject("java", "java.awt.Color").decode("##084f5a");
   textFont = Font.init(bf, 18, Font.UNDERLINE, textColor);   
   chunk.setFont( textFont );
</cfscript>


Now to position the Chunk, I decided to use a ColumnText object. As the name implies, it is used to layout text in column format. Though used quite simply here, the ColumnText class is capable of some pretty complex operations.

To create a ColumnText object you must pass in a PdfContentByte object. In loose terms that is the canvas where the text will be drawn.  In this example, the link is added to the foreground. So getOverContent() is used to grab the canvas of the target page from the stamper object and then passed into the ColumnText object.  Finally the Chunk is added to the ColumnText object for rendering.

The next to last step is to define the dimensions of the column. Once the dimensions are defined, ColumnText.go() is used to draw the link onto the pdf.  (I will not go into the details of positioning here. But if you are unfamiliar with it, this entry on buttons describes the typical way in which objects are positioned in iText.)

Note: This snippet uses deprecated methods for CF8 compatibility. For a CF9 compatible version, see the end of entry

<cfscript>
   cb = pdfStamper.getOverContent(1); 
   ct = createObject("java", "com.lowagie.text.pdf.ColumnText").init(cb);
   ct.addElement( chunk );

   // set the column dimensions
   page = pdfReader.getPageSize(1);
   llx =  page.right()- 325;   
   lly = page.top() - 36;       
   urx = page.right();                
   ury = page.top() - 8;     
   ct.setSimpleColumn(llx, lly, urx, ury);

   // write the text
   ct.go();
</cfscript>

Once you close the stamper, the resulting pdf should contain a cool looking link in the top right. Minus the thematic image of course ..


Complete Code (ColdFusion 8)
<cfscript>
     inputPath = ExpandPath("./myDocument.pdf");
     outputPath = ExpandPath("./myDocumentWithLink.pdf");

     try {
        // initialize objects
        pdfReader = createObject("java", "com.lowagie.text.pdf.PdfReader").init( inputPath );
        outStream = createObject("java", "java.io.FileOutputStream").init( outputPath );
        pdfStamper = createObject("java", "com.lowagie.text.pdf.PdfStamper").init( pdfReader, outStream );

        // create a chunk with a anchor (ie hyperlink)
        chunk = createObject("java", "com.lowagie.text.Chunk").init("Rage Against the Machine (on Wikipedia)");
        chunk.setAnchor("http://en.wikipedia.org/wiki/Rage_Against_the_Machine");

        // define an embedded font 
        BaseFont = createObject("java", "com.lowagie.text.pdf.BaseFont");
        Font = createObject("java", "com.lowagie.text.Font");
        bf = BaseFont.createFont("c:/windows/fonts/rage.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);

        // create the main font object
        textColor = createObject("java", "java.awt.Color").decode("##084f5a");
        textFont = Font.init(bf, 18, Font.UNDERLINE, textColor);   

        // apply the font to the chunk 
        chunk.setFont( textFont );

        // prepare to write the link onto the *first* page only        
        cb = pdfStamper.getOverContent(1); // first page
        ct = createObject("java", "com.lowagie.text.pdf.ColumnText").init(cb);
        ct.addElement( chunk );

        // position link near top right 
        // note: using deprecated versions of getRight() and getBottom()
        page = pdfReader.getPageSize(1);
        llx =  page.right()- 325;   
        lly = page.top() - 36;       
        urx = page.right();                
        ury = page.top() - 8;     
        // initialize column dimensions
        ct.setSimpleColumn(llx, lly, urx, ury);

        // write the text
        ct.go();
    }
    catch (java.lang.Exception e) {
       // Save the error object and use cfdump _outside_ 
       // the cfscript block to display the full error detail
       WriteOutput("ERROR: "& e.message &"<hr />");
       WriteOutput("DETAIL: "& e.detail);
    }        
   
   // closing the stamper generates the output file
    if (IsDefined("pdfStamper")) {
        WriteOutput("Closing pdfStamper ..<hr />");
       pdfStamper.close();
   }
   // also ensure the outstream is always closed
   // to avoid locked files if an error occurs early on ..
    if (IsDefined("outStream")) {
        WriteOutput("Closing outStream  ..<hr />");
       outStream.close();
   }
   WriteOutput("Output file generated: "& outputPath );
</cfscript>

Complete Code (ColdFusion 9)
<cfscript>
     inputPath = ExpandPath("./myDocument.pdf");
     outputPath = ExpandPath("./myDocumentWithLink.pdf");

     try {
        // initialize objects
        pdfReader = createObject("java", "com.lowagie.text.pdf.PdfReader").init( inputPath );
        outStream = createObject("java", "java.io.FileOutputStream").init( outputPath );
        pdfStamper = createObject("java", "com.lowagie.text.pdf.PdfStamper").init( pdfReader, outStream );

        // create a chunk with a anchor (ie hyperlink)
        chunk = createObject("java", "com.lowagie.text.Chunk").init("Rage Against the Machine (on Wikipedia)");
        chunk.setAnchor("http://en.wikipedia.org/wiki/Rage_Against_the_Machine");

        // define an embedded font 
        BaseFont = createObject("java", "com.lowagie.text.pdf.BaseFont");
        Font = createObject("java", "com.lowagie.text.Font");
        bf = BaseFont.createFont("c:/windows/fonts/rage.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);

        // create the main font object
        textColor = createObject("java", "java.awt.Color").decode("##084f5a");
        textFont = Font.init(bf, 18, Font.UNDERLINE, textColor);   

        // apply the font to the chunk 
        chunk.setFont( textFont );

        // prepare to write the link onto the *first* page only        
        cb = pdfStamper.getOverContent(1); // first page
        ct = createObject("java", "com.lowagie.text.pdf.ColumnText").init(cb);
        ct.addElement( chunk );

        // position link near top right 
        // note: using deprecated versions of getRight() and getBottom()
        page = pdfReader.getPageSize(1);
        llx =  page.getRight()- 325;   
        lly = page.getTop() - 36;       
        urx = page.getRight();                
        ury = page.getTop() - 8;     
        // initialize column dimensions
        ct.setSimpleColumn(llx, lly, urx, ury);

        // write the text
        ct.go();
    }
    catch (java.lang.Exception e) {
       // Save the error object and use cfdump _outside_ 
       // the cfscript block to display the full error detail
       WriteOutput("ERROR: "& e.message &"<hr />");
       WriteOutput("DETAIL: "& e.detail);
    }        
   
   // closing the stamper generates the output file
    if (IsDefined("pdfStamper")) {
        WriteOutput("Closing pdfStamper ..<hr />");
       pdfStamper.close();
   }
   // also ensure the outstream is always closed
   // to avoid locked files if an error occurs early on ..
    if (IsDefined("outStream")) {
        WriteOutput("Closing outStream  ..<hr />");
       outStream.close();
   }
   WriteOutput("Output file generated: "& outputPath );
</cfscript>

...Read More

OT: Valentine's Day Humor

A recent link in twitter pointed out a Science Valentine comic. It got me to thinking about the strange rituals and spectacle of Valentine's Day. It has always seemed like a hokey, greeting-card holiday, more overtly commercial than all the others. Of course most holidays have become increasingly commercialized. But despite all that, I still love the magic of Christmas, the mystery of Halloween, and the inevitable laughter and tears that accompany most dysfunctional family "get-togethers" at Thanksgiving. But to me, Valentine's Day only evokes a sense of curiosity and humor at the artificially induced drama of it all.


As a young children we usually find the whole concept "icky" and want nothing to do with it. As we get a little older we must endure the angst of deciding whether or not to give a Valentine's Day card to someone we "like". (But.. if I do that then they'll _know_ I like them!) The pins and needles of waiting to see if that person you like gives you a card. The elation when they do. The crushing disappointment when they do not. Not to mention the awkwardness of receiving a card from someone you do not like. (Mom, is there a nice way to say "Ewwww. Gross. No way."?) Oh the high drama of primary school.

Even as adults, the day still evokes strong emotions in many of us. Those that are in relationships tend to "coo" and "gush" to a revolting degree. Those that are not, either piteously wallow in sorrow while tossing back copious amounts of beer or scarfing down chocolates, or they turn morose and bitter making scathing quips about the doomed nature of all relationships.

Despite the ups and downs of many Valentine's Days past, I choose to view the day with a bit of my usual weird humor. It is best summed up by the following song requests I heard one year:


  • Love Stinks (J. Geils Band)
  • Used To Love Her (Guns N' Roses)
  • She Hates Me (Puddle of Mud)


Thinking of it still makes me laugh. If you have ever listened to the songs, and have a sense of humor about the day, you will know exactly why ;)

...Read More

ColdFusion 9: Adding Document Level Attachments to a PDF with iText

While cfpdf provides some nice features, like merging and deleting pages, it does not provide a way to attach files as far as I know. But this can be done with a small bit of iText magic. There are a few different ways to attach files to a pdf. If you are looking for examples, there are some excellent ones on the iText site you can easily adapt for ColdFusion.


One way to attach a file is at the document level. So when viewing the pdf with a tool like Adobe Reader, the files appear only in the attachments pane. The iText version that ships with CF9 has a handy convenience method for creating attachments. Unfortunately, that method did not exist yet in iText 1.4. So if you are running CF8, you will need to use the JavaLoader.cfc, and a newer version of iText, to take advantage of it. (Though a quick glance at the source code suggests it is possible with CF8's version with a little extra CF code.) But back to the example..

In case you do not have a pdf handy, simply create one with cfdocument first. Though any pdf should work.

<cfset inputFile = ExpandPath("myDocument.pdf") />
<cfdocument format="PDF" name="pdfContent">
    <cfdump var="#server.os#" label="Server O/S" />
</cfdocument>

Now if you have read any of my previous entries on iText, you will be very familiar with next few steps. Initialize a few variables with the paths of the files you wish to attach. (I went a little crazy and decided to attach three files: a Word document, an Excel file and a simple text file). Next, read in your source file with a PdfReader object. Then prepare a PdfStamper to generate the output file.

<cfscript>
   // ...
   inputFile = ExpandPath("myDocument.pdf");
   outputFile = ExpandPath("myDocumentWithAttachments.pdf");
   
   attach1   = "c:/test/docs/NewsLetter-Feb-2010.doc";
   attach2   = "c:/test/docs/Statistics-Jan-2010.xls";
   attach3   = "c:/test/docs/test.txt";

   reader = createObject("java", "com.lowagie.text.pdf.PdfReader").init( inputFile );
   outStream = createObject("java", "java.io.FileOutputStream").init( outputFile );
   stamper = createObject("java", "com.lowagie.text.pdf.PdfStamper").init( reader, outStream );
   // ...
</cfscript>

It is worth noting this example actually embeds the content of each file within the pdf. So obviously final pdf will be larger than the original. To actually attach the files, simply use the stamper's addFileAttachment(..) method on each one. That method is overloaded, but the signature used in this example accepts four arguments: file description, data, file path and file name.

The first argument is an optional description of the attachment. The second and third arguments pertain to how you wish to attach the content. You can either supply content dynamically (via an array of bytes) or using a physical file path. Since it would not make sense to supply both, just provide a value for one of the arguments and use null for the other. If you accidentally supply both, the file path will probably be ignored.


The final argument of addFileAttachment(..) allows you to customize the file name displayed in the attachment pane. Obviously a handy feature if you are supplying dynamic content and do not necessarily have a file name. But you can use it in either scenario to just display a more user friendly file name. (On a side note, the file name is technically optional. But I do not see much point in leaving it blank.)

<cfscript>
   // ...
   // create the first attachment from a file path
   stamper.addFileAttachment("Newsletter for February", javacast("null", ""), attach1, "Newsletter.doc");

   // ...
</cfscript>

Once you have attached the files, you can get a little fancy and modify the viewer preferences to display the attachment pane when the pdf is first opened. It is a nice way to draw a user's attention to the fact that the pdf contains attachments. You should also adjust the pdf version to 1.6, since that is when this feature was added.

<cfscript>
   // ...
   // display the attachment pane when the pdf opens (Since 1.6)  
   writer = stamper.getWriter();
   writer.setPdfVersion( writer.VERSION_1_6 );
   stamper.setViewerPreferences( writer.PageModeUseAttachments );    
   // ...
</cfscript>

Once you have properly closed the pdf, the final output should look like the image below in Acrobat Reader. Now whether or not you can open/save the attachments all depends on your security settings. I believe the default for Acrobat Reader 8 and 9 is to disable the opening of all non-pdf attachments. So you may need to adjust your Trust Manager settings accordingly. Keep in mind that behavior has nothing to do with the pdf file itself. It is strictly how Acrobat chooses to handle attachments.



On a closing note, if you are not up to date on your patches, be aware there are some relatively recent security updates for both Adobe Reader and Acrobat, involving the Trust Manager. So if you have not checked your version recently, now might be a good time to do so!


Complete Code
<!---
    Create test PDF
--->
<cfset inputFile = ExpandPath("myDocument.pdf") />
<cfdocument format="PDF" name="pdfContent">
    <cfdump var="#server.os#" label="Server O/S" />
</cfdocument>

<!---
    Add attachments to existing pdf
--->
<cfscript>
    try {
        // Source/destination file paths
        inputFile = ExpandPath("myDocument.pdf");
        outputFile = ExpandPath("myDocumentWithAttachments.pdf");
        
        // Sample files to attach to pdf
        attach1     = "c:/test/docs/NewsLetter-Feb-2010.doc";
        attach2        = "c:/test/docs/Statistics-Jan-2010.xls";
        attach3        = "c:/test/docs/test.txt";

        // open source file and prepare for modification
        reader = createObject("java", "com.lowagie.text.pdf.PdfReader").init( inputFile );
        outStream = createObject("java", "java.io.FileOutputStream").init( outputFile );
        stamper = createObject("java", "com.lowagie.text.pdf.PdfStamper").init( reader, outStream );

        // create the first attachment from a file path
        stamper.addFileAttachment("Newsletter for February", javacast("null", ""), attach1, "Newsletter.doc");

        // create the second "dynamically" (ie from an array of bytes). 
        // deliberately leave the description blank
        bytes = FileReadBinary(attach2);
        stamper.addFileAttachment(javacast("null", ""), bytes, javacast("null", ""), "Statistics.xls");

        // create the last attachment from a file path
        stamper.addFileAttachment("Meaningless text file", javacast("null", ""), attach3, "Stuff.txt");

        // display the attachment pane when the pdf opens (Since 1.6)  
        writer = stamper.getWriter();
        writer.setPdfVersion( writer.VERSION_1_6 );
        stamper.setViewerPreferences( writer.PageModeUseAttachments );    
            
    }
    finally {
        // always cleanup objects
        if (IsDefined("stamper")) {
               stamper.close();
        }
          if (IsDefined("outStream")) {
              outStream.close();
          }
    }

    WriteOutput("Output saved to file "& outputFile);
</cfscript>

...Read More

Wednesday, February 10, 2010

ColdFusion 9: Yet Another Way to Refresh a Bound CFSELECT

One of the really nice features of CF8 and CF9 is bindings. But there are a few noticeable gaps, such as refreshing a bound CFSELECT list. From what I can tell, this still is not supported. Though apparently CF9 did introduce support for the selected attribute.


I spent some time pouring over the cfajax.js file, and had little more than a headache to show for my troubles. Then I came across a slick suggestion in an older entry on Todd Sharp's blog. It mentioned the undocumented ColdFusion.Bind.assignValue method could be used to refresh the list.  It was perfect.  Exactly what I needed.

Well... then I got to wondering if there might be something even simpler. (I know, I know. People are never satisfied).  After further review of cfajax.js, and lots of experimentation, I came across a nice one-liner that seems to work. Still undocumented unfortunately. But still pretty neat:

<script language="javascript">
    ColdFusion.bindHandlerCache['yourElementID'].call();
</script>

... and what does this function do?
If you generate a cfselect with a CFC bind, and view the source, you will see a call to a function named register(). Not surprisingly, the register() function seems to do all the work of creating the bindings and invoking the CFC to populate the select list.


After taking a peak inside cfajax.js, I noticed the register() function creates a parameter-less function which handles invoking the CFC. (You can open up cfajax.js yourself if you want to view all the gory details.) But the key part is that CF stores this handler function in one of its cache objects. So you can easily grab a reference to it function just by passing in the id (or name) of your form element. Then simply invoke it using call().

Pretty cool stuff! It goes a long way towards alleviating my headache..

...Read More

Tuesday, February 9, 2010

ColdFusion 9: CFSELECT + BIND + Selected Attribute Work Together Now?

In case you have been living under a rock like me, it seems the selected attribute works with bound cfselect lists.


Form

<cfparam name="form.foodID" default="0">

<cfform method="post" action="#CGI.SCRIPT_NAME#">
    Favorite Food: 
                <cfselect name="foodID"
                    value="foodID"
                    display="FoodName"
                    selected="#form.foodID#"
                    bind="cfc:Food.getFood()"
                    bindOnLoad="true"
                />
    <cfinput type="submit" name="submitMe" />
</cfform>

Food.cfc
<cfcomponent output="false">
   
   <cffunction name="getFood" access="remote" output="false" returntype="query">
      <cfset var q    = QueryNew("") />
      <cfset queryAddColumn(q, "FoodID", "integer", listToArray("0,1,2,3")) />
      <cfset queryAddColumn(q, "FoodName", "varchar", listToArray("-Pick Something-,Burger,Pizza,Lasagna")) />
      <cfreturn q />      
   </cffunction>
   
</cfcomponent>

...Read More

Sunday, February 7, 2010

ColdFusion 9: CFPDFFORM (A Tag With No Name ?)

A question about saving the output of cfpdfform to a variable came up on the adobe forums recently. As I have been doing some testing with CF9 + CFPDF this week, I suggested using the name attribute. Well I was a bit surprised when someone casually mentioned that cfpdfform does not have a documented name attribute. Huh?


Scratching my head I checked the documentation and sure enough name was not one of the listed attributes. It is documented for cfpdf, but not cfpdfform. So I re-checked my code and the name attribute did work in simple tests.

<cfpdfform action="populate" source="c:\MyForm.pdf" name="pdfData">
    <cfpdfformparam name="name" value="Mark McCallan">
    <cfpdfformparam name="address" value="926 Data Avenue">
</cfpdfform>

<cfcontent type="application/pdf" variable="#ToBinary(pdfData)#" />

So I started to wonder if maybe it was just an omission in the documentation. I decided to try the highly unscientific method of using invalid attribute combinations. Just to see if the error messages would shed any light on the expected attributes. Granted it is probably not the most reliable indicator. But worth a shot.


Unfortunately the messages for some combinations suggest "name" is valid


While others seemed to side with the documentation.


A dump of the tag's class certainly seems to suggest support for a "name" attribute.


I was leaning towards a fluke in the documentation. But then I noticed a few issues with a LiveCycle form that had special rights enabled. When the pdf opened, Acrobat complained that the document was modified and disabled the special features.


So now I am thinking maybe it was not an omission in the documentation. Anyone have the real scoop on the "name" attribute?

...Read More

Thursday, February 4, 2010

Form Field Values, Multipart Forms and Servlets (... oh my)

I saw an intriguing question on stackoverflow yesterday, by Tony Petruzzi .  It was about parsing form field values, but with a bit of twist. We all know that when multiple form fields have the same name, the field values are returned as a comma delimited list in CF. While that is a handy feature at times, what do you do when the field values contain commas? Well after a few false starts, I did come up with one option, but wanted to describe it in a little more detail than stackoverflow comments allow.

Stealing from Tony's example, take the simple form below. Each of the two fields contains a different number of letters separated by a comma. But once the form is submitted, the value is just one big csv string. So you really have no way of determining which values were actually entered into each of the fields.



Tony's post described a slick method of extracting the field values as a string array, with the help of getPageContext() and getParameterMap(). The problem was it does not seem to work when the enctype is multipart/form-data.


<cfdump var="#getPageContext().getRequest().getParameterMap()#">

From what little I have read so far, my understanding of the issue is that the standard request object (ie HttpServletRequest) does not handle multipart requests.  A special multipart handler is needed instead. So with that in mind, my next question was what handler does CF use?

Well, while poking around in the CF libraries a while back, I noticed CF uses some of the O'Reilly servlet classes. In particular, the ones for MultiPartRequest handling. If you look at the API you will notice the classes return all sorts of good information like input field names, values, etcetera. So I figured that is probably what CF was using for the form and that this information had to be exposed ...somewhere.

On a whim I decided to get back to basics and dump the FORM scope class. That revealed a promising method named getPartsArray(). Looping through the array confirmed my suspicion that results were indeed one of the O'Reilly classes:
com.oreilly.servlet.multipart.ParamPart


<cfset formClass = createObject("java", form.getClass().name)>
   <cfdump var="#formClass#" label="FORM Scope Class" />
   
   <cfloop array="#form.getPartsArray()#" index="part">
      <cfdump var="#part#" label="Part Element" />
   </cfloop>


All that was left was to use a few methods from the API to the retrieve the form field information. First the isParamPart() method is used to identify the input fields only. Then getName() and getStringValue() are used to grab the field's name and value.  As you can see, there is not much to it.

<!--- if this is a multipart request ...--->
   <cfset variables.parts = form.getPartsArray()>
   <cfif structKeyExists(variables, "parts")>
      <cfoutput>   
      <cfloop array="#variables.parts#" index="p">
         <cfif p.isParam()>
            isParam() = #p.isParam()#
            getName() = #p.getName()#
            stringValue() = #p.getStringValue()#
         </cfif>
      </cfloop>
      </cfoutput>
   </cfif>


Of course this only applies to multipart requests. The part array will be null for other request types. So make sure the array exists before using it. Anyway, I thought this was an interesting approach to the old form field dilemma. But if anyone knows of any other techniques, I would love to hear them.

...Read More

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep