---
title: "Create a PDF in C#"
url: https://recursiveai.org/onepdf/cookbook/create-pdf/
description: "Create and generate PDF files in C# from code: text, tables that break across pages, headers, page numbers and logos. Runnable code with OnePdf.Layout."
version: "1.15.0"
---

# Create a PDF in C#

Add the OnePdf.Layout package, describe the document with `LayoutDocument.Compose` and call `Save`. The layout engine handles line breaks, page breaks and page numbers, and needs no browser or native code.

```shell
dotnet new console -o CreatePdf && cd CreatePdf
dotnet add package RecursiveAI.OnePdf.Layout --version 1.15.0
```

Put this in `Program.cs` and run `dotnet run`:

```csharp
using OnePdf.Layout;

LayoutDocument.Compose(doc =>
{
    doc.Title("Hello").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.Letter);
        page.Margin(72);                                   // points: 72 = 1 inch
        page.Content().Text("Hello, PDF").FontSize(24);
    });
}).Save("hello.pdf");

Console.WriteLine($"hello.pdf: {new FileInfo("hello.pdf").Length:N0} bytes");
```

```text
hello.pdf: 5,229 bytes
```

`hello.pdf` is one US Letter page. The text is set in Liberation Sans, which comes with the package, and only the letters used are embedded, so the file looks the same on every machine. The PDF is tagged for screen readers, with the title and language you set.

## Generate a real document: an invoice

A header on every page, a table that continues onto the next page with its header row repeated, a vector logo, and “Page 1 of 2” in the footer:

```csharp
using System.Globalization;
using OnePdf.Layout;

var us = CultureInfo.GetCultureInfo("en-US");
var items = Enumerable.Range(1, 45)   // 45 lines: enough for two pages
    .Select(i => (Description: $"Support hours, ticket #{4100 + i}",
                  Hours: 1 + i % 4, Rate: 95m))
    .ToList();
decimal total = items.Sum(x => x.Hours * x.Rate);

const string logo = """
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
      <title>Northwind Supply</title>
      <rect width="40" height="40" rx="8" fill="#2F5D8A"/>
      <path d="M10 30 V10 L30 30 V10" stroke="#FFFFFF" stroke-width="4" fill="none"/>
    </svg>
    """;

LayoutDocument.Compose(doc =>
{
    doc.Title("Invoice 2026-118").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.Letter);
        page.Margin(54);

        page.Header().PaddingBottom(16).Row(row =>
        {
            row.Spacing(10);
            row.AlignMiddle();
            // 40 points wide, as vector paths
            row.ConstantItem(40).Svg(logo);
            row.RelativeItem().Text("Northwind Supply").FontSize(14).Bold();
            row.RelativeItem().AlignEnd().Text("Invoice 2026-118\nOctober 26, 2026");
        });

        page.Content().Column(col =>
        {
            col.Spacing(12);
            col.Item().Text("Bill to: Example Co., 100 Main Street, Springfield");
            col.Item().Table(table =>
            {
                table.Columns(c =>
                {
                    c.Relative(1);
                    c.Constant(50);
                    c.Constant(70);
                    c.Constant(80);
                });
                table.CellPadding(4);
                table.CellBorder(0.5, Colors.LightGrey);
                table.HeaderRow(r =>                      // repeated on every page
                {
                    r.Cell().Text("Description").Bold();
                    r.Cell().AlignEnd().Text("Hours").Bold();
                    r.Cell().AlignEnd().Text("Rate").Bold();
                    r.Cell().AlignEnd().Text("Amount").Bold();
                });
                foreach (var item in items)
                    table.Row(r =>
                    {
                        r.Cell().Text(item.Description);
                        r.Cell().AlignEnd().Text(item.Hours.ToString(us));
                        r.Cell().AlignEnd().Text(item.Rate.ToString("C", us));
                        decimal amount = item.Hours * item.Rate;
                        r.Cell().AlignEnd().Text(amount.ToString("C", us));
                    });
            });
            col.Item().AlignEnd().Text($"Total due: {total.ToString("C", us)}")
                .FontSize(13).Bold();
        });

        page.Footer().AlignCenter().Text(t =>
        {
            t.FontSize(9);
            t.Span("Page ");
            t.CurrentPageNumber();
            t.Span(" of ");
            // room for the total, known only at the end
            t.TotalPages().ReservedDigits(1);
        });
    });
}).Save("invoice.pdf");

using var pdf = OnePdf.PdfDocument.Open("invoice.pdf");
Console.WriteLine($"invoice.pdf: {pdf.PageCount} pages");
string[] lastPage = pdf.Pages[^1].ExtractText().Text.Split('\n');
Console.WriteLine(lastPage.Last(line => line.StartsWith("Total")));
```

```text
invoice.pdf: 2 pages
Total due: $10,640.00
```

Both pages carry the logo and header, the table runs from page 1 onto page 2 with its header row repeated, and the footers read “Page 1 of 2” and “Page 2 of 2”.

## How it fits together

- **Slots.** `Header`, `Content` and `Footer` each take one thing: `Text`, `Column`, `Row`, `Table`, `Image`, `Svg` and so on. Modifiers such as `PaddingBottom` or `AlignEnd` go in front.
- **Widths.** `ConstantItem(points)` and `Constant` are fixed; `RelativeItem()` and `Relative` share what is left.
- **Images.** `Image(bytes)` takes a PNG or JPEG; `Svg(markup)` draws vector art, its `<title>` becoming the alternative text.
- **In a web app**, `ToBytes()` gives the finished file: `return Results.File(doc.ToBytes(), "application/pdf", "invoice.pdf");`
- **Long documents.** `Save` writes page by page, so memory stays small.

## Limits

- **No HTML.** OnePdf.Layout builds documents from code. To turn HTML into a PDF, use OneRender: see [Save as PDF](https://recursiveai.org/onepdf/cookbook/save-as-pdf/).
- **Fonts.** Liberation Sans is the default and system fonts are never used. Register your own with `doc.Fonts.Add(bytes, "Family")`. Arabic, Hindi and other complex scripts need a font that covers them: see [Arabic and Hindi text](https://recursiveai.org/onepdf/cookbook/arabic-hindi-text/).
- **.NET 10.** The packages target .NET 10 only, and support NativeAOT.

More tables in [Generate a PDF table](https://recursiveai.org/onepdf/cookbook/generate-tables/); every option in [Layout](https://recursiveai.org/onepdf/docs/layout/). Coming from QuestPDF? The calls are nearly the same: see [From QuestPDF](https://recursiveai.org/onepdf/docs/migrating-from-questpdf/). To compare libraries and licenses, see [Choosing a C# PDF library](https://recursiveai.org/onepdf/articles/csharp-pdf-library/).
