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>

0 comments:

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep