C# / .NET
Last updated: 2026-06-29
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 + A-4/4e/4f), tagged/accessible output, attachments, AcroForm fields, manipulation, text extraction, encryption and digital signatures. The native library ships inside the package for every runtime, with 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. Using a Document or EditableDoc after it has been disposed throws ObjectDisposedException (never a crash); calling Dispose() more than once is safe.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 by default; stamping calls accept an explicitVerticalAnchor(see stamping and the interactive positioning guide). - 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, 3 → 2.0). |
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, provided the embedded font covers those characters). 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");\0) character. It truncates the string at the FFI boundary, silently dropping everything after the NUL. This applies to shown text, metadata and field names.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, and A-4 (ISO 19005-4) is based on PDF 2.0.
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");Figure() only produces an accessible, alt-texted figure inside a tagged/accessible document; on a plain document the alt text has no effect.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");AttachFile before serializing.AttachFile itself. The licensed badge above reflects the PDF/A-3 workflow shown here. Calling AttachFile on a plain (non-PDF/A) document does not require a licence: it succeeds and produces a valid PDF with an /EmbeddedFile. The licence is enforced only when you also request a PDF/A level, which is what makes the attachment archival.ZUGFeRD / Factur-X e-invoices licensed
Turn the document into a ZUGFeRD / Factur-X electronic invoice: the embedded XML (the Cross-Industry Invoice) is attached as factur-x.xml, the file is marked PDF/A-3, and the Factur-X identification is written into the XMP metadata. The visible PDF is the human-readable invoice; the embedded XML is its machine-readable twin. Validates as PDF/A-3 + Factur-X under veraPDF.
Facturx(byte[] xml, FacturxProfile profile = FacturxProfile.En16931)using RustPdf;
byte[] xml = File.ReadAllBytes("factur-x.xml"); // your Cross-Industry Invoice XML
using var doc = new Document();
doc.SetInfo(title: "Invoice INV-2026-001");
int f = doc.AddFontFile("Roboto-Regular.ttf");
doc.AddPage();
doc.ShowText(f, 18, 72, 760, "Invoice INV-2026-001");
doc.Facturx(xml, FacturxProfile.En16931);
doc.Save("einvoice.pdf"); // PDF/A-3 + Factur-X; needs a PDF/A licenseSee the FacturxProfile enum for the conformance levels (Minimum to Extended).
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 and flatten fields later with EditableDoc.
page does not exist, or whose rectangle is degenerate (x1 < x0 or zero area), is rejected when the document is serialized: ToBytes()/Save() throw PdfException ("targets page index N…" / "degenerate rectangle…") instead of producing an invisible, never-appearing widget. Pass a 0-based page index that exists and a rectangle with x1 > x0 and y1 > y0.Hyperlinks free
Add clickable link rectangles to the current page: a web link opens a URL; an internal link jumps to another page (optionally scrolling so a given top y-coordinate sits at the top of the view).
LinkUri(rect, uri) LinkToPage(rect, pageIndex, top = null)using var doc = new Document();
int f = doc.AddFontFile("Roboto-Regular.ttf");
doc.AddPage();
doc.ShowText(f, 14, 72, 760, "Visit rustpdf.dev (see page 2)");
doc.LinkUri((72, 756, 320, 776), "https://rustpdf.dev/"); // web link
doc.LinkToPage((330, 756, 430, 776), 1, top: 800); // jump to page 2
doc.AddPage();
doc.Save("links.pdf");Rectangles are (x0, y0, x1, y1) tuples in points; pageIndex is 0-based.
Bookmarks / outline free
Build a navigable document outline. A Bookmark has a title, a target page and an optional top; nest children with .Child(...). A document with bookmarks opens with the outline pane shown.
new Bookmark(title, page, top = null) .Child(bookmark) AddBookmark(bookmark)using RustPdf;
using var doc = new Document();
int f = doc.AddFontFile("Roboto-Regular.ttf");
for (int i = 0; i < 3; i++)
doc.AddPage();
doc.AddBookmark(
new Bookmark("Chapter 1", 0, top: 820)
.Child(new Bookmark("Section 1.1", 1))
.Child(new Bookmark("Section 1.2", 2)));
doc.AddBookmark(new Bookmark("Chapter 2", 2));
doc.Save("outline.pdf");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");Load does not throw in that case. Serializing a zero-page document throws PdfException ("document has no pages") rather than writing an invalid file, but you should check PageCount > 0 after loading untrusted input before relying on it.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");ReorderPages requires a true permutation of every page index (each used exactly once); an invalid argument (wrong length, a repeated index, or out-of-range) is rejected and leaves the page order unchanged; the call is a silent no-op that raises no error, so an invalid reorder cannot be detected from a return value.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");Form fill & flatten free
Fill the fields of an existing AcroForm and (optionally) flatten them: filling generates a fresh appearance stream (no NeedAppearances), and flattening bakes every widget's appearance into the page content and removes the interactive form entirely.
| Method | Description |
|---|---|
FieldNames() → IReadOnlyList<string> | Fully-qualified names of every terminal field. |
FillTextField(name, value) → bool | Set a text (or text-style choice) field; returns whether it matched. |
SetCheckbox(name, checkedFlag = true) → bool | Check/uncheck a checkbox. |
SetRadio(name, exportValue) → bool | Select a radio button by its export value. |
SetChoice(name, value) → bool | Set a dropdown / list-box value. |
FlattenForms() | Bake all fields into static content and drop the /AcroForm. |
using RustPdf;
using var ed = EditableDoc.LoadFile("form.pdf");
Console.WriteLine(string.Join(", ", ed.FieldNames())); // applicant.name, agree, plan, ...
ed.FillTextField("applicant.name", "Jane Doe");
ed.SetCheckbox("agree", true);
ed.SetRadio("billing", "annual");
ed.SetChoice("plan", "Pro");
ed.FlattenForms(); // optional: make it non-editable
ed.Save("filled.pdf");Watermarks free
Stamp a diagonal text watermark or a centered image watermark across every page, drawn semi-transparently over the existing content. Text uses the standard Helvetica font, so keep it to WinAnsi (Latin-1) for stamps like "CONFIDENTIAL".
WatermarkText(text, size = 64.0, color = null, opacity = 0.30, rotationDeg = 45.0, opaqueBackground = false)WatermarkImageFile(path, width, height, opacity = 0.30, rotationDeg = 0.0)Set opaqueBackground: true to draw an opaque box behind the text, a white-out / redaction-style banner instead of a transparent overlay. WatermarkImageFile takes a rotationDeg to rotate the stamped image.
using var ed = EditableDoc.LoadFile("report.pdf");
ed.WatermarkText("CONFIDENTIAL", color: (0.7, 0.1, 0.1),
opacity: 0.25, rotationDeg: 45, opaqueBackground: false);
ed.WatermarkImageFile("stamp.png", 200, 120, opacity: 0.30, rotationDeg: 30);
ed.Save("stamped.pdf");Positioned drawing free
Stamp a single page in place: fill a rectangle and/or drop a line of text at exact coordinates. Coordinates are in the page's visible space (origin lower-left, y up), so content lands where a viewer sees it regardless of the page's /Rotate; rotationDeg rotates the text counter-clockwise about its anchor. Text uses the standard Helvetica font (keep it WinAnsi / Latin-1). Both return whether the page existed.
bool FillRect(int pageIndex, double x, double y, double width, double height, (double R, double G, double B)? color = null, double opacity = 1.0)bool PlaceText(int pageIndex, double x, double y, string text, double size = 12.0, (double R, double G, double B)? color = null, double rotationDeg = 0.0, Align align = Align.Left, int fontId = -1, VerticalAnchor anchor = VerticalAnchor.Baseline)bool MaskedText(int pageIndex, double x, double y, double width, double height, string text, double size = 12.0, (double R, double G, double B)? textColor = null, (double R, double G, double B)? bgColor = null, Align align = Align.Left, int fontId = -1, VerticalAlign valign = VerticalAlign.Middle, double? padding = null)bool DrawImage(int pageIndex, byte[] image, double x, double y, double width, double height, double rotationDeg = 0.0, ImageAnchor anchor = ImageAnchor.Corner)A typical use is masking a spot with an opaque white box, then writing a value over it. MaskedText does exactly that in one call: it fills the box [x, y, x+width, y+height] in bgColor (default opaque white), then writes the text in textColor horizontally aligned per align and vertically centered in the box, so you never hand-compute the baseline. PlaceText's align shifts the start point along the baseline by the text width for Center or Right. DrawImage takes raw JPEG/PNG image bytes and places the image with its lower-left corner at (x, y) scaled to width × height points.
byte[] data = File.ReadAllBytes("invoice.pdf");
PageGeometry g = Pdf.MeasurePage(data, 0); // read the page geometry first
using var ed = EditableDoc.Load(data);
ed.FillRect(0, 400, g.Height - 60, 120, 18); // mask: default opaque white
ed.PlaceText(0, 404, g.Height - 56, "PAID", size: 12, color: (0.0, 0.5, 0.0));
// or mask + stamp a right-aligned value in one call:
ed.MaskedText(0, 400, g.Height - 60, 120, 18, "R$ 1.250,00", size: 12, align: Align.Right);
ed.DrawImage(0, File.ReadAllBytes("logo.png"), 404, g.Height - 120, 120, 48);
ed.Save("stamped.pdf");Stamping: fonts, anchors & paragraphs free
Everything below extends the positioned-drawing primitives with production-grade control: real embedded fonts, explicit vertical anchors, word-wrapping paragraphs and a per-document coordinate space. Try every mode live in the interactive positioning guide.
Embedded stamping fonts
int AddFontFile(string path) int AddFont(byte[] data)Register a TrueType/OpenType font on the EditableDoc and pass its fontId to PlaceText/MaskedText/PlaceParagraph. The font is embedded as a subset; text may be arbitrary Unicode. fontId: -1 keeps the built-in Helvetica (WinAnsi).
using var ed = EditableDoc.LoadFile("contract.pdf");
int times = ed.AddFontFile("/Library/Fonts/Times New Roman.ttf");
ed.PlaceText(0, 72, 96, "Assinado por Maria — ação registrada", size: 11, fontId: times);Vertical anchors: what y means
VerticalAnchor.Baseline (default) anchors the text baseline. Top/Bottom anchor the geometric ascender/descender lines. LineTop/LineBottom anchor the layout line box (OS/2 win metrics + half-leading), the drop-in pair when matching output from legacy layout engines's fixed-position layout. Rotation always pivots at the anchor (x, y); anchor offsets rotate with the text.
// y is the BOTTOM of the line box, like legacy fixed-position layout:
ed.PlaceText(0, 72, 140, "Total: R$ 1.250,00", size: 12, fontId: times,
anchor: VerticalAnchor.LineBottom);MaskedText: box alignment & padding
valign places the line inside the box: Top (top line-alignment in rectangle-based text APIs), Middle (default, cap-height centered) or Bottom. padding is the horizontal inset for Left/Right alignment; null keeps the default min(0.15 × size, width / 4), 0 starts flush at the box edge.
ed.MaskedText(0, 400, 700, 120, 18, "R$ 1.250,00", size: 12,
valign: VerticalAlign.Top, padding: 0);Paragraphs: wrapping, bottom-pin, ceiling
bool PlaceParagraph(int pageIndex, double x, double y, double width, string text, double size = 12.0, (double R, double G, double B)? color = null, Align align = Align.Left, int fontId = -1, double? maxHeight = null, double lineHeight = 1.0, VerticalAnchor anchor = VerticalAnchor.Top, double rotationDeg = 0.0)(int Lines, double Height) PlaceParagraphMeasured(...) int PlaceParagraphCounted(...)Breaks text greedily by word inside width (\n forces a line break). anchor: Top (default) hangs the block from y. Bottom/LineBottom bottom-pin it: the block's bottom rests on y and grows upward by its real content height; maxHeight is a ceiling that cuts overflowing lines from the top (the last lines stay pinned) and never inflates the position. PlaceParagraphMeasured also returns the consumed height, so blocks stack without re-measuring. The Line* anchors use the layout line box for the first line and the legacy multiplied-leading advance between lines.
var (lines, height) = ed.PlaceParagraphMeasured(0, 72, 96, 220,
"Digitally signed by Maria Silva\nReason: Approval\n2026-07-02 14:31 UTC",
size: 10, fontId: times, maxHeight: 64, anchor: VerticalAnchor.LineBottom);Coordinate space on rotated scans: StampSpace
StampSpace EditableDoc.StampSpace { get; set; }Visible (default): coordinates in the page as displayed; the library compensates /Rotate, so a rotationDeg: 0 stamp reads upright on screen. Media: the raw PDF user space (the semantics of legacy engines that draw in raw coordinates), nothing composed on top of your math. Applies to FillRect, PlaceText, MaskedText, PlaceParagraph and DrawImage; watermarks and redaction are unaffected.
ed.StampSpace = StampSpace.Media; // porting coordinates computed for legacy PDF libraries
ed.PlaceText(0, x, y, "APPROVED", rotationDeg: 90, anchor: VerticalAnchor.LineBottom);Rotated images: ImageAnchor
Corner (default) keeps (x, y) as the image's own lower-left corner; the image sweeps around it when rotated. BoundingBox lands the rotated image's bounding box at (x, y) (bounding-box layout: a 90° image occupies [x, x+height] × [y, y+width]).
ed.DrawImage(0, File.ReadAllBytes("stamp.png"), 404, 96, 120, 48,
rotationDeg: 90, anchor: ImageAnchor.BoundingBox);Redaction licensed
True redaction: every shown glyph whose box intersects a rectangle is removed from the content stream (surviving glyphs keep their exact positions), images/XObjects overlapping a rect are dropped with their data, and intersecting annotations are deleted; only then are opaque black boxes painted. The call throws when a page cannot be safely rewritten (inline BI images, undecodable streams) instead of painting over still-present data.
Redact(pageIndex, rects) → boolusing var ed = EditableDoc.LoadFile("statement.pdf");
// rects = list of (x0, y0, x1, y1) tuples on that page
ed.Redact(0, new[]
{
(60.0, 590.0, 400.0, 620.0),
(60.0, 540.0, 400.0, 570.0),
});
ed.Save("redacted.pdf"); // throws PdfException without a license granting redactionPdf.ExtractText on the output no longer returns it.Convert to PDF/A licensed
Convert an existing PDF to archival PDF/A (a basic profile: A-1b, A-2b or A-3b). An sRGB output intent, PDF/A XMP metadata (synced with /Info) and a document /ID are added. Fails if any font is not embedded (PDF/A requires every font embedded) or a level-A profile is requested.
ConvertToPdfa(PdfaLevel level = PdfaLevel.A2b)using RustPdf;
using var ed = EditableDoc.LoadFile("in.pdf");
ed.ConvertToPdfa(PdfaLevel.A2b); // throws PdfException if fonts aren't embedded
ed.Save("archival.pdf"); // veraPDF: PDF/A-2b compliantOptimize & 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.
readOnly permission flags, by contrast, are advisory: they are enforced only by the viewer, and a file with an empty user password opens with no prompt, so any tool can strip the restrictions. Treat readOnly as a hint to well-behaved viewers, not as an access control.Normalize & downgrade free
Turn a PDF/A or PDF 2.0 file back into a plain PDF: SetVersion sets the header version, StripPdfa removes the PDF/A conformance markers (/OutputIntents, the XMP pdfaid entry and any /Version override), and Normalize does both at once.
| Method | Description |
|---|---|
SetVersion(version) | Set the output version (0 → 1.4, 1 → 1.5, 2 → 1.7, 3 → 2.0). Clears any catalog /Version override. |
StripPdfa() | Remove PDF/A conformance so the file is a plain PDF. |
Normalize(version) | Strip PDF/A and set the version in one call (codes as in SetVersion). |
using var ed = EditableDoc.LoadFile("archival.pdf");
ed.Normalize(2); // strip PDF/A + set version 1.7
// or step by step:
// ed.StripPdfa().SetVersion(2);
ed.Save("plain.pdf");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."Deferred / HSM signing licensed
Sign without ever handing the library a private key. When the key lives in an HSM, a cloud KMS, a smartcard or a PKI token (any PKI: eIDAS, AATL, a national CA), you supply the raw RSA signature and the library builds and embeds the CMS / PKCS#7 container. There are two flavours: a synchronous remote signer callback, and a two-phase begin / complete flow for asynchronous or out-of-band signing.
Model A: remote signer callback
The library assembles the signed attributes and calls back for the raw RSA PKCS#1 v1.5 signature over their SHA-256 digest. The certificate is passed as DER; the key stays inside your HSM. Provide a RemoteSign delegate, or an IRemoteSigner for the interface-shaped overload.
Pdf.SignWith(pdf, certDer, RemoteSign signHash, chain?, options?) → byte[]Pdf.SignWith(pdf, certDer, IRemoteSigner signer, chain?, options?) → byte[]using RustPdf;
byte[] pdf = File.ReadAllBytes("contract.pdf");
byte[] certDer = File.ReadAllBytes("signer-cert.der"); // X.509 (DER); key stays remote
byte[] signed = Pdf.SignWith(pdf, certDer,
signHash: dataToSign => hsm.SignRsaPkcs1Sha256(dataToSign), // your HSM / KMS / token
chain: new[] { intermediateDer },
options: new SigningOptions { Reason = "Approved", Pades = true });
File.WriteAllBytes("contract.signed.pdf", signed);signHash delegate receives the bytes to sign and must return the raw RSA PKCS#1 v1.5 signature over their SHA-256 digest. The private key never reaches this library, so the same code path works for cloud KMS, smartcards and PKI tokens.Model B: two-phase begin / complete
For an asynchronous HSM or a detached workflow, phase 1 prepares the PDF with a placeholder and returns the exact bytes the signature covers; you sign the digest out of band and build the CMS container; phase 2 embeds it.
Pdf.BeginSigning(pdf, options?) → SigningSessionPdf.CompleteSignature(document, container) → byte[]A SigningSession exposes Document (the prepared PDF), Bytes (the bytes covered by the signature), Hash (their SHA-256, the value an HSM signs) and Complete(container), which embeds a finished DER CMS / PKCS#7 container and returns the final signed PDF.
SigningSession session = Pdf.BeginSigning(pdf,
new SigningOptions { Name = "Jane Doe", ContainerSize = 16384 });
byte[] cmsDer = await BuildCmsWithRemoteSignatureAsync(session.Hash); // sign the digest remotely
byte[] signed = session.Complete(cmsDer);
// equivalently: Pdf.CompleteSignature(session.Document, cmsDer)
File.WriteAllBytes("contract.signed.pdf", signed);Inspect existing signature fields
Before signing, list the signature fields already present (a classic pre-sign signature-field inventory). An empty list means the document is unsigned.
Pdf.ListSignatures(pdf) → IReadOnlyList<SignatureField>foreach (SignatureField f in Pdf.ListSignatures(pdf))
Console.WriteLine($"{f.Name} signed: {f.Signed}");SigningOptions
Both models accept an optional SigningOptions:
| Property | Meaning |
|---|---|
Reason, Location, Name | Signature metadata (all string?). |
Pades | true produces a PAdES-B-B signature (ETSI.CAdES.detached). |
Certify | Certify the document (DocMDP); use only on the first signature. See Certify below. |
ContainerSize | Reserved /Contents bytes; 0 uses the default (8192). Raise it for large cloud-HSM CMS containers. |
Policy | A SignaturePolicy identifier (PAdES-EPES); null for none. |
Visible | true draws a visible signature appearance using the fields below. |
VisiblePage, VisibleRect | 0-based page index and the appearance rectangle [x0, y0, x1, y1] in points. |
VisibleText | Appearance text lines (separated by \n); null for none. |
VisibleImage | PNG/JPEG bytes drawn aspect-fit behind any text (e.g. a handwritten-signature scan); null for none. |
A visible signature can carry an embedded image: set Visible = true, give it a rectangle, and pass the PNG/JPEG bytes in VisibleImage (drawn aspect-fit behind VisibleText).
var opts = new SigningOptions
{
Name = "Jane Doe",
Visible = true,
VisiblePage = 0,
VisibleRect = new[] { 72.0, 96.0, 272.0, 156.0 },
VisibleText = "Signed by Jane Doe\n2026-06-30",
VisibleImage = File.ReadAllBytes("signature.png"),
};Certify
| Value | DocMDP level |
|---|---|
Certify.None | Not a certifying signature (default). |
Certify.Locked | /P 1: no changes permitted after signing. |
Certify.Forms | /P 2: form-filling and signing permitted. |
Certify.FormsAndAnnotations | /P 3: form-filling, signing and annotations permitted. |
SignaturePolicy
A PAdES-EPES signature-policy identifier. Oid is the policy OID (dotted-decimal); Hash is the policy document hash (byte[]); HashAlgorithmOid is the hash algorithm OID (null means SHA-256); Uri is an optional SPURI qualifier pointing to where the policy can be retrieved.
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);Network timestamp (AD-RT)
To stamp from a real network RFC 3161 TSA, the transport is left to you: Pdf.BeginTimestamp prepares the PDF and returns the bytes to timestamp, Pdf.TimestampRequest builds the request (DER), you POST it to the TSA, Pdf.TimestampTokenFromResponse extracts the token, then Pdf.CompleteSignature embeds it. Free public TSAs work (FreeTSA https://freetsa.org/tsr, DigiCert http://timestamp.digicert.com).
Pdf.BeginTimestamp(pdf) → (byte[] Document, byte[] Bytes)Pdf.TimestampRequest(imprint, nonce?, certReq = true) → byte[]Pdf.TimestampTokenFromResponse(response) → byte[]using System.Security.Cryptography;
byte[] pdf = File.ReadAllBytes("contract.signed.pdf");
var (document, toStamp) = Pdf.BeginTimestamp(pdf);
byte[] imprint = SHA256.HashData(toStamp);
byte[] request = Pdf.TimestampRequest(imprint);
// POST the DER request to the TSA (you own the HTTP call):
using var http = new HttpClient();
using var body = new ByteArrayContent(request);
body.Headers.ContentType = new("application/timestamp-query");
byte[] response = await (await http.PostAsync("https://freetsa.org/tsr", body))
.Content.ReadAsByteArrayAsync();
byte[] token = Pdf.TimestampTokenFromResponse(response);
byte[] lta = Pdf.CompleteSignature(document, token);
File.WriteAllBytes("contract.lta.pdf", lta);Validate signatures licensed
Validate every signature in a PDF: each report recomputes the /ByteRange digest, parses the CMS, and checks that the cryptographic signature is valid, that the messageDigest matches the covered bytes, and whether the signature covers the whole document.
Pdf.VerifySignatures(byte[] pdf) → IReadOnlyList<SignatureInfo>Each SignatureInfo has FieldName, SubFilter, Signer, CoversWholeDocument, DigestValid, SignatureValid, IsValid and ByteRange. When the signer certificate parses, it also carries the certificate detail: Issuer, SerialNumber, ValidFrom, ValidTo, Algorithm (e.g. SHA256withRSA), SigningTime, CertCount and HasTimestamp. The string fields may be null when absent. An empty list means the document is unsigned. Signature validation is a licensed feature: this call requires an active license (signatures) even when the document is unsigned, so it is not available on the free tier.
using RustPdf;
byte[] data = File.ReadAllBytes("contract.signed.pdf");
foreach (SignatureInfo sig in Pdf.VerifySignatures(data))
{
Console.WriteLine($"{sig.Signer} valid: {sig.IsValid} " +
$"covers whole doc: {sig.CoversWholeDocument}");
Console.WriteLine($" issuer: {sig.Issuer}");
Console.WriteLine($" serial: {sig.SerialNumber}");
Console.WriteLine($" valid: {sig.ValidFrom} … {sig.ValidTo}");
Console.WriteLine($" algorithm: {sig.Algorithm}");
Console.WriteLine($" signed at: {sig.SigningTime}");
Console.WriteLine($" certs: {sig.CertCount}, timestamp: {sig.HasTimestamp}");
}Positional text search free
Find every occurrence of a query string and get back each match's bounding box in PDF points (origin lower-left). Useful for highlighting, redaction targeting, or locating where to stamp. Search is case-insensitive by default; pass caseSensitive: true to match case. See the searching a PDF concept page for background.
Pdf.FindText(byte[] pdf, string query, bool caseSensitive = false) → IReadOnlyList<TextHit>Each TextHit has Page (0-based), Text, and the box X, Y, Width, Height. An empty list means no match.
using RustPdf;
byte[] data = File.ReadAllBytes("report.pdf");
foreach (TextHit hit in Pdf.FindText(data, "Total"))
Console.WriteLine(
$"page {hit.Page}: \"{hit.Text}\" at " +
$"({hit.X:0.#}, {hit.Y:0.#}) {hit.Width:0.#}×{hit.Height:0.#} pt");Text & image extraction
Extract a document's text, mapping shown glyph codes back to Unicode through each font's ToUnicode map, with space/line inference. Raster images can be pulled out too: JPEGs are written verbatim as .jpg, everything else as .png. The output directory is created automatically if it does not already exist.
Pdf.ExtractText(byte[] data) → string freePdf.ExtractImagesToDir(byte[] data, string outDir) → int freebyte[] data = File.ReadAllBytes("report.pdf");
Console.WriteLine(Pdf.ExtractText(data));
int n = Pdf.ExtractImagesToDir(data, "out_images/"); // returns how many were written
Console.WriteLine($"wrote {n} image(s)");Page geometry free
Read each page's size, rotation and boxes without modifying the file. Width/Height ignore /Rotate; RotatedWidth/RotatedHeight are the dimensions a viewer shows. Sizes are in PDF points (72 per inch).
IReadOnlyList<PageGeometry> Pdf.MeasurePages(byte[] pdf)PageGeometry Pdf.MeasurePage(byte[] pdf, int pageIndex)Each PageGeometry has Page (0-based), Width, Height, Rotation (0/90/180/270), RotatedWidth, RotatedHeight, and the MediaBox/CropBox as a PdfRect (X0, Y0, X1, Y1 with Width/Height helpers).
byte[] data = File.ReadAllBytes("report.pdf");
PageGeometry g = Pdf.MeasurePage(data, 0);
Console.WriteLine($"page {g.Page}: {g.Width:0.#}×{g.Height:0.#} pt, rotate {g.Rotation}");
Console.WriteLine($"visible: {g.RotatedWidth:0.#}×{g.RotatedHeight:0.#} pt");Document inspection free
Get a non-mutating overview of a PDF, its version, PDF/A level (if any) and encryption posture, without decrypting or rewriting it. Handy for triage before deciding how to process a file.
PdfOverview Pdf.Inspect(byte[] pdf)PdfOverview carries Version (e.g. "1.7"), PdfaLevel (e.g. "2b", or null if not PDF/A), Encrypted, Encryption ("None", "RC4", "AES-128" or "AES-256"), RequiresPassword, and PageCount.
byte[] data = File.ReadAllBytes("report.pdf");
PdfOverview o = Pdf.Inspect(data);
Console.WriteLine($"PDF {o.Version}, {o.PageCount} page(s)");
Console.WriteLine($"PDF/A: {o.PdfaLevel ?? "no"}, encryption: {o.Encryption}");
if (o.RequiresPassword) Console.WriteLine("needs a password to open");Render a page to an image licensed
Rasterize a page to a PNG image. A native Rust renderer (built on tiny-skia, with no headless browser) interprets the page content stream, painting real glyph outlines, vector graphics, images, color and transparency. Page rendering is a Pro feature; page_count is free.
byte[] Pdf.RenderPageToPng(byte[] pdf, int pageIndex = 0, double dpi = 150.0) licensedint Pdf.PageCount(byte[] pdf) freebyte[] data = File.ReadAllBytes("report.pdf");
Console.WriteLine($"{Pdf.PageCount(data)} page(s)");
byte[] png = Pdf.RenderPageToPng(data, pageIndex: 0, dpi: 150.0);
File.WriteAllBytes("page1.png", png);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) |
PdfaLevel.A4 | PDF/A-4 (ISO 19005-4, based on PDF 2.0) |
PdfaLevel.A4e | PDF/A-4e (engineering) |
PdfaLevel.A4f | PDF/A-4f (requires at least one embedded file) |
Align
| Value | Meaning |
|---|---|
Align.Left | Left-aligned (default) |
Align.Right | Right-aligned |
Align.Center | Centered |
Align.Justify | Justified (space distributed between words) |
VerticalAnchor
| Value | Meaning |
|---|---|
VerticalAnchor.Baseline | y is the text baseline (default) |
VerticalAnchor.Top | y is the ascender line (geometric top) |
VerticalAnchor.Bottom | y is the descender line (geometric bottom) |
VerticalAnchor.LineTop | y is the top of the layout line box |
VerticalAnchor.LineBottom | y is the bottom of the layout line box |
VerticalAlign
| Value | Meaning |
|---|---|
VerticalAlign.Top | Line hangs from the box top (top line-alignment in rectangle-based text APIs) |
VerticalAlign.Middle | Cap-height centered (default) |
VerticalAlign.Bottom | Descender rests on the box bottom |
StampSpace
| Value | Meaning |
|---|---|
StampSpace.Visible | Coordinates in the displayed page, /Rotate compensated (default) |
StampSpace.Media | Raw PDF user space, legacy layout semantics (no composition) |
ImageAnchor
| Value | Meaning |
|---|---|
ImageAnchor.Corner | Rotate about the image's own lower-left corner (default) |
ImageAnchor.BoundingBox | Rotated bounding box lands at (x, y) (bounding-box layout) |
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 |
FacturxProfile
| Value | Conformance level |
|---|---|
FacturxProfile.Minimum | Minimal header data only |
FacturxProfile.BasicWL | Basic, without line items |
FacturxProfile.Basic | Basic, with line items |
FacturxProfile.En16931 | EN 16931 (Comfort): the interoperable core, default |
FacturxProfile.Extended | EN 16931 plus extensions |
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. Token activation failures carry the dedicated license status code (12); a gated build, sign or encrypt call instead reports that operation’s own status (for example Serialize = 4 or Sign = 10) with the same “requires a valid license” message.
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=4 (Serialize): 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). |