C# / .NET
The RustPdf NuGet package wraps the rust-pdf C core with idiomatic, chainable classes over source-generated P/Invoke (LibraryImport). It covers the whole product surface: vector graphics, embedded/subset fonts & Unicode text, paragraphs, images, PDF/A (1b–3a), tagged/accessible output, attachments, AcroForm fields, manipulation, text extraction, encryption and digital signatures. The native library ships inside the package for every runtime — no native build.
Document authors a new PDF; EditableDoc loads and manipulates an existing one. Both implement IDisposable and hold a native handle, so wrap them in using to free it promptly.Installation
Install from NuGet. The native library (libpdf_ffi) is bundled as a per-runtime asset (runtimes/<rid>/native/ for osx-arm64, linux-x64, linux-arm64 and win-x64): .NET resolves the one matching your OS/architecture automatically, so there's nothing to compile.
dotnet add package RustPdfTargets net8.0 (works on .NET 8+). Verify it loaded:
using RustPdf;
Console.WriteLine(Pdf.Version()); // native library version-r <rid> (e.g. dotnet publish -r linux-x64) so the matching native asset is copied into the output. RUSTPDF_LIB can point at an explicit library path to override resolution.Quick start
A one-page document with a filled rectangle, saved to disk:
using RustPdf;
using var doc = new Document(); // A4 by default
doc.AddPage();
doc.SetFillRgb(0.86, 0.20, 0.18);
doc.Rect(72, 640, 200, 120); // x, y, width, height (points)
doc.Fill();
doc.Save("out.pdf");Most methods return the document, so calls chain:
using var doc = new Document();
int font = doc.AddFontFile("Roboto-Regular.ttf");
doc.AddPage()
.SetFillRgb(0.1, 0.1, 0.12)
.Rect(0, 800, 595, 42).Fill()
.ShowText(font, 24, 72, 740, "Olá, açúcar — café");
byte[] data = doc.ToBytes(); // a byte[] instead of a fileLicensing & activation
Basic generation (everything above) is always free. The corporate features (PDF/A, digital signatures/PAdES, encryption, accessibility) require an active license token. Without one, those calls throw PdfException and produce no output.
Activation needs no rebuild. Easiest is an environment variable, auto-activated the first time a corporate feature is used:
export RUSTPDF_LICENSE="010f0000…" # the token we email you
# or point at a file:
export RUSTPDF_LICENSE_FILE=/etc/rustpdf/license.txtOr activate explicitly in code:
using RustPdf;
Pdf.ActivateLicense(token); // throws PdfException if forged / expired / malformedCoordinate system
- Units are points (1 pt = 1/72 inch). A4 is
595 × 842, US Letter612 × 792. - The origin
(0, 0)is the bottom-left corner;ygrows upward. - For text,
(x, y)is the baseline of the first glyph. - Drawing/text always targets the most recently added page.
Threading & concurrency
Every native call is synchronous and the core is Send but not Sync: a single handle must never be touched by two threads at once.
- Documents are independent. Each
Document/EditableDocshares no state, so they're safe to build in parallelTasks or threads — one handle per task. - Don't share a live handle across threads; give each its own.
- Errors are per-thread. The native last-error is thread-local, so a failure on one thread never clobbers another's, and
PdfExceptionsurfaces on the calling side. - License is process-global.
Pdf.ActivateLicense(or the env var) applies to every thread; activate once at startup.
// one document per task — no shared handle
var jobs = Enumerable.Range(0, 8).Select(i => Task.Run(() =>
{
using var doc = new Document();
doc.AddPage().SetFillRgb(0.1, 0.1, 0.12).Rect(72, 700, 200, 80).Fill();
return doc.ToBytes();
}));
byte[][] pdfs = await Task.WhenAll(jobs);Authoring: create & save
new Document() freeCreates an empty document (A4 default page size). Wrap in using (or call Dispose()) to free the native handle.
| Member | Description |
|---|---|
AddPage((w, h)?) | Append a page. Optional size tuple in points. |
SetDefaultSize(w, h) | Default size for subsequently added pages. |
SetVersion(v) | Set the PDF header version (0 → 1.4, 1 → 1.5, 2 → 1.7). |
PageCount | Property: number of pages so far. |
ToBytes() | Render the document to a byte[]. |
Save(path) | Render and write to a file. |
Dispose() | Free the native handle. |
Pages & vector graphics
Graphics state and path operators mirror PDF's content-stream model. Colors are RGB in 0.0–1.0.
| Method | Description |
|---|---|
SetFillRgb(r, g, b) | Fill color. |
SetStrokeRgb(r, g, b) | Stroke color. |
SetLineWidth(w) | Stroke width in points. |
Rect(x, y, w, h) | Add a rectangle subpath. |
Fill() | Fill the current path with the fill color. |
Stroke() | Stroke the current path with the stroke color. |
using var doc = new Document();
doc.AddPage();
doc.SetStrokeRgb(0.10, 0.45, 0.90).SetLineWidth(3);
doc.Rect(72, 600, 300, 160).Stroke();
doc.SetFillRgb(0.95, 0.77, 0.06);
doc.Rect(120, 640, 120, 80).Fill();
doc.Save("shapes.pdf");Fonts & text
Fonts are embedded and subsetted, with HarfBuzz-quality shaping, kerning and full Unicode (Type0/CIDFontType2 with ToUnicode, so text extracts and copies correctly). Register a font once, then reference it by its integer id.
AddFontFile(path) → int AddFont(byte[] data) → intShowText(font, size, x, y, text, headingLevel = 0)headingLevel (1–6) tags the run as H1–H6 in an accessible document (see Accessibility); leave it as 0 for ordinary text.
using var doc = new Document();
int regular = doc.AddFontFile("Roboto-Regular.ttf");
// …or from bytes you already have in memory:
// int regular = doc.AddFont(File.ReadAllBytes("Roboto-Regular.ttf"));
doc.AddPage();
doc.ShowText(regular, 28, 72, 760, "Invoice #1024");
doc.ShowText(regular, 12, 72, 720, "日本語 · Ελληνικά · العربية");
doc.Save("text.pdf");Paragraphs
The paragraph layer wraps, aligns and justifies text inside a fixed width (greedy line breaking using shaped glyph widths).
Paragraph(font, size, x, y, width, text, align = Align.Left)using RustPdf;
string intro =
"A long paragraph that wraps to the given width and is justified " +
"automatically; extra space is distributed between words.";
using var doc = new Document();
int f = doc.AddFontFile("Roboto-Regular.ttf");
doc.AddPage();
doc.Paragraph(f, 12, 72, 700, 451, intro, Align.Justify);
doc.Save("paragraph.pdf");See the Align enum for the alignment options.
Images
JPEGs are embedded verbatim (DCTDecode, no re-encode). PNGs are decoded and re-encoded (FlateDecode); alpha becomes an /SMask, palette becomes an Indexed color space. Register an image once, draw it many times.
| Method | Description |
|---|---|
AddImageFile(path) → int | Load JPEG/PNG from a file; returns the image id. |
AddImagePng(byte[] data) → int | Register a PNG from memory. |
AddImageJpeg(byte[] data) → int | Register a JPEG from memory. |
DrawImage(image, x, y, w, h) | Draw at (x, y) scaled to w × h points. |
Figure(image, x, y, w, h, alt) | Draw as a tagged /Figure with alt text (accessibility). |
using var doc = new Document();
int logo = doc.AddImageFile("logo.png");
doc.AddPage();
doc.DrawImage(logo, 72, 680, 160, 90);
doc.Save("with_image.pdf");PDF/A licensed
Produce archival-grade output. Pdfa() defaults to A-2b; pass a PdfaLevel for a specific level. An embedded sRGB ICC profile, output intent, XMP metadata and document /ID are added automatically; A-1b also forces PDF 1.4 and emits a /CIDSet.
Pdfa(PdfaLevel? level = null)using RustPdf;
using var doc = new Document();
doc.Pdfa(PdfaLevel.A2b).SetInfo(title: "Q3 Report", author: "Acme Inc.");
int f = doc.AddFontFile("Roboto-Regular.ttf");
doc.AddPage();
doc.ShowText(f, 20, 72, 760, "Archival report");
doc.Save("report_pdfa.pdf"); // throws PdfException without a license granting PDF/ASetInfo(title: …).Accessibility (Tagged PDF / PDF/UA) licensed
Tagged() builds a logical structure tree (PDF/UA-1). Combine with Pdfa(PdfaLevel.A2a) for archival and accessible output. Use headingLevel on ShowText for H1–H6, and Figure(..., alt) for described images.
Tagged()using RustPdf;
using var doc = new Document();
doc.Pdfa(PdfaLevel.A2a).Tagged().SetInfo(title: "Accessible report");
int f = doc.AddFontFile("Roboto-Regular.ttf");
doc.AddPage();
doc.ShowText(f, 26, 72, 760, "Annual report", headingLevel: 1); // H1
doc.ShowText(f, 14, 72, 720, "Overview", headingLevel: 2); // H2
doc.ShowText(f, 11, 72, 690, "Body paragraph of the section…");
int chart = doc.AddImageFile("chart.png");
doc.Figure(chart, 72, 520, 300, 150, "Revenue grew 18% year over year");
doc.Save("accessible.pdf");Attachments (PDF/A-3) licensed
PDF/A-3 allows embedding arbitrary source files (e.g. the XML behind an e-invoice). Each attachment carries a MIME type and an AFRelationship.
AttachFile(name, mime, byte[] data, relationship = AFRelationship.Source, description = "")using RustPdf;
byte[] xml = File.ReadAllBytes("invoice.xml");
using var doc = new Document();
doc.Pdfa(PdfaLevel.A3b).SetInfo(title: "E-invoice 1024");
int f = doc.AddFontFile("Roboto-Regular.ttf");
doc.AddPage();
doc.ShowText(f, 18, 72, 760, "Invoice 1024");
doc.AttachFile("invoice.xml", "text/xml", xml,
AFRelationship.Source, "Structured invoice data");
doc.Save("einvoice.pdf");AcroForm fields
Build interactive forms with generated appearance streams (no NeedAppearances). Rectangles are (x0, y0, x1, y1) tuples; page is a 0-based page index. Dotted names ("a.b.c") create hierarchical fields.
| Method | Description |
|---|---|
TextField(name, page, rect, value = "", size = 0) | Text input (size = 0 → auto font size). |
Checkbox(name, page, rect, checkedFlag) | Checkbox. |
Dropdown(name, page, rect, options, selected = null, size = 0) | Combo box from a sequence of strings. |
RadioGroup(name, page, buttons, selected = null) | buttons = list of (rect, export) tuples. |
using var doc = new Document();
doc.AddPage();
doc.TextField("applicant.name", 0, (72, 700, 320, 720));
doc.Checkbox("agree", 0, (72, 660, 88, 676), false);
doc.Dropdown("plan", 0, (72, 620, 240, 640),
new[] { "Starter", "Pro", "Enterprise" }, selected: 1);
doc.RadioGroup("billing", 0, new[]
{
((72.0, 580.0, 88.0, 596.0), "monthly"),
((140.0, 580.0, 156.0, 596.0), "annual"),
}, selected: 1);
doc.Save("form.pdf");Fill fields later with EditableDoc.FillTextField.
Metadata
SetInfo(title?, author?, subject?, keywords?, creator?)Sets the document information dictionary (and, for PDF/A, the matching XMP). Pass only the named arguments you need.
doc.SetInfo(title: "Q3 Report", author: "Acme Inc.",
subject: "Quarterly results", keywords: "finance, q3");Manipulation: load an existing PDF
EditableDoc parses an existing document (classic & xref streams, object streams, all standard filters, RC4/AES decryption) into an editable model. Pages are a flat list; the page tree is rebuilt on output.
EditableDoc.Load(byte[] data, password = null)EditableDoc.LoadFile(path, password = null)using RustPdf;
using (var ed = EditableDoc.LoadFile("in.pdf"))
Console.WriteLine(ed.PageCount);
// encrypted input:
using var sec = EditableDoc.LoadFile("secured.pdf", "user-or-owner-pw");
sec.Save("plain.pdf");Pages: merge, split, reorder, rotate
| Method | Description |
|---|---|
Merge(other) | Append all pages of another EditableDoc (objects renumbered & remapped). |
RotatePage(index, degrees) | Rotate one page (90 / 180 / 270). |
DeletePage(index) | Remove a page. |
ReorderPages(order) | Reorder with a full permutation list of indices. |
ExtractPages(indices) → EditableDoc | New document containing just those pages. |
PageCount | Property: current page count. |
using var a = EditableDoc.LoadFile("a.pdf");
using (var b = EditableDoc.LoadFile("b.pdf"))
a.Merge(b); // a now has a's pages followed by b's
a.RotatePage(0, 90);
a.ReorderPages(Enumerable.Range(0, a.PageCount).Reverse().ToList());
a.Save("merged.pdf");
using var subset = a.ExtractPages(new[] { 0, 2 }); // pages 1 and 3
subset.Save("subset.pdf");Metadata, overlay & form fill
| Method | Description |
|---|---|
SetInfo(key, value) | Set one info entry (e.g. "Title"). |
GetInfo(key) → string | Read an info entry. |
SetXmp(byte[] xml) | Replace the XMP metadata stream. |
OverlayPage(index, byte[] content) | Overlay a content-stream fragment onto a page (stamps/watermarks). |
FillTextField(name, value) → bool | Fill an AcroForm text field; returns whether it was found. |
using var ed = EditableDoc.LoadFile("form.pdf");
ed.SetInfo("Title", "Filled form");
bool found = ed.FillTextField("applicant.name", "Jane Doe");
Console.WriteLine($"filled: {found} | title: {ed.GetInfo("Title")}");
ed.Save("filled.pdf");Optimize & compact
| Method | Description |
|---|---|
Optimize() | Drop unreferenced objects, Flate-compress uncompressed streams, dedupe identical objects. |
Compact(on = true) | Pack objects into object streams + emit a cross-reference stream. |
using var ed = EditableDoc.LoadFile("big.pdf");
ed.Optimize().Compact(true);
ed.Save("small.pdf");Encryption licensed
Apply standard-handler encryption at output. AES-256 (V5/R6) uses OS-CSPRNG keys/IVs.
Encrypt(user = "", owner = "", method = Encryption.Aes256, readOnly = false)using RustPdf;
using var ed = EditableDoc.LoadFile("in.pdf");
ed.Encrypt(user: "", owner: "owner-secret",
method: Encryption.Aes256, readOnly: true);
ed.Save("secured.pdf"); // throws PdfException without an Encryption licenseSee the Encryption enum for RC4 / AES-128 / AES-256.
Output & incremental update
| Method | Description |
|---|---|
ToBytes() → byte[] | Serialize the manipulated document. |
Save(path) | Serialize to a file. |
ToBytesIncremental(byte[] original) → byte[] | Append only changes to the original bytes (signature-safe, non-destructive). |
byte[] original = File.ReadAllBytes("in.pdf");
using var ed = EditableDoc.Load(original);
ed.SetInfo("Subject", "reviewed");
byte[] incremental = ed.ToBytesIncremental(original); // original bytes preserved verbatim
File.WriteAllBytes("reviewed.pdf", incremental);Digital signatures licensed
Sign a PDF with a PKCS#7 detached signature via an incremental update (the original bytes are preserved). Keys and certificates are passed as DER byte[]. pades: true switches to PAdES-B-B.
Pdf.Sign(pdf, keyDer, certDer, reason?, location?, name?, pades = false) → byte[]using RustPdf;
byte[] pdf = File.ReadAllBytes("contract.pdf");
byte[] keyDer = File.ReadAllBytes("signing-key.pkcs8.der"); // PKCS#8 private key (DER)
byte[] certDer = File.ReadAllBytes("signing-cert.der"); // X.509 certificate (DER)
byte[] signed = Pdf.Sign(pdf, keyDer, certDer,
reason: "Approved", location: "New York",
name: "Jane Doe", pades: true);
File.WriteAllBytes("contract.signed.pdf", signed);
// Verify in a shell: pdfsig contract.signed.pdf → "Signature is Valid."Timestamp & DSS (PAdES LTV) licensed
Build long-term-validation signatures offline. Pdf.AddDss appends a Document Security Store (/DSS with certs/CRLs, PAdES-B-LT); Pdf.Timestamp appends an RFC 3161 document timestamp (/DocTimeStamp, PAdES-B-LTA).
Pdf.AddDss(pdf, certs?, crls?) → byte[]Pdf.Timestamp(pdf, tsaKeyDer, tsaCertDer, date?) → byte[]byte[] signed = File.ReadAllBytes("contract.signed.pdf");
// B-LT: embed validation material (caller supplies DER certs/CRLs)
byte[] lt = Pdf.AddDss(signed, new[] { certDer }, new[] { crlDer });
// B-LTA: add a document timestamp signed by a TSA key/cert
byte[] lta = Pdf.Timestamp(lt, tsaKeyDer, tsaCertDer);
File.WriteAllBytes("contract.lta.pdf", lta);Text extraction
Extract a document's text, mapping shown glyph codes back to Unicode through each font's ToUnicode map, with space/line inference.
Pdf.ExtractText(byte[] data) → string freebyte[] data = File.ReadAllBytes("report.pdf");
Console.WriteLine(Pdf.ExtractText(data));Enums
PdfaLevel
| Value | Level |
|---|---|
PdfaLevel.A1b | PDF/A-1b (basic, PDF 1.4) |
PdfaLevel.A2b | PDF/A-2b (basic): default of Pdfa() |
PdfaLevel.A2a | PDF/A-2a (accessible: pair with Tagged()) |
PdfaLevel.A3b | PDF/A-3b (basic, allows attachments) |
PdfaLevel.A3a | PDF/A-3a (accessible + attachments) |
Align
| Value | Meaning |
|---|---|
Align.Left | Left-aligned (default) |
Align.Right | Right-aligned |
Align.Center | Centered |
Align.Justify | Justified (space distributed between words) |
AFRelationship
| Value | Meaning |
|---|---|
AFRelationship.Source | Source data for the document (e.g. the invoice XML) |
AFRelationship.Data | Data used to derive the visual content |
AFRelationship.Alternative | Alternative representation |
AFRelationship.Supplement | Supplementary material |
AFRelationship.Unspecified | Unspecified relationship |
Encryption
| Value | Cipher |
|---|---|
Encryption.Rc4 | RC4 (legacy) |
Encryption.Aes128 | AES-128 |
Encryption.Aes256 | AES-256 (V5/R6): recommended |
Error handling
Every failing native call throws PdfException carrying the Status (PdfStatus) code and the library's last-error message. License failures (missing/expired/forged token, or a feature the token doesn't grant) surface here too.
using RustPdf;
try
{
using var doc = new Document();
doc.Pdfa().SetInfo(title: "x");
doc.AddPage();
doc.Save("out.pdf");
}
catch (PdfException e)
{
Console.Error.WriteLine($"failed: {e.Status} {e.Message}");
// e.g. PdfStatus=7: feature 'pdfa' requires a valid license
}Utilities
| Member | Description |
|---|---|
Pdf.Version() → string | Native library version string. |
Pdf.ActivateLicense(token) | Activate a license token (throws on invalid/expired). |