---
title: "Create a PDF in C#"
url: https://recursiveai.org/onepdf/articles/create-pdf-csharp/
description: "Create PDF files in C# from code with OnePdf.Layout: text, tables that break across pages, SVG logos, page X of Y, ASP.NET Core downloads and PDF/A. No HTML."
date: 2026-10-26
author: OnePDF team
---

# Create a PDF in C#

Create PDF files in C# from code with OnePdf.Layout: text, tables that break across pages, SVG logos, page X of Y, ASP.NET Core downloads and PDF/A. No HTML.

October 26, 2026 · OnePDF team

To create a PDF in C#, add the `RecursiveAI.OnePdf.Layout` package, describe the document with `LayoutDocument.Compose` and call `Save`. OnePdf.Layout is a code-first PDF generator: you build pages from text, rows, columns, tables, images and SVG, and it handles line breaking, page breaks, repeated table headers and page numbers. It writes the file page by page. It is pure C# with no browser and no native binaries, licensed MIT OR Apache-2.0. This page builds a first PDF, a multi-page invoice, an ASP.NET Core download and a PDF/A archive copy. Every sample was compiled and run against OnePDF 1.11.0 on .NET 10.

## Create your first PDF

*Terminal · NuGet*

```
dotnet new console -o PdfCreate
cd PdfCreate
dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0
```

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf.Layout;
 
LayoutDocument.Compose(doc =>
{
    doc.Title("Hello").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.Letter);
        page.Margin(72);
        page.Content().Text("Hello, PDF").FontSize(24);
    });
}).Save("hello.pdf");
```

This writes a one-page US Letter `hello.pdf`. A few things happen without being asked:

- The text is set in Liberation Sans, which comes with the package, and only the glyphs used are embedded. System fonts are never used unless you add them, so the output is the same on Windows, Linux, macOS and in containers.
- The PDF is tagged: headings, paragraphs, tables and figures are marked for screen readers. `Title` and `Language` set the document’s title and language.
- Nothing is laid out until `Save`, which writes the file one page at a time, so long documents don’t build up in memory.

Units are points (1/72 inch). `PageSize` has the usual sizes (`Letter`, `A4` and others), and `10.Mm()`, `2.Cm()` and `1.Inch()` convert.

## Generate an invoice with a table that breaks across pages

A real document needs a header on every page, a table that continues onto the next page with its header row repeated, and “Page 1 of 2” in the footer. This invoice has 45 lines, a vector logo and a total:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using System.Globalization;
using OnePdf.Layout;
 
var us = CultureInfo.GetCultureInfo("en-US");
 
// 45 line items, enough to run onto a second page.
var items = Enumerable.Range(1, 45)
    .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();                             // center the items of the row vertically
            row.ConstantItem(40).Svg(logo);                // vector logo, 40 pt wide
            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.RowBackground(i => i % 2 == 1 ? LayoutColor.FromHex("#F3F6F9") : null);
 
                table.HeaderRow(r =>                       // repeated at the top of 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));
                        r.Cell().AlignEnd().Text((item.Hours * item.Rate).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 ");
            t.TotalPages().ReservedDigits(1);              // room for the total, which is known only at the end
        });
    });
}).Save("invoice.pdf");
```

The result is a two-page US Letter PDF of about 30 KB. The logo and company header sit at the top of both pages, and the table runs from page 1 to page 2 with the “Description, Hours, Rate, Amount” row repeated. The footer reads “Page 1 of 2” and “Page 2 of 2”, and page 2 ends with “Total due: $10,640.00”.

How the pieces fit:

- **Slots.** `Header`, `Content` and `Footer` are slots, and every slot takes one thing: `Text`, `Column`, `Row`, `Table`, `Image`, `Svg` and so on. Decorators such as `PaddingBottom`, `AlignEnd` or `Width` go in front of it.
- **Rows and columns.** `ConstantItem(points)` has a fixed width and `RelativeItem()` shares what is left. Table columns work the same way with `Constant` and `Relative`.
- **Tables.** `HeaderRow` repeats at the top of every page the table reaches. `FooterRow` repeats at the bottom, and body rows split between pages. `RowBackground` stripes the rows. See [generate a PDF with tables](https://recursiveai.org/onepdf/cookbook/generate-tables/) for spans, footers and column-width rules.
- **SVG.** `Svg` draws the logo as vector paths, with its `<title>` as alternative text. A PNG or JPEG goes in with `Image(path)` instead. [SVG in layout](https://recursiveai.org/onepdf/docs/layout-svg/) lists what is supported.
- **Page numbers.** The total page count isn’t known until the end, so `ReservedDigits` sets aside room for it. Reserve as many digits as the count can have. Extra room shows as a gap before the number.
- **Vertical alignment.** Put `AlignMiddle()` on the row, as above, and not on an item. On a slot, `AlignMiddle` makes it as tall as the space offered, and in a header that is the whole page.

For content that repeats, implement `IComponent` and place it with `Component(...)`. Running totals per page (“carried forward”), tables of contents, lists, links and bookmarks are in the [layout guide](https://recursiveai.org/onepdf/docs/layout/).

## Return a PDF from ASP.NET Core

`ToBytes()` gives you the finished file as a byte array, ready for `Results.File`. Create the project with `dotnet new web` and add the package:

*Program.cs · OnePDF 1.11.0 · ASP.NET Core 10*

```
using OnePdf.Layout;
 
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
app.MapGet("/invoices/{number}", (string number) =>
{
    byte[] pdf = LayoutDocument.Compose(doc =>
    {
        doc.Title($"Invoice {number}").Language("en-US");
        doc.Page(page =>
        {
            page.Size(PageSize.Letter);
            page.Margin(54);
            page.Content().Text($"Invoice {number}").FontSize(24).Bold();
        });
    }).ToBytes();
 
    return Results.File(pdf, "application/pdf", $"invoice-{number}.pdf");
});
 
app.Run();
```

A request to `/invoices/2026-118` returns `200 OK` with `Content-Type: application/pdf` and `Content-Disposition: attachment; filename=invoice-2026-118.pdf`, and the body is a one-page PDF titled “Invoice 2026-118”. To show the PDF in the browser instead of downloading it, leave out the file name. The same `Compose` and `ToBytes` calls work anywhere .NET 10 runs, such as a Blazor Server app or a background service.

Layout objects are independent, so one server can generate many documents at once. In the project’s benchmark, 32 invoices generated concurrently took 9.99 ms in total (see the numbers [below](#benchmarks)).

## Fonts, Arabic, Hindi and other scripts

Register your own TrueType or OpenType fonts on `doc.Fonts` with `doc.Fonts.Add(File.ReadAllBytes("Brand-Regular.ttf"), "Brand")`, then select them with `FontFamily("Brand")`. OnePdf.Layout shapes text through the OneFont engine, so kerning and ligatures are applied, and Arabic is joined right to left and Devanagari conjuncts form. Fallback families cover characters your main font lacks. The [Arabic and Hindi recipe](https://recursiveai.org/onepdf/cookbook/arabic-hindi-text/) is a complete program. For hyphenation in 26 language variants, add `RecursiveAI.OnePdf.Hyphenation` and opt in with `Hyphenator(Hyphenators.Auto())`.

## Save as PDF/A for archiving

The same document can be saved as PDF/A-2b, a profile of PDF for long-term archiving. `Save(stream, PdfSaveOptions)` lays the document out and then writes it with the conformance options:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
using OnePdf.Layout;
 
LayoutDocument report = LayoutDocument.Compose(doc =>
{
    doc.Title("Archive copy").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.Letter);
        page.Margin(72);
        page.Content().Text("This copy is saved as PDF/A-2b for long-term archiving.");
    });
});
 
using FileStream file = File.Create("archive.pdf");
report.Save(file, new PdfSaveOptions
{
    PdfA = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.B },
});
```

We checked `archive.pdf` with the veraPDF 1.30.3 validator, and it passes as PDF/A-2b. OnePDF embeds the fonts, writes the XMP metadata and adds an sRGB output intent itself. If a document can’t conform, the save is refused with every violation listed. Because layout output is tagged, the same approach gives PDF/UA for accessibility: see [create PDF/UA](https://recursiveai.org/onepdf/cookbook/create-pdf-ua/) and [create PDF/A](https://recursiveai.org/onepdf/cookbook/create-pdf-a/), including PDF/A-3 with attached files.

## What about HTML to PDF?

OnePDF has no HTML engine, so it doesn’t convert HTML or Razor views to PDF. If your templates are already HTML and CSS, you need a library with a browser engine inside: IronPDF and iText’s pdfHTML add-on are two, both commercial or AGPL (see [choosing a C# PDF library](https://recursiveai.org/onepdf/articles/csharp-pdf-library/) for their terms). The code-first approach above needs no browser: page breaks, repeated headers and page numbers are part of the layout model.

## OnePdf.Layout, QuestPDF and MigraDoc

OnePdf.Layout uses the same nested-delegate style as QuestPDF. If you know QuestPDF, [migrating from QuestPDF](https://recursiveai.org/onepdf/docs/migrating-from-questpdf/) maps the calls. The differences that usually decide are the license and the dependencies:

- **QuestPDF** is free under its Community License for eligible users, including organizations under USD 1 million in annual gross revenue. Others need a paid license from $1,999 ([pricing](https://www.questpdf.com/pricing.html)). The 2026.9.1 package carries native Skia and qpdf libraries for eight runtimes.
- **MigraDoc** 6.5.0 is AGPL or commercial (€750, [pricing](https://pdfsharp.com/Pricing)). Version 6.2.4 and earlier are MIT.
- **OnePdf.Layout** is MIT OR Apache-2.0 with no native code. It depends on `RecursiveAI.OnePdf`, `RecursiveAI.OnePdf.Fonts` and `RecursiveAI.OneFont`, all from the same project. It targets .NET 10 only and supports NativeAOT.

The project’s [published benchmark](https://github.com/recursive-ai-oss/onepdf-csharp/blob/v1.11.0/README.md#layout-1) generated the same documents with OnePdf.Layout and MigraDoc 6.2.4, on an AMD Ryzen 9 7950X3D under Linux with .NET 10.0.13. Times are BenchmarkDotNet means.

| Document | OnePdf.Layout | MigraDoc 6.2.4 |
|---|---|---|
| Invoice, 1 page | 1.01 ms | 1.30 ms |
| Report, 50 pages (tables, images, headers, page X of Y) | 37.1 ms | 175 ms |
| Table, 10,000 rows (about 250 pages, repeated header) | 362 ms | 1,191 ms |
| Book, 500 pages, justified | 317 ms | 496 ms |
| 32 invoices concurrently | 9.99 ms | 51.7 ms |

Published as NativeAOT, OnePDF generates its first invoice in a fresh process in 4 ms with a 12 MiB peak. That matters for serverless functions and command-line tools, where every run is a cold start.

## Where next

- [Getting started with OnePDF](https://recursiveai.org/onepdf/docs/getting-started/), including reading, editing, forms and signing.
- [Generate a PDF with tables](https://recursiveai.org/onepdf/cookbook/generate-tables/), [Arabic and Hindi text](https://recursiveai.org/onepdf/cookbook/arabic-hindi-text/), [PDF/UA](https://recursiveai.org/onepdf/cookbook/create-pdf-ua/).
- The [layout guide](https://recursiveai.org/onepdf/docs/layout/) and [layout text](https://recursiveai.org/onepdf/docs/layout-text/) for every option.
- [Extract text from a PDF in C#](https://recursiveai.org/onepdf/articles/extract-text-from-pdf-csharp/), to read documents back.
