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.
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
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:
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
PdfDocumentholds the file open and reads it as needed, so dispose it. It opens a path, aStreamor aReadOnlyMemory<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.Textis one of three plain-text modes:ReadingOrder(the default),ContentOrder, andLayout, which keeps the arrangement of the page likepdftotext -layout. See Extract text from a PDF.- A word’s
Boundsis 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.BlocksholdLines, which holdWords. - OnePDF does no OCR. A scanned page without a text layer gives no text.
Merge, split, rotate and delete pages
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 = trueto change one. MergeandSplitreturn 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.Mergecopies no title or other metadata, which is why the sample sets one.- After a page operation, read
doc.Pagesagain.PdfPageobjects taken before it are out of date. Savewrites a clean new file.SaveIncrementalappends 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
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");
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
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}"); }
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). GivePdfSigningOptions.Timestampa 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, withUseSystemTrustStore). 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:
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:
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
H1and the table’s header cells areTH. 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.
| Package | What it is | License |
|---|---|---|
RecursiveAI.OnePdf | The 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.Layout | Generates 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.Fonts | Fonts 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.Hyphenation | Hyphenation 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 |
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:
- Extract text from a PDF and get words with their positions
- Merge PDFs, split a PDF, rotate, delete and reorder pages
- Fill and flatten form fields, add an image or a watermark, redact text
- Sign a PDF and verify its signatures, decrypt or encrypt a PDF, repair a damaged PDF
- Create PDF/A, create PDF/UA, generate a PDF with tables, Arabic and Hindi text
Articles:
- Choosing a C# PDF library: license, price and native code compared
- Extract text from PDF in C# and create PDF invoices and reports in C#
- Moving from iText: the same five jobs side by side
The reference docs:
- Text layout and reading order and the JSON form of extracted text
- Writing, page operations, forms, content creation, redaction
- Signatures, repair rules, PDF/A, PDF/UA, tagging existing files
- Layout, layout text, SVG in layout
- Moving from another library: PdfPig, PDFsharp, QuestPDF