§HOW-TO GUIDE · C# · .NET 8 & 10

Add page numbers to a PDF in C# (Page X of Y)

Turn on the converter's footer, add a PdfTextSection, and put {page_number} and {total_pages} in its text. The same placeholders work in headers and in templates added to a PDF you already have.

Compiled and run: Linux ARM64 · Linux x64 · macOS ARM64 · Windows x64

Video walkthrough · 2:21 · Watch on YouTube · Short · 0:29
§STEP BY STEP

Build it step by step.

  1. 1Install the NuGet packages

    SelectPdf.Universal is the library. The native package carries the Chromium engine for one platform: add the one for the platform you run on.

    terminal
    dotnet add package SelectPdf.Universal
    dotnet add package SelectPdf.Universal.Native.win-x64
    terminal
    dotnet add package SelectPdf.Universal
    dotnet add package SelectPdf.Universal.Native.linux-x64
    terminal
    dotnet add package SelectPdf.Universal
    dotnet add package SelectPdf.Universal.Native.linux-arm64
    terminal
    dotnet add package SelectPdf.Universal
    dotnet add package SelectPdf.Universal.Native.osx-arm64
  2. 2Create a console app

    Any .NET 8 or .NET 10 project works. To follow along, create a console app, run the install commands above inside its folder, then replace Program.cs with the code below.

    terminal
    dotnet new console -n PdfSample
    cd PdfSample
  3. 3Turn on the footer

    Program.cs · lines 3–7
    HtmlToPdf converter = new HtmlToPdf();
    
    // footer with "Page X of Y", right-aligned
    converter.Options.DisplayFooter = true;
    converter.Footer.Height = 40;

    Options.DisplayFooter enables the footer on every page, and Footer.Height reserves 40 points at the bottom of each page for it.

  4. 4Add the page-number text

    Program.cs · lines 9–13
    PdfTextSection pageNumbers = new PdfTextSection(0, 10,
        "Page {page_number} of {total_pages}  ",
        PdfFont.CreateFromSystemFont("Liberation Sans", 9f));
    pageNumbers.HorizontalAlign = PdfTextHorizontalAlign.Right;
    converter.Footer.Add(pageNumbers);

    PdfTextSection places text inside the footer at x = 0, y = 10. {page_number} and {total_pages} are replaced on each page. PdfFont.CreateFromSystemFont("Liberation Sans", 9f) uses a font bundled with the library, so the footer renders identically on every platform. HorizontalAlign = Right pushes it to the right edge; Footer.Add attaches it.

  5. 5Convert content long enough to span pages

    Program.cs · lines 15–19
    // enough content for three pages
    string rows = string.Concat(Enumerable.Range(1, 120).Select(i => $"<p>Line item {i}</p>"));
    PdfDocument doc = converter.ConvertHtmlString($"<h1>Report</h1>{rows}");
    doc.Save("report.pdf");
    doc.Close();

    120 line items fill several pages, so you can see the numbering change. ConvertHtmlString renders it, and the footer is applied to every page of report.pdf.

  6. 6Run it

    terminal
    dotnet run

    The program writes report.pdf to the working folder. Without a license key, every page carries a trial watermark; set GlobalProperties.LicenseKey before the first conversion to remove it.

§COMPLETE PROGRAM

The complete Program.cs.

This is the exact file our samples pipeline compiled and ran on Linux ARM64 and Linux x64 and macOS ARM64 and Windows x64 to produce the output on this page.

Program.cs
using SelectPdf.Universal;

HtmlToPdf converter = new HtmlToPdf();

// footer with "Page X of Y", right-aligned
converter.Options.DisplayFooter = true;
converter.Footer.Height = 40;

PdfTextSection pageNumbers = new PdfTextSection(0, 10,
    "Page {page_number} of {total_pages}  ",
    PdfFont.CreateFromSystemFont("Liberation Sans", 9f));
pageNumbers.HorizontalAlign = PdfTextHorizontalAlign.Right;
converter.Footer.Add(pageNumbers);

// enough content for three pages
string rows = string.Concat(Enumerable.Range(1, 120).Select(i => $"<p>Line item {i}</p>"));
PdfDocument doc = converter.ConvertHtmlString($"<h1>Report</h1>{rows}");
doc.Save("report.pdf");
doc.Close();

Open the project on GitHub → this program and the variations below, ready to dotnet run

§OUTPUT

The PDF it produces.

First page of report.pdf, the PDF this sample produces
report.pdf, page 1

Download report.pdf ↓ the file this sample produced, unedited

§VARIATIONS

Variations you will need next.

Each variation is a separate file in the same project, compiled and run with the program above.

Skip the header and footer on a cover page

DisplayOnFirstPage = false hides the header or footer on page 1. The cover still counts, so the second page reads "Page 2 of N". The header here is a PdfHtmlSection: any HTML and CSS, rendered by the same Chromium engine as the page. DisplayOnOddPages and DisplayOnEvenPages work the same way for book-style layouts.

SkipCoverPage.cs
using SelectPdf.Universal;

static class SkipCoverPage
{
    public static void Run()
    {
        HtmlToPdf converter = new HtmlToPdf();

        // HTML header on every page except the cover
        converter.Options.DisplayHeader = true;
        converter.Header.Height = 30;
        converter.Header.DisplayOnFirstPage = false;
        converter.Header.Add(new PdfHtmlSection(
            "<div style='font: 10px sans-serif; color: #555; padding-top: 10px'>Quarterly report</div>", ""));

        // footer: page number on every page except the cover, which still counts as page 1
        converter.Options.DisplayFooter = true;
        converter.Footer.Height = 40;
        converter.Footer.DisplayOnFirstPage = false;
        PdfTextSection pageNumbers = new PdfTextSection(0, 10,
            "Page {page_number} of {total_pages}  ",
            PdfFont.CreateFromSystemFont("Liberation Sans", 9f));
        pageNumbers.HorizontalAlign = PdfTextHorizontalAlign.Right;
        converter.Footer.Add(pageNumbers);

        string rows = string.Concat(Enumerable.Range(1, 120).Select(i => $"<p>Line item {i}</p>"));
        string html = $"<h1 style='page-break-after: always'>Quarterly report</h1>{rows}";

        PdfDocument doc = converter.ConvertHtmlString(html);
        doc.Save("report-with-cover.pdf");
        doc.Close();
    }
}

Add page numbers to an existing PDF

For a PDF you did not generate, PdfResizeManager shrinks the content to free 40 points at the bottom of every page, and a template placed in that strip repeats on each page. {page_number} and {total_pages} work in a PdfTextElement inside a template too.

NumberExistingPdf.cs
using SelectPdf.Universal;
using SelectPdf.Universal.Drawing;

static class NumberExistingPdf
{
    public static void Run()
    {
        // an existing PDF without page numbers (made here so the sample runs on its own)
        HtmlToPdf converter = new HtmlToPdf();
        string rows = string.Concat(Enumerable.Range(1, 120).Select(i => $"<p>Line item {i}</p>"));
        PdfDocument source = converter.ConvertHtmlString($"<h1>Report</h1>{rows}");
        source.Save("unnumbered.pdf");
        source.Close();

        // shrink the content to free 40 points at the bottom of every page
        PdfResizeManager resizer = new PdfResizeManager();
        resizer.Load("unnumbered.pdf");
        resizer.PageMargins = new PdfMargins(0, 0, 0, 40);
        PdfDocument doc = resizer.GetDocument();

        // a template docked in that space repeats on every page
        float width = doc.Pages[0].ClientRectangle.Width;
        float height = doc.Pages[0].ClientRectangle.Height;
        PdfTemplate footer = doc.Templates.Add(new RectangleF(0, height - 40, width, 40));

        PdfFont font = doc.Fonts.Add(PdfStandardFont.Helvetica);
        font.Size = 9;
        footer.Add(new PdfTextElement(width - 90, 14, "Page {page_number} of {total_pages}", font));

        doc.Save("numbered.pdf");
        resizer.Close();
    }
}
§TROUBLESHOOTING

Common problems and fixes.

The page number overlaps the content

Footer.Height is the space reserved at the bottom of every page. Make it taller than the text you place in it, and keep the section's y offset plus its font size inside that height.

The numbering should continue from another document

Footer.FirstPageNumber sets the number of the first page, and Footer.TotalPagesOffset adds to {total_pages}. Use them when the PDF will be merged after other pages.

The footer does not appear

Adding sections to converter.Footer is not enough: converter.Options.DisplayFooter must be true too. Headers work the same way with Options.DisplayHeader.

§GOOD TO KNOW

Good to know.

  • The {page_number} placeholder shows the page number and {total_pages} the total number of pages. Source
  • Headers and footers can also hold HTML (PdfHtmlSection) and images (PdfImageSection). Source
  • To add a footer to an existing PDF, PdfResizeManager first makes room on each page, then a template holds the footer. Source
?FAQ

Questions, answered.

Short answers about this guide. The SelectPdf.Universal documentation covers every class and option.

Set converter.Options.DisplayFooter to true, give the footer a height, and add a PdfTextSection whose text contains {page_number} and {total_pages}. SelectPdf.Universal replaces both placeholders on every page.
Yes. Add a PdfHtmlSection to the header or footer with an HTML string or a URL. It is rendered with the same Chromium engine as the page, so CSS and web fonts work.
Set converter.Footer.DisplayOnFirstPage to false. The first page still counts, so numbering on page 2 starts at 2.
Yes. Load it with PdfResizeManager to make room at the bottom of each page, then add a template with a PdfTextElement that contains {page_number} and {total_pages}.