Recursive AI

Getting started with OnePDF

All onepdf docs

Install OnePDF for .NET 10, read text with word positions, edit pages, fill a form, sign a PDF, and generate an invoice with OnePdf.Layout.

OnePDF is a PDF library written in C# with no dependency beyond .NET itself. Every sample on this page was compiled and run against the 1.11.0 packages from NuGet.

Install

OnePDF targets .NET 10 (net10.0), so you need the .NET 10 SDK.

Terminal
dotnet new console -o PdfStart
cd PdfStart
dotnet add package RecursiveAI.OnePdf --version 1.11.0
dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0   # only for generating documents

The namespace is OnePdf. The core package has no dependencies: no native binaries and no other NuGet packages. It contains no unsafe code, and NativeAOT and trimming are supported. The license is MIT or Apache-2.0, at your choice.

The samples read and write files in the current directory. Put your own PDFs there under the names the code uses.

Read the text

C#
using OnePdf;
using OnePdf.Text;
 
using PdfDocument doc = PdfDocument.Open("report.pdf");
Console.WriteLine($"{doc.PageCount} pages, PDF {doc.Version}, title: {doc.Info.Title}");
 
// The text of every page, in reading order.
IReadOnlyList<PdfPageText> pages = doc.ExtractText();
Console.WriteLine(pages[0].Text);
 
// Words with their boxes, in PDF points with the origin at the lower left of the page.
PdfPageText first = doc.Pages[0].ExtractText();
foreach (PdfTextWord word in first.Words.Take(5))
    Console.WriteLine($"{word.Text,-10} left={word.Bounds.Left:F1} bottom={word.Bounds.Bottom:F1} width={word.Bounds.Width:F1}");

The start of the output, for a six-page report with a two-column section:

Text
6 pages, PDF 1.7, title: Harbor trial report
Harbor trial report
 
Week 1
 
Prepared by Jane Doe, account 4471-2290. The harbor trial ran for six weeks. Each crew logged its
...
Left column: crews on the north quay. Left column:
crews on the north quay. Left column: crews on the
...
Right column: crews on the south quay. Right column:
...
Harbor     left=54.0 bottom=727.7 width=27.5
trial      left=84.0 bottom=727.7 width=14.5
  • PdfDocument holds the file open and reads it as needed, so dispose it. It opens a path, a Stream or a ReadOnlyMemory<byte>. Page indexes are zero-based.
  • doc.ExtractText() reads the pages in parallel. Reading order comes from the structure tree when the file is tagged, and from the geometry of the page otherwise, so the two columns above come out one after the other instead of line by line. Right-to-left text comes out in logical order.
  • Text is one of three plain-text modes: ReadingOrder (the default), ContentOrder, and Layout, which keeps the arrangement of the page like pdftotext -layout. See Extract text from a PDF.
  • A word’s Bounds is in points (1/72 inch), with the origin at the lower left of the page and y going up, after the page’s rotation is applied. Blocks hold Lines, which hold Words.
  • OnePDF does no OCR. A scanned page without a text layer gives no text.

Merge, split, rotate and delete pages

C#
using OnePdf;
 
// Merge two files into a new document.
using (PdfDocument a = PdfDocument.Open("chapter-1.pdf"))
using (PdfDocument b = PdfDocument.Open("chapter-2.pdf"))
using (PdfDocument merged = PdfDocument.Merge([a, b]))
{
    merged.SetInfo("Title", "Chapters 1 and 2");
    merged.Save("chapters.pdf");
}
 
// Rotate and delete pages of an existing file. Page indexes are zero-based.
using (PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true }))
{
    doc.RotatePage(0, 90);       // clockwise, added to the current rotation
    doc.RemovePages([4, 5]);     // the last two pages
    doc.Save("report-edited.pdf");
}
 
// Split into files of two pages each.
using (PdfDocument doc = PdfDocument.Open("report.pdf"))
{
    int n = 1;
    foreach (PdfDocument part in PdfDocument.Split(doc, pagesPerPart: 2))
    {
        part.Save($"report-part-{n++}.pdf");
        part.Dispose();
    }
}
  • Documents open read-only. Pass AllowEditing = true to change one.
  • Merge and Split return new documents held in memory. They don’t refer to their sources, so the sources can be disposed straight away. Bookmarks, links, form fields and the tagged structure of the copied pages come along. Merge copies no title or other metadata, which is why the sample sets one.
  • After a page operation, read doc.Pages again. PdfPage objects taken before it are out of date.
  • Save writes a clean new file. SaveIncremental appends only the changes to the original bytes.

In this run chapters.pdf has 4 pages and all four bookmarks of its sources, report-edited.pdf has 4 pages with the first turned 90 degrees, and the report became three files of two pages.

Fill a form

C#
using OnePdf;
 
using PdfDocument doc = PdfDocument.Open("of345.pdf", new PdfOpenOptions { AllowEditing = true });
PdfAcroForm form = doc.Form ?? throw new InvalidOperationException("The file has no form.");
 
foreach (PdfFormField field in form.TerminalFields.Take(6))
    Console.WriteLine($"{field.FullName} ({field.Kind})");
 
form.SetValue("name", "Ada Lovelace");
form.SetValue("title", "Analyst");
form.SetChecked("xyes1", true);
doc.Save("of345-filled.pdf");
Text
name (Text)
dob (Text)
title (Text)
homeaddr (Text)
agcy (Text)
xyes1 (CheckBox)

The form here is OF 345, a US government form. Fields are addressed by their fully qualified names, which TerminalFields lists. Every value is checked before it is stored: an unknown field, a value that isn’t one of a list’s options, or text longer than the field allows throws PdfFormValueException and changes nothing. OnePDF also draws each filled field’s appearance, so the value shows in every viewer, not only in viewers that redraw fields themselves.

Field scripts never run, so calculated fields aren’t recomputed. XFA form data is read as raw XML and never filled. To fill a form and then flatten it into the page, see Fill and flatten form fields.

Sign and verify

C#
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using OnePdf;
 
// A self-signed certificate for trying this out. In production, load your own certificate with its private key.
using RSA key = RSA.Create(3072);
var request = new CertificateRequest("CN=Example Signer", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation, critical: true));
using X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
 
using (PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true }))
{
    doc.Sign("report-signed.pdf", PdfSigningCredential.FromCertificate(certificate), new PdfSigningOptions
    {
        FieldName = "Approval",
        Reason = "Approved for release",
        PageIndex = 5,
        Rectangle = new PdfRectangle(54, 400, 254, 460),   // omit for an invisible signature
    });
}
 
using (PdfDocument signed = PdfDocument.Open("report-signed.pdf"))
{
    PdfSignatureReport report = signed.VerifySignatures(new PdfSignatureVerifyOptions { TrustAnchors = [certificate] });
    foreach (PdfSignatureVerification v in report.Signatures)
        Console.WriteLine($"{v.Signature.FieldName}: valid={v.IsValid}, chain={v.Chain}, coverage={v.Coverage}");
}
Text
Approval: valid=True, chain=Trusted, coverage=WholeDocument
  • Signing is always an incremental update, so the original bytes stay an exact prefix of the output and earlier signatures stay valid.
  • The default format is PAdES baseline B-B (ETSI.CAdES.detached). Give PdfSigningOptions.Timestamp a callback that posts the request to your time-stamp authority and the signature becomes B-T. OnePDF doesn’t write long-term validation data (PAdES B-LT and B-LTA).
  • Verification never touches the network. A chain counts as trusted only when it leads to a certificate in TrustAnchors (or to the system store, with UseSystemTrustStore). The report also lists every change made after each signature.

In production, load your certificate with X509CertificateLoader.LoadPkcs12FromFile, or keep the key in a hardware module and sign through PdfSigningCredential.FromExternalSigner. More in Sign a PDF and verify its signatures.

Generate a document

RecursiveAI.OnePdf.Layout builds new documents from code. This one is a one-page invoice:

C#
using System.Globalization;
using OnePdf.Layout;
 
var us = CultureInfo.GetCultureInfo("en-US");
var lines = new (string Item, int Quantity, decimal Price)[]
{
    ("Design review", 2, 450.00m),
    ("Implementation, hours", 12, 120.00m),
    ("Hosting, October", 1, 80.00m),
};
decimal total = lines.Sum(l => l.Quantity * l.Price);
 
LayoutDocument.Compose(doc =>
{
    doc.Title("Invoice 2026-104").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.Letter);
        page.Margin(54);
        page.Content().Column(col =>
        {
            col.Spacing(14);
            col.Item().Row(row =>
            {
                row.RelativeItem().Heading(1).Text("Invoice").FontSize(26).Bold();
                row.RelativeItem().AlignEnd().Text("No. 2026-104\nOctober 26, 2026");
            });
            col.Item().Text("Bill to: Example Co., 100 Main Street, Springfield");
            col.Item().Table(table =>
            {
                table.Columns(c => { c.Relative(3); c.Constant(60); c.Constant(90); c.Constant(90); });
                table.CellPadding(5);
                table.CellBorder(0.5, Colors.LightGrey);
                table.HeaderRow(r =>
                {
                    r.Cell().Text("Item").Bold();
                    r.Cell().AlignEnd().Text("Qty").Bold();
                    r.Cell().AlignEnd().Text("Price").Bold();
                    r.Cell().AlignEnd().Text("Amount").Bold();
                });
                foreach (var l in lines)
                {
                    table.Row(r =>
                    {
                        r.Cell().Text(l.Item);
                        r.Cell().AlignEnd().Text(l.Quantity.ToString(us));
                        r.Cell().AlignEnd().Text(l.Price.ToString("C", us));
                        r.Cell().AlignEnd().Text((l.Quantity * l.Price).ToString("C", us));
                    });
                }
                table.FooterRow(r =>
                {
                    r.Cell().Span(3).AlignEnd().Text("Total").Bold();
                    r.Cell().AlignEnd().Text(total.ToString("C", us)).Bold();
                });
            });
        });
        page.Footer().AlignCenter().Text("Payment due within 30 days.").FontSize(9);
    });
}).Save("invoice.pdf");

pdftotext -layout reads the result back as:

Text
                                                                                       No. 2026-104
Invoice                                                                             October 26, 2026
 
Bill to: Example Co., 100 Main Street, Springfield
 
 Item                                                           Qty         Price          Amount
 
 Design review                                                        2   $450.00          $900.00
 
 Implementation, hours                                            12      $120.00        $1,440.00
 
 Hosting, October                                                     1    $80.00           $80.00
 
                                                                            Total        $2,420.00
  • Pages are described with nested delegates. Nothing is laid out until Save, which writes the file page by page.
  • Text is shaped (kerning, ligatures, Arabic, Indic and other complex scripts) and set in Liberation Sans by default, embedded as a subset. System fonts are never used unless you add them.
  • Output is tagged by default: the heading is an H1 and the table’s header cells are TH. That is what makes a PDF/UA save possible.
  • Header and footer rows repeat on every page a table reaches. See Generate a PDF with tables.

The packages

All four packages share one version number.

PackageWhat it isLicense
RecursiveAI.OnePdfThe library: open and repair, decrypt, extract text, edit pages, fill forms, draw on pages, redact, sign and verify, and save as PDF/A or PDF/UA. Namespace OnePdf.MIT or Apache-2.0
RecursiveAI.OnePdf.LayoutGenerates documents from code: shaped text, tables, lists, images, SVG, headers and footers, page numbers, tagged output. Namespace OnePdf.Layout. Depends on RecursiveAI.OnePdf, RecursiveAI.OnePdf.Fonts and RecursiveAI.OneFont.MIT or Apache-2.0
RecursiveAI.OnePdf.FontsFonts for documents that use the standard 14 fonts without embedding them: Liberation Sans, Serif and Mono, Noto Sans Math and Noto Sans Symbols. Layout uses them as its default fonts, and PDF/A and PDF/UA saves embed them for standard fonts OnePDF added. Namespace OnePdf.Fonts.MIT or Apache-2.0, fonts under the SIL Open Font License
RecursiveAI.OnePdf.HyphenationHyphenation patterns for Layout in 26 language variants, loaded on first use. Opt in with Hyphenator(Hyphenators.Auto()). Namespace OnePdf.Hyphenation.MIT or Apache-2.0, with the patterns' own MIT, BSD-3-Clause and Unlicense terms
Terminal
dotnet add package RecursiveAI.OnePdf.Fonts --version 1.11.0
dotnet add package RecursiveAI.OnePdf.Hyphenation --version 1.11.0

Layout brings in the Fonts package itself. Files you open that use the standard fonts without embedding them get their outlines from the package once you call OnePdf.Fonts.FontsPackage.Register() at startup, before opening documents.

What OnePDF doesn’t do

  • HTML to PDF. There is no HTML engine. Build documents with OnePdf.Layout instead.
  • Rendering pages to images. OnePDF turns a page into drawing calls for a device you supply. To get PNG or JPEG pages, use OneImage.Pdf: see PDF pages as images.
  • OCR. Scanned pages without a text layer give no text.

Where next

Cookbook recipes, each with a complete program:

Articles:

The reference docs: