DEV Community

IronSoftware
IronSoftware

Posted on

iTextSharp Creating a Footer with Page Numbers (Issue Fixed)

Adding "Page X of Y" footers to PDF documents seems like a straightforward task, yet iTextSharp developers routinely spend hours wrestling with the PdfPageEventHelper class just to display page numbers. The complexity stems from a fundamental problem: when iTextSharp renders the first page, it has no way of knowing how many total pages the document will contain.

The Problem

iTextSharp requires developers to implement a custom class extending PdfPageEventHelper to add headers and footers. For simple static text, this works reasonably well. However, the moment you need dynamic content like "Page 1 of 5", you encounter a chicken-and-egg problem: the total page count is only known after the entire document has been rendered.

The solution iTextSharp provides involves creating a PdfTemplate placeholder during document creation, writing it to each page's footer area, and then filling in the actual total page count in the OnCloseDocument event after the document finishes rendering. This requires understanding several interconnected concepts:

  • PdfContentByte for direct content manipulation
  • PdfTemplate for deferred content rendering
  • BaseFont configuration for text rendering
  • Coordinate calculations for positioning
  • The subtle timing differences between OnStartPage, OnEndPage, and OnCloseDocument

Error Messages and Symptoms

Developers commonly encounter these issues when implementing page footers:

DocumentException: The document is not open
IOException: The document has no pages
NullReferenceException: Object reference not set to an instance of an object
System.InvalidOperationException: The document is already closed
Enter fullscreen mode Exit fullscreen mode

Stack traces frequently point to attempts to add content in the wrong event handler or to access the PdfWriter before the document is properly initialized.

Who Is Affected

This complexity affects any developer who needs to:

  • Generate reports with page numbering
  • Create invoices with footer information
  • Produce legal documents requiring page counts
  • Build batch processing systems that generate paginated PDFs
  • Implement print-ready PDF export from web applications

The 88,000+ views on the original Stack Overflow question demonstrate the scale of developers encountering this issue across .NET Framework and .NET Core applications.

Evidence from the Developer Community

The difficulty of implementing page footers in iTextSharp has generated extensive discussion across developer forums over more than a decade.

Timeline

Date Event Source
2009-06-23 Original question posted Stack Overflow
2009-2017 Multiple answers and variations posted Stack Overflow
2017-09-15 Last significant activity on thread Stack Overflow
Ongoing 88,647 views accumulated Stack Overflow

Community Reports

"Though I keep getting an exception on cb"
— Original poster, Stack Overflow, 2009

The accepted answer on Stack Overflow spans over 100 lines of code, implementing a TwoColumnHeaderFooter class with multiple properties and event handlers. Alternative solutions posted by other developers range from 50 to 200 lines, each with different trade-offs.

Forum threads on C# Corner, CodeProject, and Microsoft Q&A show developers asking the same question repeatedly, often with slightly different requirements that require re-implementing the page event handler from scratch.

Root Cause Analysis

The complexity arises from iTextSharp's low-level document model. Unlike libraries that abstract page rendering, iTextSharp exposes the PDF content stream directly. This provides flexibility but requires developers to understand PDF internals.

The page numbering problem specifically occurs because:

  1. Streaming Architecture: iTextSharp writes pages to the output stream sequentially. Once a page is written, its content cannot be modified.

  2. Unknown Total: The total page count depends on content flow, table sizes, and page breaks that are calculated during rendering.

  3. Template Workaround: The PdfTemplate object provides a way to reserve space on each page and fill it later, but this requires coordinating multiple event handlers.

  4. Coordinate System: Developers must calculate exact X/Y coordinates for footer placement, accounting for page margins, font metrics, and text alignment.

The iText documentation explicitly states: "It is forbidden to add content to the document object in the onEndPage(). You should add your header and your footer in the onEndPage() method using PdfWriter, NOT document."

Attempted Workarounds

Workaround 1: The PdfTemplate Approach

The standard solution involves creating a template placeholder and filling it when the document closes.

Approach: Create a PdfTemplate in OnOpenDocument, add it to each page in OnEndPage, and populate the total in OnCloseDocument.

public class PageFooterHandler : PdfPageEventHelper
{
    private PdfContentByte contentByte;
    private PdfTemplate totalPagesTemplate;
    private BaseFont baseFont;

    public override void OnOpenDocument(PdfWriter writer, Document document)
    {
        base.OnOpenDocument(writer, document);
        baseFont = BaseFont.CreateFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
        contentByte = writer.DirectContent;
        totalPagesTemplate = contentByte.CreateTemplate(50, 50);
    }

    public override void OnEndPage(PdfWriter writer, Document document)
    {
        base.OnEndPage(writer, document);
        int currentPage = writer.PageNumber;
        string pageText = "Page " + currentPage + " of ";
        float textWidth = baseFont.GetWidthPoint(pageText, 8);

        Rectangle pageSize = document.PageSize;

        contentByte.BeginText();
        contentByte.SetFontAndSize(baseFont, 8);
        contentByte.SetTextMatrix(pageSize.GetLeft(40), pageSize.GetBottom(30));
        contentByte.ShowText(pageText);
        contentByte.EndText();

        contentByte.AddTemplate(totalPagesTemplate, pageSize.GetLeft(40) + textWidth, pageSize.GetBottom(30));
    }

    public override void OnCloseDocument(PdfWriter writer, Document document)
    {
        base.OnCloseDocument(writer, document);
        totalPagesTemplate.BeginText();
        totalPagesTemplate.SetFontAndSize(baseFont, 8);
        totalPagesTemplate.SetTextMatrix(0, 0);
        // Subtract 1 because OnCloseDocument is called with page number incremented
        totalPagesTemplate.ShowText((writer.PageNumber - 1).ToString());
        totalPagesTemplate.EndText();
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

Document document = new Document(PageSize.A4, 36, 36, 54, 54);
PdfWriter writer = PdfWriter.GetInstance(document, new FileStream("output.pdf", FileMode.Create));
writer.PageEvent = new PageFooterHandler();
document.Open();
// Add content...
document.Close();
Enter fullscreen mode Exit fullscreen mode

Limitations:

  • Requires approximately 60+ lines of boilerplate code
  • Must manually calculate positions using coordinate system
  • Font metrics must be considered for proper alignment
  • Different page sizes require different calculations
  • Centering text requires additional width calculations
  • No built-in support for styling or HTML formatting

Workaround 2: Using PdfPTable for Footer Layout

Some developers use PdfPTable within the page event for more complex footer layouts.

Approach: Create a table in OnEndPage and use WriteSelectedRows to position it.

public override void OnEndPage(PdfWriter writer, Document document)
{
    PdfPTable footerTable = new PdfPTable(3);
    footerTable.TotalWidth = document.PageSize.Width - document.LeftMargin - document.RightMargin;
    footerTable.DefaultCell.Border = Rectangle.NO_BORDER;

    footerTable.AddCell(new Phrase("Left text", FontFactory.GetFont(FontFactory.HELVETICA, 8)));
    footerTable.AddCell(new Phrase("Page " + writer.PageNumber, FontFactory.GetFont(FontFactory.HELVETICA, 8)));
    footerTable.AddCell(new Phrase("Right text", FontFactory.GetFont(FontFactory.HELVETICA, 8)));

    footerTable.WriteSelectedRows(0, -1, document.LeftMargin, document.BottomMargin - 10, writer.DirectContent);
}
Enter fullscreen mode Exit fullscreen mode

Limitations:

  • Still cannot display total page count without template workaround
  • Additional complexity layered on top of existing complexity
  • Performance overhead from creating table on every page
  • Table cell alignment requires additional configuration

Workaround 3: Two-Pass Rendering

Some applications render the document twice: once to count pages, then again with the correct total.

Limitations:

  • Doubles processing time
  • Doubles memory usage
  • Not practical for dynamic or large documents
  • Requires identical rendering conditions between passes

A Different Approach: IronPDF

Rather than working around the limitations of a low-level PDF API, IronPDF provides a declarative approach to headers and footers. The library handles the complexity of page numbering internally, exposing simple placeholder tokens that get replaced during rendering.

Why IronPDF Handles This Differently

IronPDF uses a Chrome-based rendering engine that processes the entire document before finalizing output. This architecture allows the library to:

  • Calculate total page count before writing any pages
  • Replace placeholder tokens with actual values
  • Support HTML and CSS styling in headers and footers
  • Handle positioning automatically based on margins

The footer is defined as an HTML fragment with placeholder tokens. The rendering engine substitutes {page} and {total-pages} with the actual values during PDF generation.

Code Example

using IronPdf;

public class PdfFooterGenerator
{
    public void GeneratePdfWithPageFooter()
    {
        // Initialize the Chrome-based PDF renderer
        var renderer = new ChromePdfRenderer();

        // Configure footer with page numbering using placeholder tokens
        renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
        {
            HtmlFragment = "<div style='text-align: center; font-size: 10px; font-family: Arial, sans-serif;'>" +
                           "Page {page} of {total-pages}" +
                           "</div>",
            MaxHeight = 20,
            DrawDividerLine = false
        };

        // Set bottom margin to accommodate the footer
        renderer.RenderingOptions.MarginBottom = 25;

        // Render HTML content to PDF
        string htmlContent = @"
            <html>
            <body>
                <h1>Sample Report</h1>
                <p>This document demonstrates automatic page numbering.</p>
                <!-- Additional content that may span multiple pages -->
            </body>
            </html>";

        var pdf = renderer.RenderHtmlAsPdf(htmlContent);

        // Save the generated PDF
        pdf.SaveAs("report-with-footer.pdf");
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points about this code:

  • The {page} placeholder is replaced with the current page number
  • The {total-pages} placeholder is replaced with the total page count
  • HTML and inline CSS control the footer appearance
  • No event handlers, templates, or coordinate calculations required
  • The MaxHeight property reserves space for the footer content

Extended Example with Styled Footer

using IronPdf;

public class StyledFooterGenerator
{
    public void GeneratePdfWithStyledFooter()
    {
        var renderer = new ChromePdfRenderer();

        // Create a footer with company branding and page numbers
        renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
        {
            HtmlFragment = @"
                <div style='width: 100%; font-family: Arial, sans-serif; font-size: 9px;
                            display: flex; justify-content: space-between; padding: 0 20px;'>
                    <span style='color: #666;'>Confidential Document</span>
                    <span style='color: #333;'>Page {page} of {total-pages}</span>
                    <span style='color: #666;'>Generated: {date}</span>
                </div>",
            MaxHeight = 25,
            DrawDividerLine = true
        };

        // Configure header as well
        renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter
        {
            HtmlFragment = @"
                <div style='text-align: right; font-size: 8px; color: #999; padding-right: 20px;'>
                    {html-title}
                </div>",
            MaxHeight = 20
        };

        // Set margins to accommodate header and footer
        renderer.RenderingOptions.MarginTop = 30;
        renderer.RenderingOptions.MarginBottom = 30;

        // Render content
        var pdf = renderer.RenderHtmlAsPdf("<html><head><title>Quarterly Report</title></head><body>...</body></html>");
        pdf.SaveAs("styled-report.pdf");
    }
}
Enter fullscreen mode Exit fullscreen mode

Available Placeholder Tokens

IronPDF supports these merge fields in headers and footers:

Placeholder Description
{page} Current page number
{total-pages} Total page count in document
{date} Current date
{time} Current time
{url} Source URL (for URL-to-PDF conversion)
{html-title} Title from the HTML <title> tag
{pdf-title} PDF document title metadata

API Reference

For more details on the methods and classes used:

Migration Considerations

Licensing

IronPDF is commercial software. A free trial is available for evaluation, and licenses are available for individual developers, teams, and enterprises. The licensing model differs from iTextSharp's AGPL license, which requires either open-sourcing your application or purchasing a commercial license.

API Differences

The migration from iTextSharp to IronPDF involves conceptual differences:

iTextSharp IronPDF
Programmatic document construction HTML/CSS-based rendering
PdfPageEventHelper for headers/footers HtmlHeaderFooter with placeholders
Manual coordinate positioning Automatic layout with margins
PdfPTable for complex layouts HTML tables with CSS
BaseFont configuration Standard web fonts or embedded fonts

For existing iTextSharp projects, migration involves translating document construction logic to HTML templates. Simple reports can often be converted quickly; complex programmatic layouts may require more significant restructuring.

What You Gain

  • Significantly reduced code complexity for headers and footers
  • HTML and CSS styling support
  • Automatic page number calculation
  • Cross-platform support (Windows, Linux, macOS, Docker)
  • No dependency on system fonts or libraries like libgdiplus

What to Consider

  • IronPDF requires learning HTML-to-PDF patterns if your team primarily used programmatic PDF construction
  • Very low-level PDF manipulation (byte-level editing, form field creation from scratch) may require different approaches
  • Existing iTextSharp utility libraries and extensions will need replacement or adaptation

Conclusion

The "Page X of Y" footer in iTextSharp requires implementing page event handlers, managing templates, and calculating coordinates manually. For developers spending hours on what should be a simple feature, IronPDF's placeholder-based approach offers a more straightforward alternative that reduces footer implementation from 60+ lines of event handler code to a single HTML fragment.


Jacob Mellor is CTO at Iron Software and the original developer of IronPDF.


References

  1. iTextSharp: Creating a footer Page # of #{:rel="nofollow"} - Original Stack Overflow question with 88,647 views
  2. Page events for headers and footers - iText 5 Documentation{:rel="nofollow"} - Official iText documentation on page events
  3. Adding page events to PdfWriter{:rel="nofollow"} - Detailed tutorial on PdfWriter page events
  4. Add page number in footer of pdf using iTextsharp{:rel="nofollow"} - Community tutorial with code examples
  5. How to get total page count on footer{:rel="nofollow"} - C# Corner forum discussion

For the latest IronPDF documentation and tutorials, visit ironpdf.com. The HTML Headers and Footers example provides additional code samples.

Top comments (0)