iTextSharp is a C# port of a popular java library for creating Adobe Acrobat PDF files. It's powerful and flexible and, best of all, it's free. Here I'm going to show you how to create a simple PDF report using the iTextSharp library.

To obtain iTextSharp, visit the iTextSharp home page at iTextSharp.SourceForge.Net. The version that I'm using now is 4.0.8, released on January 25, 2008. For support there is the iText documentation which refers to the java library but to which the C# port hews very closely, as well as a mailing list specifically for iTextSharp.

iTextSharp is a pretty good way to create nicely (and precisely) formatted documents for printing from a web application. This is necessary if, for example, users request that you print a page in landscape mode, or put a page break in a specific location, or request anything else that is prohibitively difficult to render with HTML and the browser's print page function.

Here, I'm going to describe how I implemented a one page project issue report. The report looks like the following, and I chose to render it as PDF so I could include header and footer information and so I could control where and how page breaks are handled.

Sample Report

I'm not going to go into too much depth. This is an intro, not a tutorial. Folks wanting more information should see Bruno Lowagie's book, iText in Action, and should subscribe to the iTextSharp mailing list. Some of what I will cover here are:

  • How to precisely position text within a document using tables.
  • Useful properties of the PDFPCell object
  • How to add header and footer text to a document
  • How to insert an image into a document
  • How to make a PDF document prompt to print

The first step in creating an iTextSharp report is to create a document object and a writer object. The document object controls the size and layout of the page, and the writer object controls where the finished PDF document file will reside.

If you're creating a PDF from a web application, then you probably want each PDF to have a unique filename, so that multiple people can request PDF reports without stepping on each other. I use the GUID object for this, and put the PDF files in a temporary directory which I clean out at application start (see global.asax for this event). Here's the code that I use to ensure that the PDF has a unique filename. The "TempPath" and "TempURL" variables contain the physical (and virtual) path to the temporary folder.


        msFileName = Guid.NewGuid.ToString & ".pdf"
        msFilePath = HttpContext.Current.Application("TempPath") & msFileName
        msFileURL = HttpContext.Current.Session("TempURL") & msFileName

You specify the size of the page to generate when instansiating the Document object. There are constants that represent various page sizes in the PageSize ENUM.


        Dim oDoc As New iTextSharp.text.Document(iTextSharp.text.PageSize.LETTER)

To print a page in landscape orientation, use the .Rotate modifier.


        Dim oDoc As New iTextSharp.text.Document(iTextSharp.text.PageSize.LETTER.Rotate)

Set any metadata before you open the document.


        With oDoc
            .AddTitle("PROBLEM SOLVING INVESTIGATION SHEET (PSIS)")
            .AddSubject("PSIS")
            If HttpContext.Current.User.Identity.IsAuthenticated Then
                .AddAuthor(HttpContext.Current.User.Identity.Name)
            End If
        End With

In addition to creating the Document object to contain your PDF document, you need to create a writer object. The writer object is responsible for creating the actual PDF file. Here's how it is instansiated:


        Dim oWriter As iTextSharp.text.pdf.PdfWriter = _
            iTextSharp.text.pdf.PdfWriter.GetInstance(oDoc, New FileStream(msFilePath, FileMode.Create))

To add header and footer text to a PDF document, you need to trap page events. However, page events are implemented in an unusual way in iTextSharp. To trap these events, you need to create a class that implements the iTextSharp.text.pdf.IPdfPageEvent interface. You then need to create an instance of that class and attach it to the writer object's PageEvent property.

Here's a snippet of code that shows how header and footer elements are handled within the PDFPageEvent class that I created to implement the IPdfPageEvent interface.


    Public Class PDFPageEvent
        Implements iTextSharp.text.pdf.IPdfPageEvent

        Private msMachineInfo As String
        Private msShiftInfo As String

        Private moTemplate As PdfTemplate
        Private moCB As PdfContentByte
        Private moBF As BaseFont = Nothing

        Public Sub New( _
                    ByVal sMachineInfo As String, _
                    ByVal sShiftInfo As String)
            MyBase.New()

            msMachineInfo = sMachineInfo
            msShiftInfo = sShiftInfo
        End Sub

There are lots of event stubs that are added to the class automatically when it inherits the IPdfPageEvent interface. In the interest of brevity, I've omitted them here.


        Public Sub OnCloseDocument( _
                        ByVal writer As iTextSharp.text.pdf.PdfWriter, _
                        ByVal document As iTextSharp.text.Document) _
                            Implements iTextSharp.text.pdf.IPdfPageEvent.OnCloseDocument
                            
            moTemplate.BeginText()
            moTemplate.SetFontAndSize(moBF, 8)
            moTemplate.ShowText((writer.PageNumber - 1).ToString)
            moTemplate.EndText()
        End Sub

        Public Sub OnEndPage( _
                        ByVal writer As iTextSharp.text.pdf.PdfWriter, _
                        ByVal document As iTextSharp.text.Document) _
                            Implements iTextSharp.text.pdf.IPdfPageEvent.OnEndPage
                            
            setHeader(writer, document)
            setFooter(writer, document)
        End Sub

        Public Sub OnOpenDocument( _
                        ByVal writer As iTextSharp.text.pdf.PdfWriter, _
                        ByVal document As iTextSharp.text.Document) _
                            Implements iTextSharp.text.pdf.IPdfPageEvent.OnOpenDocument
            Try
                moBF = BaseFont.CreateFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED)
                moCB = writer.DirectContent
                moTemplate = moCB.CreateTemplate(50, 50)

            Catch de As DocumentException

            Catch ioe As IOException

            End Try
        End Sub

Here are the private subroutines that actually draw the header and footer text on the page. Both the header and footer have three columns.


#Region "OnPageEnd"
        Private Sub setHeader(ByVal oWriter As iTextSharp.text.pdf.PdfWriter, ByVal oDocument As Document)
            Dim oTable As New PdfPTable(3)
            Dim oCell As PdfPCell
            With oTable
                oCell = New PdfPCell( _
                                New iTextSharp.text.Phrase( _
                                        "PSIS Report", _
                                        FontFactory.GetFont("Arial", 10, iTextSharp.text.Font.NORMAL)))
                oCell.Border = 0
                oCell.HorizontalAlignment = Element.ALIGN_CENTER
                .AddCell(oCell)

                oCell = New PdfPCell( _
                                New iTextSharp.text.Phrase( _
                                        msMachineInfo, _
                                        FontFactory.GetFont("Arial", 10, iTextSharp.text.Font.NORMAL)))
                oCell.Border = 0
                oCell.HorizontalAlignment = Element.ALIGN_CENTER
                .AddCell(oCell)

                oCell = New PdfPCell( _
                                New iTextSharp.text.Phrase( _
                                        msShiftInfo, _
                                        FontFactory.GetFont("Arial", 10, iTextSharp.text.Font.NORMAL)))
                oCell.Border = 0
                oCell.HorizontalAlignment = Element.ALIGN_CENTER
                .AddCell(oCell)
            End With

            oTable.TotalWidth = oDocument.PageSize.Width - 20
            oTable.WriteSelectedRows(0, -1, 10, oDocument.PageSize.Height - 15, oWriter.DirectContent)

            '---Line just below header
            moCB.MoveTo(30, oDocument.PageSize.Height - 35)
            moCB.LineTo(oDocument.PageSize.Width - 40, oDocument.PageSize.Height - 35)
            moCB.Stroke()
        End Sub

        Private Sub setFooter( _
                        ByVal oWriter As iTextSharp.text.pdf.PdfWriter, _
                        ByVal oDocument As iTextSharp.text.Document)
                        
            'three columns at bottom of page
            Dim sText As String
            Dim fLen As Single

            '---Column 1: Disclaimer
            sText = "Confidential"
            fLen = moBF.GetWidthPoint(sText, 8)

            moCB.BeginText()
            moCB.SetFontAndSize(moBF, 8)
            moCB.SetTextMatrix(30, 30)
            moCB.ShowText(sText)
            moCB.EndText()

            '---Column 2: Date/Time
            sText = Now.ToString
            fLen = moBF.GetWidthPoint(sText, 8)

            moCB.BeginText()
            moCB.SetFontAndSize(moBF, 8)
            moCB.SetTextMatrix(oDocument.PageSize.Width / 2 - fLen / 2, 30)
            moCB.ShowText(sText)
            moCB.EndText()

            '---Column 3: Page Number
            Dim iPageNumber As Integer = oWriter.PageNumber
            sText = "Page " & iPageNumber & " of "
            fLen = moBF.GetWidthPoint(sText, 8)

            moCB.BeginText()
            moCB.SetFontAndSize(moBF, 8)
            moCB.SetTextMatrix(oDocument.PageSize.Width - 90, 30)
            moCB.ShowText(sText)
            moCB.EndText()

            moCB.AddTemplate(moTemplate, oDocument.PageSize.Width - 90 + fLen, 30)
            moCB.BeginText()
            moCB.SetFontAndSize(moBF, 8)
            moCB.SetTextMatrix(280, 820)
            moCB.EndText()

            '---Line just above footer
            moCB.MoveTo(30, 40)
            moCB.LineTo(oDocument.PageSize.Width - 40, 40)
            moCB.Stroke()

        End Sub
#End Region

    End Class

Here you can see the PDFPageEvent class being attached to the writer object.


        Dim sMachineInfo As String = "Machine " & _
                oStatementNode.Attributes("machine").Value & " / " & _
                oStatementNode.Attributes("area").Value

        Dim sShiftInfo As String = _
                CDate(oStatementNode.Attributes("initiated_date").Value).ToShortDateString & _
                " shift " & _
                sShift

        Dim oEvents As New PDFPageEvent(sMachineInfo, sShiftInfo)
        oWriter.PageEvent = oEvents

Once you've created the document, and attached the events object then you're ready to start adding content.


        oDoc.Open()

PDF and iTextSharp is a great way to address printing requirements but the standard window.print(); javascript function doesn't work to print a web page with a PDF document embedded within an IFRAME, even if a timeout is used to give the PDF document time to load before the print dialog is displayed. An alternative is to embed a javascript command within the PDF document itself.

The following command will cause the Acrobat Reader to prompt the user: "This document is trying to print. Do you want to allow this?" and if the user clicks on "yes" then the PDF document will print.


        oWriter.AddJavaScript("this.print(false);", False)

Creating a report using iTextSharp is a lot like creating a report using HTML tables with one huge caveat: There is no rowspan property in iTextSharp. It is possible to implement something that looks like rowspan by embedding tables within tables.

There are two table models in iTextSharp. PdfPTables are currently the recommended choice.

When creating a PdfPTable you can specify the number of columns that the table should have, like this:


        Dim oTable As New iTextSharp.text.pdf.PdfPTable(6)

Just specifying the number of columns will cause iTextSharp to render the columns proportionately. If you want to specify specific widths, you can create an array of single width percentages and use the SetTotalWidth property.


        Dim oWidths() As Single = {16.5, 16.5, 16.5, 16.5, 16.5, 16.5}
        oTable.SetTotalWidth(oWidths)

By default, tables are created with margins. If you're using them for layout purposes you don't want the default margins to be used. To cause the PdfPTable to be rendered without the default margins, use the WidthPercentage property. This property controls the amount of available space to be used by the table. To use all available space set WidthPercentage=100.

PdfPTables do not have an end-of-row marker. The row ends when the designated number of cells have been added to the table. So, if the table has six columns and seven cells are added, then the seventh cell will be rendered as the first cell on the second row of the table.

Tables can be added to cells and this, in fact, is how rowspan is implemented.

Some of the more interesting attributes of the PdfPCell object are:

Borders

To specify that a cell has no border at all, or a proportionate border, the Border property can be used. Alternatively, if there sides of the cell are to have different border thicknesses, then the BorderWidthTop, BorderWidthLeft, BorderWidthRight and BorderWidthBottom properties can be used.

Note: If your table is being displayed with a border, try putting it inside a single cell table with the Border property set to 0. The table object doesn't have a border property of its own but this seems to work.

Border Colors

Border colors work just like border thicknesses. The BorderColor property can be used if all borders will have the same color, and the BorderColorTop, BorderColorLeft, BorderColorRight and BorderColorBottom properties can be used if the cell will have borders of different colors.

Padding

There is are Padding and PaddingTop, PaddingLeft, PaddingRight and PaddingBottom properties that control the amount of interior padding in the cell. Use the .HorizontalAlignment and .VerticalAlignment properties to control how content is aligned within the cell. These properties accept various values of the ELEMENT enum such as Element.ALIGN_CENTER and Element.ALIGN_LEFT.

Row height and left-right posititioning

The PdfPCell object has a FixedHeight property that can be used to mandate the height of a cell. There is also a MinimumHeight property.

There doesn't seem tobe a corresponding property to control the width of a cell, but I've found through trial and error that the width of a LETTER page in portrait layout is 450 points. If you then know the width of the thing you're trying to position, you can use table column width percentages to put it where you need it.

For example, to draw two columns each 100 points wide on the right side of a document, you'd use a table with three columns. The physical widths of the columns (assuming a 450 point page width) would be 250 for the left column and 100 each for the two right columns. The percentages would then be: 55.5, 22.22 and 22.22.

If you are using percentages to position cells on a page, then you may wish to use the PdfPTable LockedWidth property. Setting this to true prevents the width of the table from being dynamically adjusted.

Text

Text can be added either to a PdfPCell or to the document directly using the PDFPhrase object. The FontFactory object controls the font family, size and other attributes.


    dim oPhrase as New iTextSharp.text.Phrase( _
                "My PDF Text", _
                FontFactory.GetFont("Arial", 10, iTextSharp.text.Font.NORMAL)))

Rotated Text

Here is a little function that can be used to create rotated text. To use it, use the AddElement method of the PdfPCell object.


          Private Function MakeVerticalText( _
                                ByVal oWriter As PdfWriter, _
                                ByVal sText As String) As iTextSharp.text.Image

            Dim oTemplate As PdfTemplate = oWriter.DirectContent().CreateTemplate(20, 20)
            Dim oBF As BaseFont = BaseFont.CreateFont("Helvetica", "winansi", False)    '<--Font family
            Dim fsize As Single = 10    '<--Font size
            Dim fwidth As Single = oBF.GetWidthPoint(sText, fsize)
            oTemplate.BeginText()
            oTemplate.SetFontAndSize(oBF, fsize)
            oTemplate.SetTextMatrix(0, 2)
            oTemplate.ShowText(sText)
            oTemplate.EndText()
            oTemplate.Width = fwidth

            Dim oImg As iTextSharp.text.Image = iTextSharp.text.Image.GetInstance(oTemplate)
            oImg.RotationDegrees = -90  '<--Angle of font rotation

            Return oImg
        End Function

Images

While the adding of images to a PDF document is a very complicated topic, here is a little function that can be used to add an image referenced by a URL.


    Private Function GetURLImage( _
                        ByVal sURL As String) As iTextSharp.text.Image

        Dim oURI As New System.Uri(sURL)
        Dim oImg As iTextSharp.text.Image = iTextSharp.text.Image.GetInstance(oURI)
        oImg.ScalePercent(100)

        Return oImg
    End Function

Finally, once you've completed generating the document, use the Close method of the Document object to let iTextSharp know you're done.


        oDoc.Close()