Thursday, February 11, 2010

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>

4 comments:

Merritt April 19, 2010 at 12:32 AM  

Have you tried using the VFS in CF9 to speed up the creation of PDFs with iText?

cfSearching April 19, 2010 at 5:12 AM  

@Merritt,

I really have not worked with VFS much. But from what I have read, it seems similar to just using a ByteArrayOuputStream / ByteArrayInputStream. If that is the case, is there a reason you would want to use VFS instead ?

-Leigh

Merritt,  April 29, 2010 at 11:35 AM  

Really I was just looking for an added performance boost. Do you know of any examples that use "ByteArrayOuputStream / ByteArrayInputStream"? In any case, thanks for responding. Your blog is a great resource for CF users looking to tap into iText!

cfSearching April 29, 2010 at 8:26 PM  

@Merritt,

Well, iText may be doing some of these things already behind the scenes anyway. But in a basic case, using byte streams is not significantly different. (Watch the line wrapping) For the input replace the file path with an InputStream:

inStream = createObject("java", "java.io.ByteArrayInputStream").init( ToBinary(myPdf) );
pdfReader = createObject("java", "com.lowagie.text.pdf.PdfReader").init(inStream);

For output, simply replace the FileOutputStream with a ByteArrayOutputStream

outStream = createObject("java", "java.io.ByteArrayOutputStream").init();
pdfStamper = createObject("java", "com.lowagie.text.pdf.PdfStamper").init(pdfReader, outStream);

HTH
-Leigh

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep