Exis.PdfEditor
3.7.8
dotnet add package Exis.PdfEditor --version 3.7.8
NuGet\Install-Package Exis.PdfEditor -Version 3.7.8
<PackageReference Include="Exis.PdfEditor" Version="3.7.8" />
<PackageVersion Include="Exis.PdfEditor" Version="3.7.8" />
<PackageReference Include="Exis.PdfEditor" />
paket add Exis.PdfEditor --version 3.7.8
#r "nuget: Exis.PdfEditor, 3.7.8"
#:package Exis.PdfEditor@3.7.8
#addin nuget:?package=Exis.PdfEditor&version=3.7.8
#tool nuget:?package=Exis.PdfEditor&version=3.7.8
Exis.PdfEditor
Comprehensive PDF toolkit for .NET — find/replace, merge, split, build, form filling, redaction, optimization, digital signatures, and PDF/A compliance. Operates directly on PDF content streams with zero external dependencies.
Platform Compatibility
| Build | Use with |
|---|---|
| netstandard2.0 | .NET Framework 4.6.1+, .NET Core 2.0+, .NET 5, .NET 6, .NET 7 |
| net8.0 | .NET 8, .NET 9, .NET 10+ (optimized, adds digital signature support) |
NuGet automatically selects the correct build for your project. .NET 9 and .NET 10 projects use the net8.0 build with full feature support. No additional configuration required.
dotnet add package Exis.PdfEditor
Samples: github.com/exisllc/Exis.PdfEditor-Samples
Features
- Find & Replace — regex, case-insensitive, whole-word matching with width-aware text fitting, replacement text styling (color, highlight, bold, underline, strikethrough)
- Merge — combine multiple PDFs with optional page range selection
- Split — extract individual pages or page ranges into separate PDFs
- Build — create PDFs from scratch with absolute positioning or auto-layout (tables, pagination, headers/footers)
- Form Filling — read and fill AcroForm fields (text, checkbox, radio, dropdown/listbox), intelligent display labels from nearby page text, flatten forms to static PDF
- Redaction — remove sensitive text or areas with black rectangles (regex supported)
- Image Editor — find all images with displayable bytes (JPEG/BMP), replace all or specific images with JPEG/PNG, configurable scaling (match original, keep replacement size, scale to fit)
- Watermark — add text watermarks with configurable position, font size, color, opacity, and page selection
- Bates Stamping — sequential page numbering for legal production (prefix/suffix, configurable digit width, any corner/edge, optional confidentiality label, continuous numbering across batches, XMP audit metadata)
- Page Editing — rotate, crop, reorder, insert blank pages, delete pages
- Stamping — overlay or underlay a PDF on top of/behind another PDF's pages, with opacity control
- Encryption — decrypt password-protected PDFs (AES-256, AES-128, RC4-128), encrypt with password and permission control
- Optimization — compress streams, deduplicate objects, strip metadata, downsample images
- Digital Signatures — sign with X.509 certificates (invisible or visible), verify single or all signatures, certificate metadata (net8.0+)
- PDF/A Compliance — validate and convert to PDF/A-1b, 2b, 2u, 3b, 3u
- Extract — pull text content with optional position and font metadata
- Inspect — read metadata, fonts, page dimensions (no license required)
- Metadata — read, replace, or wipe both modern XMP (Catalog
/Metadata) and legacy/Infodictionary; extraction works on encrypted PDFs and requires no license - Async — all I/O APIs have async overloads with CancellationToken support
- Zero dependencies — no iTextSharp, no PDFsharp, no Aspose
- Lossless — preserves form fields, checkboxes, digital signatures, layout
Quick Start
using Exis.PdfEditor;
using Exis.PdfEditor.Licensing;
// Start a 14-day trial — no key required - no limits
ExisLicense.Initialize();
// When you're ready, pass your activation key after the end of evaluation period:
// ExisLicense.Initialize("XXXX-XXXX-XXXX-XXXX");
Find & Replace
var result = PdfFindReplace.Execute("input.pdf", "output.pdf", "old text", "new text");
Console.WriteLine($"Replaced {result.TotalReplacements} occurrences on {result.PagesModified} pages");
// Style replacement text with color, highlight, and decorations
var styled = PdfFindReplace.Execute("input.pdf", "output.pdf", "old text", "new text",
new PdfFindReplaceOptions
{
ReplacementTextColor = PdfColor.Red, // Red replacement text
ReplacementHighlightColor = PdfColor.Yellow, // Yellow background highlight
ReplacementBold = true, // Faux bold (fill + stroke)
ReplacementUnderline = true, // Underline below text
ReplacementStrikethrough = true // Strikethrough line
});
// Stream-based (for in-memory processing)
var input = new MemoryStream(File.ReadAllBytes("input.pdf"));
var output = new MemoryStream();
PdfFindReplace.Execute(input, output, "old text", "new text",
new PdfFindReplaceOptions
{
CaseSensitive = false,
TextFitting = TextFittingMode.Adaptive,
ReplacementTextColor = PdfColor.Red,
ReplacementBold = true,
ReplacementUnderline = true
});
File.WriteAllBytes("output.pdf", output.ToArray());
// Multiple replacements processed sequentially
// NOTE: Styling options must be set on EACH call's options — they don't persist.
var pairs = new[] { ("[Company]", "Acme Inc"), ("[Date]", "3/26/2026"), ("[City]", "Anytown") };
byte[] current = File.ReadAllBytes("template.pdf");
foreach (var (search, replace) in pairs)
{
var inp = new MemoryStream(current);
var outp = new MemoryStream();
PdfFindReplace.Execute(inp, outp, search, replace,
new PdfFindReplaceOptions
{
CaseSensitive = false,
TextFitting = TextFittingMode.Adaptive,
ReplacementTextColor = PdfColor.Red
});
current = outp.ToArray();
}
File.WriteAllBytes("filled.pdf", current);
Merge PDFs
byte[] merged = PdfMerger.Merge(new[] { "file1.pdf", "file2.pdf", "file3.pdf" });
File.WriteAllBytes("merged.pdf", merged);
// Or merge to file directly
PdfMerger.MergeToFile(new[] { "file1.pdf", "file2.pdf" }, "merged.pdf");
// Merge with page range selection
byte[] selected = PdfMerger.Merge(new[]
{
new PdfMergeInput(File.ReadAllBytes("doc1.pdf"), new[] { 1, 3, 5 }),
new PdfMergeInput(File.ReadAllBytes("doc2.pdf")) // all pages
});
Split PDF
// Split into individual pages
List<byte[]> pages = PdfSplitter.Split("input.pdf");
// Extract specific pages (1-based)
byte[] subset = PdfSplitter.ExtractPages("input.pdf", new[] { 1, 3, 5 });
// Split to individual files
PdfSplitter.SplitToFiles("input.pdf", "page_{0}.pdf");
Build a PDF from Scratch
byte[] pdf = PdfBuilder.Create()
.WithMetadata(m => m.Title("Report").Author("Exis"))
.AddPage(page => page
.Size(PdfPageSize.A4)
.AddText("Hello, World!", x: 72, y: 750, fontSize: 24,
options: o => o.Font("Helvetica").Bold().Color(0, 0, 0.8))
.AddText("Generated with Exis.PdfEditor", x: 72, y: 720, fontSize: 12)
.AddLine(72, 710, 523, 710, strokeWidth: 1)
.AddRectangle(72, 600, 200, 80, fill: true,
fillRed: 0.95, fillGreen: 0.95, fillBlue: 1.0)
.AddImage(imageBytes, x: 300, y: 400, width: 200, height: 150)) // JPEG or PNG
.AddPage(page => page
.Size(PdfPageSize.Letter)
.AddText("Page 2", x: 72, y: 700, fontSize: 14))
.Build();
File.WriteAllBytes("output.pdf", pdf);
Build a Document with Auto-Layout
byte[] pdf = PdfDocumentBuilder.Create()
.PageSize(PdfPageSize.A4)
.Margins(72)
.WithMetadata(m => m.Title("Report").Author("Exis"))
.Header(h => h
.AddText("Quarterly Report", PdfHorizontalAlignment.Center, 12, o => o.Bold())
.AddLine())
.Footer(f => f
.AddLine()
.AddPageNumber()) // "Page 1 of 3"
.AddParagraph("Introduction", 18, o => o.Bold())
.AddSpacing(8)
.AddParagraph("This report covers Q1 results.")
.AddSpacing(12)
.AddTable(t => t
.Columns(2, 1, 1)
.HeaderRow(r => r.AddCell("Product").AddCell("Units").AddCell("Revenue"))
.AddRow(r => r.AddCell("Widget A").AddCell("1,200").AddCell("$24,000"))
.AddRow(r => r.AddCell("Widget B").AddCell("850").AddCell("$17,000")))
.AddPageBreak()
.AddParagraph("Appendix", 14, o => o.Bold())
.Build();
Features: auto-pagination, text wrapping, tables with headers repeated on page breaks, headers/footers with page numbers, horizontal rules, images, spacing.
Form Filling
// Read form fields (includes smart display labels resolved from nearby page text)
List<PdfFormField> fields = PdfFormFiller.GetFields("form.pdf");
foreach (var field in fields)
{
string label = field.DisplayName ?? field.Name;
Console.WriteLine($"{label} ({field.Type}) = {field.Value}");
// e.g. "Single or Married filing separately (Checkbox) = "
// e.g. "First name and middle initial (Text) = John"
}
// Fill form fields
var result = PdfFormFiller.Fill("form.pdf", "filled.pdf", new Dictionary<string, string>
{
{ "FirstName", "John" },
{ "LastName", "Doe" },
{ "State", "CA" },
{ "AgreeToTerms", "Yes" } // checkbox
});
Console.WriteLine($"Filled {result.FieldsFilled} fields");
// Override alignment (default Auto honors each field's /Q quadding value)
var centered = PdfFormFiller.Fill("form.pdf", "filled.pdf", values,
new PdfFormFillOptions { TextAlignment = PdfFormFieldAlignment.Center });
// Flatten form (merge field appearances into page content, remove interactive fields)
PdfFormFiller.Flatten("filled.pdf", "flattened.pdf");
PdfFormField: Name (fully qualified field name), DisplayName (human-readable label resolved from nearby page text, or null), Type (Text/Checkbox/Radio/Dropdown/Listbox/Signature), Value, Options (for choice fields), IsReadOnly, HasDuplicateWidgets (true if this field has multiple widget kids at distinct positions — i.e., would be split into per-widget entries if SplitDuplicateWidgets were enabled; useful for showing a "duplicate sections detected — enable split mode?" hint in UIs).
Smart label detection: Many PDF forms use cryptic internal field names (e.g., f1_01, c1_1[0]). DisplayName resolves human-readable labels by analyzing nearby text on the page — checking right-of-field (checkbox/radio labels), left-of-field, above, and below. Handles multi-fragment labels split across font changes (e.g., "Single or Married filing separately" rendered as separate bold/normal text operations).
Text alignment (PdfFormFillOptions.TextAlignment): Auto (default) reads the field's /Q quadding value — 0 Left, 1 Center, 2 Right — so most forms render correctly without configuration. Pass Left, Center, or Right to force a single alignment for every filled field. Width is measured from real font metrics (Standard 14 width tables for Helvetica/Times/Courier; embedded-font glyph widths via the engine's font decoder); on fonts where no metrics can be resolved, the appearance falls back to left alignment.
Duplicate-widget split (PdfFormFillOptions.SplitDuplicateWidgets): Some templates — typically carbon-copy receipts or "two-up" forms where one logical field is rendered twice on a page — define a single AcroForm field whose /Kids array points to two widget annotations in different sections. Because PDF stores /V on the field (not per widget), the two widgets normally show the same value. Setting SplitDuplicateWidgets = true opts into a separate name per widget:
// GetFields with the flag returns one entry per widget, suffixed _1, _2, ...
// in page-reading order (top-to-bottom, left-to-right).
var fields = PdfFormFiller.GetFields("two-up-receipt.pdf", splitDuplicateWidgets: true);
// Address_1, Address_2, date_1, date_2, ...
// Fill writes a different value into each widget and auto-flattens so per-kid
// values survive in any viewer (interactive form is removed after fill).
PdfFormFiller.Fill("two-up-receipt.pdf", "filled.pdf", new Dictionary<string,string>
{
{ "Address_1", "Mr. David G Cruz" }, // top receipt
{ "date_1", "10-Jan-67" },
{ "Sum_1", "$35.00" },
{ "Pre-Licensing Course_1", "Yes" }, // checkbox per-receipt
{ "Address_2", "Ms. Claudia Morales" },// bottom receipt
{ "date_2", "22-Feb-26" },
{ "Sum_2", "$500.00" },
{ "Pre-Licensing Course_2", "Off" },
}, new PdfFormFillOptions { SplitDuplicateWidgets = true });
Splitting is triggered only when a field's kids sit at meaningfully different positions (different pages, or more than one field-height apart on the same page). Default false preserves legacy behavior: one entry per logical field, all kids show the same value.
Redaction
var result = PdfRedactor.Redact("input.pdf", "redacted.pdf", new[]
{
// Text-based redaction
new PdfRedaction { Text = "CONFIDENTIAL" },
// Regex pattern (e.g., SSN)
new PdfRedaction { Text = @"\d{3}-\d{2}-\d{4}", IsRegex = true },
// Replace with alternative text
new PdfRedaction { Text = "SECRET", ReplaceWith = "[REDACTED]" },
// Area-based redaction on a specific page
new PdfRedaction { PageNumber = 3, Area = new PdfRect(100, 200, 300, 50) }
});
Console.WriteLine($"Applied {result.RedactionsApplied} redactions");
Image Editor
// Find all images in a PDF (includes displayable image bytes)
var found = PdfImageEditor.FindImages("input.pdf");
foreach (var img in found.Images)
{
Console.WriteLine($"Image #{img.Index}: {img.PixelWidth}x{img.PixelHeight} {img.ColorSpace} {img.Format} " +
$"on page(s) {string.Join(", ", img.PageNumbers)}");
Console.WriteLine($" Data: {img.Data.Length} bytes"); // JPEG or BMP bytes for display
}
// Display image thumbnail in WPF
var firstImage = found.Images[0];
if (firstImage.Data.Length > 0)
{
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.StreamSource = new MemoryStream(firstImage.Data);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
myImageControl.Source = bmp; // Works for both JPEG and BMP data
}
// Replace all images with a new one
byte[] newLogo = File.ReadAllBytes("new-logo.jpg");
var result = PdfImageEditor.ReplaceAll("input.pdf", "output.pdf", newLogo);
Console.WriteLine($"Replaced {result.ImagesReplaced} of {result.ImagesFound} images");
// Replace specific images by index or page range
var selective = PdfImageEditor.Replace("input.pdf", "output.pdf", newLogo,
new PdfImageReplaceOptions { ImageIndices = new[] { 0, 2 } });
// Replace with scaling options
var scaled = PdfImageEditor.Replace("input.pdf", "output.pdf", newLogo,
new PdfImageReplaceOptions
{
ScaleMode = ImageScaleMode.ScaleToFit // Fit within original bounds, preserve aspect ratio
});
ImageScaleMode:
| Mode | Behavior |
|---|---|
MatchOriginalSize |
Default. Fills the exact same display rectangle as the original. May distort if aspect ratios differ. |
KeepReplacementSize |
Displays at natural size, preserving the original DPI. Larger images appear larger, smaller appear smaller. |
ScaleToFit |
Fits within the original rectangle while preserving aspect ratio. Centered, with empty space if needed. |
Watermark
// Diagonal watermark across all pages (default)
PdfWatermark.AddText("input.pdf", "output.pdf", "CONFIDENTIAL");
// Top watermark, red, 50% opacity, specific pages
PdfWatermark.AddText("input.pdf", "output.pdf", "DRAFT", new PdfWatermarkOptions
{
Position = WatermarkPosition.Top,
FontSize = 36,
TextColor = PdfColor.Red,
Opacity = 0.5,
PageRange = new[] { 1, 2, 3 }
});
Page Editing
// Rotate all pages 90 degrees clockwise
PdfPageEditor.Rotate("input.pdf", "rotated.pdf", 90);
// Rotate specific pages
PdfPageEditor.Rotate("input.pdf", "rotated.pdf", 180,
new PdfPageEditOptions { PageRange = new[] { 1, 3 } });
// Crop pages (coordinates in points: 72pt = 1 inch)
PdfPageEditor.Crop("input.pdf", "cropped.pdf", new PdfRect(72, 72, 468, 648));
// Reorder pages (1-based)
PdfPageEditor.Reorder("input.pdf", "reordered.pdf", new[] { 3, 1, 2 });
// Delete pages
PdfPageEditor.DeletePages("input.pdf", "trimmed.pdf", new[] { 2, 4 });
// Insert blank pages
byte[] result = PdfPageEditor.InsertBlankPages(data, new[]
{
new PdfBlankPageInsertion { AfterPage = 0, Size = PdfPageSize.A4 }, // before page 1
new PdfBlankPageInsertion { AfterPage = 3, Size = PdfPageSize.Letter } // after page 3
});
Stamping (PDF Overlay/Underlay)
byte[] letterhead = File.ReadAllBytes("letterhead.pdf");
// Overlay: stamp on top of existing content
PdfStamper.Overlay("input.pdf", "stamped.pdf", letterhead);
// Underlay: stamp behind existing content (like a PDF-based watermark)
PdfStamper.Underlay("input.pdf", "branded.pdf", letterhead);
// With options: specific pages, partial opacity
PdfStamper.Overlay("input.pdf", "output.pdf", letterhead, new PdfStampOptions
{
PageRange = new[] { 1 }, // first page only
StampPageNumber = 1, // use page 1 of stamp PDF
Opacity = 0.5 // 50% transparent
});
Bates Stamping
Sequential page numbering for legal production and discovery workflows. Each page receives a zero-padded identifier (e.g. ABC000001, ABC000002) rendered in a chosen corner of the visual page. Placement is relative to the page's /Rotate orientation, so mixed-rotation documents look consistent. An XMP audit block recording the range, digit width, and prefix/suffix is written to the catalog by default.
// Defaults: number starts at 1, 6 digits, bottom-right corner
var result = PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf");
Console.WriteLine($"Stamped pages {result.FirstNumber}–{result.LastNumber}");
// Prefix/suffix, custom position, color, confidentiality label
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
Prefix = "ABC",
StartNumber = 1,
Digits = 6, // → "ABC000001"
Position = BatesPosition.BottomRight,
FontSize = 10,
TextColor = PdfColor.Black,
BackgroundColor = PdfColor.White, // opaque box behind stamp
MarginInches = 0.5f,
ConfidentialityLabel = "CONFIDENTIAL" // stacked above Bates number
});
// Continuous numbering across a batch — thread LastNumber + 1 into the next call
int next = 1;
foreach (var path in docs)
{
var r = PdfBatesStamp.ApplyBatesStamp(path, path + ".stamped.pdf",
new BatesStampOptions { Prefix = "ABC", StartNumber = next });
next = r.LastNumber + 1;
}
// Skip the cover page, but keep the counter advancing (legal convention —
// the cover is "ABC000001" in the production log even if unstamped)
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
Prefix = "ABC",
SkipFirstPage = true,
CounterAdvancesOnSkippedPages = true // default
});
// Stamp only selected pages
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
PageRange = new[] { 2, 3, 5 } // 1-based
});
// Signed input: stamping invalidates signatures. Off by default — opt in.
PdfBatesStamp.ApplyBatesStamp("signed.pdf", "stamped.pdf", new BatesStampOptions
{
AllowSignedInput = true // Warnings[] will record it
});
// Suppress the XMP audit block (on by default)
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
WriteXmpMetadata = false
});
Encryption & Decryption
// Decrypt a password-protected PDF
PdfSecurity.Decrypt("protected.pdf", "unlocked.pdf", "password");
// Encrypt with AES-256 (strongest standard encryption)
PdfSecurity.Encrypt("input.pdf", "locked.pdf", new PdfEncryptOptions
{
UserPassword = "open123", // password to open
OwnerPassword = "admin456", // password for full access
Permissions = PdfPermissions.Print | PdfPermissions.CopyText // restricted permissions
});
// Check encryption status (no license required)
var info = PdfSecurity.GetEncryptionInfo("file.pdf");
Console.WriteLine($"Encrypted: {info.IsEncrypted}, Version: {info.Version}");
Optimization
var result = PdfOptimizer.Optimize("input.pdf", "optimized.pdf", new PdfOptimizeOptions
{
CompressStreams = true, // Compress uncompressed streams
RemoveDuplicateObjects = true, // Deduplicate identical objects
RemoveMetadata = false, // Keep metadata by default
DownsampleImages = true, // Reduce oversized images
MaxImageDpi = 150 // Target DPI (default: 150)
});
Console.WriteLine($"Saved {result.BytesSaved} bytes ({result.ReductionPercent:F1}%)");
Console.WriteLine($"Images downsampled: {result.ImagesDownsampled}");
Digital Signatures (net8.0+)
using System.Security.Cryptography.X509Certificates;
// Sign a PDF (invisible signature)
var cert = new X509Certificate2("certificate.pfx", "password");
PdfSigner.Sign("input.pdf", "signed.pdf", new PdfSignOptions
{
Certificate = cert,
Reason = "Approved",
Location = "New York",
ContactInfo = "admin@example.com"
});
// Sign with a visible signature annotation on the page
PdfSigner.Sign("input.pdf", "signed.pdf", new PdfSignOptions
{
Certificate = cert,
Reason = "Final Approval",
Location = "New York",
SignerName = "Jane Doe",
Visible = true,
PageNumber = 1,
Rectangle = new PdfSignatureRectangle(72, 50, 200, 60) // x, y, width, height in points
});
// Verify a signed PDF
PdfSignatureInfo info = PdfSigner.Verify("signed.pdf");
Console.WriteLine($"Signed: {info.IsSigned}");
Console.WriteLine($"Valid: {info.IsValid}");
Console.WriteLine($"Signer: {info.SignerName}");
Console.WriteLine($"Certificate: {info.CertificateSubject}");
Console.WriteLine($"Issuer: {info.CertificateIssuer}");
Console.WriteLine($"Timestamp: {info.HasTimestamp}");
// Verify all signatures in a document
List<PdfSignatureInfo> all = PdfSigner.VerifyAll("multi-signed.pdf");
foreach (var sig in all)
Console.WriteLine($"{sig.SignerName}: valid={sig.IsValid}");
PDF/A Compliance
// Validate (no license required)
PdfAValidationResult result = PdfAConverter.Validate("input.pdf", PdfALevel.PdfA2b);
Console.WriteLine($"Compliant: {result.IsCompliant}");
foreach (var v in result.Violations)
Console.WriteLine($" [{v.Code}] {v.Message} (auto-fix: {v.CanAutoFix})");
// Convert to PDF/A
byte[] pdfa = PdfAConverter.Convert("input.pdf", PdfALevel.PdfA2b);
File.WriteAllBytes("output-pdfa.pdf", pdfa);
Extract Text
// Simple extraction
PdfTextResult text = PdfTextExtractor.ExtractText("input.pdf");
Console.WriteLine(text.FullText);
// Extract from specific pages
PdfTextResult partial = PdfTextExtractor.ExtractText("input.pdf", new[] { 1, 3 });
// Structured extraction with position and font data
PdfStructuredTextResult structured = PdfTextExtractor.ExtractStructured("input.pdf");
foreach (var block in structured.Pages[0].TextBlocks)
Console.WriteLine($"[{block.X:F0},{block.Y:F0}] {block.Text} " +
$"(font={block.FontName}, size={block.FontSize})");
Inspect Document (No License Required)
PdfDocumentInfo info = PdfInspector.Inspect("input.pdf");
Console.WriteLine($"Pages: {info.PageCount}");
Console.WriteLine($"Title: {info.Title}");
Console.WriteLine($"Fonts: {string.Join(", ", info.FontsUsed)}");
Console.WriteLine($"Encrypted: {info.IsEncrypted}");
Console.WriteLine($"Form fields: {info.FormFieldCount}");
Diagnostic Structure Dump (No License Required)
When a PDF fails to process and you can't share the file, PdfInspector.DumpStructure
produces a self-contained text report you can paste into a bug report. It walks every
object in the file, tallies filter chains and font subtypes, lists encryption details,
and records any streams that fail to decode — without needing the original file.
// Inspect a problem file and print a human-readable report
PdfStructureDump dump = PdfInspector.DumpStructure("problem.pdf");
Console.WriteLine(dump.ToString());
// Or pull individual fields programmatically
Console.WriteLine($"PDF version: {dump.Version}");
Console.WriteLine($"Pages: {dump.PageCount}");
Console.WriteLine($"Encrypted: {dump.IsEncrypted} (V={dump.EncryptionVersion}, R={dump.EncryptionRevision})");
foreach (var kv in dump.FilterChains)
Console.WriteLine($" {kv.Key}: {kv.Value} streams");
// Streams that failed to decode (capped at 50)
foreach (var bad in dump.UnsupportedStreams)
Console.WriteLine($"obj {bad.ObjectNumber} [{bad.FilterChain}]: {bad.Error}");
The ToString() output is also designed for support workflows — copy it into an email
or issue and the maintainer has everything needed to diagnose without the source file:
=== Exis.PdfEditor Structure Dump ===
File: problem.pdf
Size: 1159381 bytes
PDF version: 1.7
Pages: 12
Objects: 8505 (xref entries: 8506)
Stream objects: 1171
Xref streams: yes
--- Encryption ---
Encrypted: yes
Version (V): 5
Revision (R): 6
Key length: 256 bits
Permissions: 0xFFFFFBE4
Filter: /Standard
StmF: /StdCF
StrF: /StdCF
--- Filter chains (streams) ---
FlateDecode 1163
DCTDecode 3
--- Catalog flags ---
AcroForm: yes
Signed: no
EmbeddedFiles: no
Portfolio: no
Overloads available: DumpStructure(string path), DumpStructure(byte[] data, string? password = null),
DumpStructure(Stream stream, string? password = null), plus DumpStructureAsync with CancellationToken.
XMP + Info Metadata
PdfXmpMetadata reads, replaces, and removes both forms of PDF document metadata:
- XMP — the modern RDF/XML packet referenced from the Catalog
/Metadataentry - Info — the legacy
/Infotrailer dictionary (Title, Author, Subject, Keywords, Creator, Producer, CreationDate, ModDate, plus custom keys)
Extract is license-free and never throws on encrypted files. All mutation methods (SetXmp, RemoveXmp, SetInfo, RemoveInfo, RemoveAll) require a license and reject encrypted PDFs — decrypt first with PdfSecurity.Decrypt.
Read metadata
// No license required — works on encrypted files too (XMP only).
PdfMetadataInfo meta = PdfXmpMetadata.Extract("input.pdf");
if (meta.HasXmp)
{
Console.WriteLine($"XMP packet: {meta.XmpByteSize} bytes");
Console.WriteLine(meta.XmpXml); // Full <?xpacket ... ?> payload as UTF-8 string
}
if (meta.HasInfo)
{
PdfInfoDict info = meta.Info;
Console.WriteLine($"Title: {info.Title}");
Console.WriteLine($"Author: {info.Author}");
Console.WriteLine($"Subject: {info.Subject}");
Console.WriteLine($"Keywords: {info.Keywords}");
Console.WriteLine($"Creator: {info.Creator}");
Console.WriteLine($"Producer: {info.Producer}");
Console.WriteLine($"Created: {info.CreationDate:o}");
Console.WriteLine($"Modified: {info.ModificationDate:o}");
// Non-standard /Info keys (e.g. "Company", custom producer fields)
foreach (var kv in info.Custom)
Console.WriteLine($"[custom] {kv.Key} = {kv.Value}");
}
// Byte[] / Stream overloads are available too:
byte[] pdf = File.ReadAllBytes("input.pdf");
PdfMetadataInfo meta2 = PdfXmpMetadata.Extract(pdf);
Replace XMP packet
// Set a brand-new XMP packet. Must be a valid <?xpacket ...?> wrapped RDF/XML document.
string xmp = @"<?xpacket begin=""""?>
<x:xmpmeta xmlns:x=""adobe:ns:meta/"">
<rdf:RDF xmlns:rdf=""http://www.w3.org/1999/02/22-rdf-syntax-ns#"">
<rdf:Description xmlns:dc=""http://purl.org/dc/elements/1.1/"">
<dc:title><rdf:Alt><rdf:li xml:lang=""x-default"">Quarterly Report</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>Finance Team</rdf:li></rdf:Seq></dc:creator>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end=""w""?>";
PdfXmpMetadata.SetXmp("input.pdf", "output.pdf", xmp);
// Byte[] and Stream overloads:
byte[] updated = PdfXmpMetadata.SetXmp(File.ReadAllBytes("input.pdf"), xmp);
using (var inStream = File.OpenRead("input.pdf"))
using (var outStream = File.Create("output.pdf"))
{
PdfXmpMetadata.SetXmp(inStream, outStream, xmp);
}
Passing a null or empty string to SetXmp writes an empty XMP packet (still present, but with no data). To drop the /Metadata reference entirely, use RemoveXmp.
Replace /Info dictionary
// The document must already have an /Info reference in its trailer.
// If it doesn't, SetInfo throws InvalidOperationException — use SetXmp instead.
var info = new PdfInfoDict
{
Title = "Quarterly Report",
Author = "Finance Team",
Subject = "Q1 2026 earnings summary",
Keywords = "earnings, Q1, 2026",
Creator = "Exis.PdfEditor",
Producer = "Exis.PdfEditor 3.6",
CreationDate = new DateTime(2026, 4, 1, 9, 0, 0, DateTimeKind.Utc),
ModificationDate = DateTime.UtcNow,
};
// Preserve or add custom /Info keys (Acrobat accepts any PDF name key)
info.Custom["Company"] = "Exis LLC";
info.Custom["Revision"] = "v3.6.3";
PdfXmpMetadata.SetInfo("input.pdf", "output.pdf", info);
Fields left null on PdfInfoDict are written as missing — not as empty strings — which preserves "not set" semantics in the output.
Remove metadata (wipe for privacy)
// Drop only the XMP packet (Catalog /Metadata key removed)
PdfXmpMetadata.RemoveXmp("input.pdf", "stripped-xmp.pdf");
// Empty the /Info dict (trailer ref kept, contents cleared)
PdfXmpMetadata.RemoveInfo("input.pdf", "stripped-info.pdf");
// Drop both in one incremental update (smaller output than calling both in sequence)
PdfXmpMetadata.RemoveAll("input.pdf", "stripped.pdf");
Async overloads
Every method has an async counterpart with CancellationToken support:
PdfMetadataInfo meta = await PdfXmpMetadata.ExtractAsync("input.pdf", ct);
await PdfXmpMetadata.SetXmpAsync("input.pdf", "output.pdf", xmp, ct);
await PdfXmpMetadata.SetInfoAsync("input.pdf", "output.pdf", info, ct);
await PdfXmpMetadata.RemoveAllAsync("input.pdf", "stripped.pdf", ct);
byte[] cleaned = await PdfXmpMetadata.RemoveAllAsync(File.ReadAllBytes("input.pdf"), ct);
API Reference
PdfFindReplace
// File-based
PdfFindReplaceResult Execute(string inputPath, string outputPath,
string searchText, string replaceText, PdfFindReplaceOptions? options = null);
// Stream-based
PdfFindReplaceResult Execute(Stream input, Stream output,
string searchText, string replaceText, PdfFindReplaceOptions? options = null);
// Multiple pairs
PdfFindReplaceResult Execute(string inputPath, string outputPath,
IEnumerable<FindReplacePair> pairs, PdfFindReplaceOptions? options = null);
PdfFindReplaceOptions
var options = new PdfFindReplaceOptions
{
CaseSensitive = true, // Case-sensitive matching (default: true)
WholeWordOnly = false, // Match whole words only
UseRegex = false, // Enable regex patterns
UseIncrementalUpdate = true, // Incremental PDF update (smaller output)
PageRange = null, // Limit to specific pages (null = all)
// Text fitting — controls what happens when replacement is wider than original
TextFitting = TextFittingMode.None, // None | PreserveWidth | FitToPage | Adaptive
MinHorizontalScale = 70, // Minimum Tz percentage (50-100)
MaxFontSizeReduction = 1.5, // Max font size reduction in points (Adaptive only)
// Replacement text styling
ReplacementTextColor = PdfColor.Red, // Change fill color of replacement text
ReplacementHighlightColor = PdfColor.Yellow, // Draw colored rectangle behind replacement text
ReplacementBold = false, // Faux bold via fill+stroke rendering
ReplacementUnderline = false, // Draw underline below replacement text
ReplacementStrikethrough = false // Draw strikethrough through replacement text
};
TextFittingMode
| Mode | Behavior |
|---|---|
None |
No fitting. Text renders at natural size. |
PreserveWidth |
Compress horizontally to match original text width exactly. |
FitToPage |
Compress only enough to prevent overflow past the page edge. |
Adaptive |
Progressive: character spacing, word spacing, horizontal scaling, font size. Best quality. |
PdfMerger
byte[] Merge(string[] inputPaths);
byte[] Merge(Stream[] inputStreams);
byte[] Merge(byte[][] inputData);
byte[] Merge(IEnumerable<PdfMergeInput> inputs); // With page range selection
void MergeToFile(string[] inputPaths, string outputPath);
PdfSplitter
List<byte[]> Split(string inputPath); // One PDF per page
List<byte[]> Split(Stream inputStream);
List<byte[]> Split(byte[] inputData);
byte[] ExtractPages(string inputPath, int[] pageNumbers); // Selected pages in one PDF
byte[] ExtractPages(Stream inputStream, int[] pageNumbers);
byte[] ExtractPages(byte[] inputData, int[] pageNumbers);
void SplitToFiles(string inputPath, string outputPattern); // Pattern: "page_{0}.pdf"
PdfBuilder
PdfBuilder.Create()
.WithMetadata(m => m
.Title("...").Author("...").Subject("...").Creator("...").Keywords("..."))
.AddPage(page => page
.Size(PdfPageSize.A4) // A4, Letter, Legal, A3, A5, Tabloid
.Size(widthPoints, heightPoints) // Custom size (72pt = 1 inch)
.AddText(text, x, y, fontSize, options?) // Positioned text
.AddImage(imageBytes, x, y, width, height) // JPEG or PNG (auto-detected)
.AddLine(x1, y1, x2, y2, strokeWidth?, r?, g?, b?)
.AddRectangle(x, y, w, h, fill?, strokeWidth?,
strokeRed?, strokeGreen?, strokeBlue?,
fillRed?, fillGreen?, fillBlue?))
.Build(); // Returns byte[]
.BuildToFile(path); // Write to file
.BuildToStream(stream); // Write to stream
Built-in fonts (no embedding needed): Helvetica, Times-Roman, Courier — each with Bold, Italic, BoldItalic variants.
Text options:
.AddText("text", 72, 700, 14, o => o
.Font("Helvetica") // Font family
.Bold() // Bold variant
.Italic() // Italic variant
.Color(1, 0, 0)) // RGB color (0.0 to 1.0)
PdfFormFiller
// Read form fields
List<PdfFormField> GetFields(string path);
List<PdfFormField> GetFields(byte[] pdfData);
List<PdfFormField> GetFields(Stream stream);
// Fill form fields (each method has an overload that accepts PdfFormFillOptions)
PdfFormFillResult Fill(string inputPath, string outputPath, Dictionary<string, string> fieldValues);
PdfFormFillResult Fill(string inputPath, string outputPath, Dictionary<string, string> fieldValues, PdfFormFillOptions options);
byte[] Fill(byte[] inputData, Dictionary<string, string> fieldValues);
byte[] Fill(byte[] inputData, Dictionary<string, string> fieldValues, PdfFormFillOptions options);
PdfFormFillResult Fill(Stream input, Stream output, Dictionary<string, string> fieldValues);
PdfFormFillResult Fill(Stream input, Stream output, Dictionary<string, string> fieldValues, PdfFormFillOptions options);
// Options
class PdfFormFillOptions {
PdfFormFieldAlignment TextAlignment; // Auto (honor field /Q), Left, Center, Right
bool SplitDuplicateWidgets; // Per-widget values via name_1, name_2, ... (auto-flatten)
}
// GetFields with optional split flag
List<PdfFormField> GetFields(string path, bool splitDuplicateWidgets);
List<PdfFormField> GetFields(byte[] data, bool splitDuplicateWidgets);
List<PdfFormField> GetFields(Stream stream, bool splitDuplicateWidgets);
// Flatten — merge field appearances into page content, remove interactive fields
void Flatten(string inputPath, string outputPath);
byte[] Flatten(byte[] inputData);
void Flatten(Stream input, Stream output);
PdfRedactor
PdfRedactionResult Redact(string inputPath, string outputPath, PdfRedaction[] redactions);
byte[] Redact(byte[] inputData, PdfRedaction[] redactions);
byte[] Redact(Stream input, PdfRedaction[] redactions);
PdfImageEditor
// Find images
PdfImageFinderResult FindImages(string inputPath);
PdfImageFinderResult FindImages(Stream input);
// Replace all images
PdfImageReplaceResult ReplaceAll(string inputPath, string outputPath, byte[] replacementImage);
PdfImageReplaceResult ReplaceAll(Stream input, Stream output, byte[] replacementImage);
// Replace with options (filter by page range or image index)
PdfImageReplaceResult Replace(string inputPath, string outputPath, byte[] replacementImage,
PdfImageReplaceOptions? options = null);
PdfImageReplaceResult Replace(Stream input, Stream output, byte[] replacementImage,
PdfImageReplaceOptions? options = null);
PdfImageReplaceOptions: PageRange (1-based page numbers, null = all), ImageIndices (0-based image indices from FindImages, null = all), ScaleMode (MatchOriginalSize | KeepReplacementSize | ScaleToFit, default: MatchOriginalSize).
PdfImageInfo: Index, PageNumbers, PixelWidth, PixelHeight, ColorSpace (RGB/Gray/CMYK/Indexed/Unknown), Format (JPEG/Flate/Raw/JPEG2000), BitsPerComponent, Data (displayable image bytes — JPEG for JPEG images, BMP for Flate/Raw/CMYK/Grayscale; load directly into WPF BitmapImage via MemoryStream).
PdfWatermark
PdfWatermarkResult AddText(string inputPath, string outputPath, string text, PdfWatermarkOptions? options = null);
PdfWatermarkResult AddText(Stream input, Stream output, string text, PdfWatermarkOptions? options = null);
byte[] AddText(byte[] inputData, string text, PdfWatermarkOptions? options = null);
PdfWatermarkOptions: Position (Top/Bottom/Center/Across, default: Across), FontSize (default: 48), TextColor (PdfColor, default: light gray), Opacity (0.0–1.0, default: 0.3), PageRange (1-based page numbers, null = all).
PdfBatesStamp
BatesStampResult ApplyBatesStamp(string inputPath, string outputPath, BatesStampOptions? options = null);
BatesStampResult ApplyBatesStamp(Stream input, Stream output, BatesStampOptions? options = null);
byte[] ApplyBatesStamp(byte[] inputData, BatesStampOptions? options = null);
BatesStampOptions:
Prefix/Suffix— text bracketing the number (default: empty).StartNumber(default 1) — first Bates number. For batch continuation, pass the previous document'sLastNumber + 1.Digits(default 6) — minimum zero-padded width. Auto-expands for every page if the range requires more digits, with a warning in the result.Position(defaultBottomRight) — one ofTopLeft,TopCenter,TopRight,BottomLeft,BottomCenter,BottomRight. Corners are relative to the visual page (after/Rotate).FontSize(default 10) — point size. The font is Helvetica (standard 14, no embedding required).TextColor(default black) /BackgroundColor(default none) —PdfColorvalues; background draws a solid rectangle behind the stamp for legibility.MarginInches(default 0.5) — distance from the trimmed page edge. Applied per-page so mixed page sizes (Letter, A4, Legal) share the same visual margin.ConfidentialityLabel— optional adjacent label (e.g."CONFIDENTIAL").ConfidentialityPosition— corner/edge for the label. When null, the label is stacked in the same corner as the Bates number (above for bottom positions, below for top positions) so it never spills past the page edge.ConfidentialityFontSize— label point size. When null, matchesFontSize.PageRange— 1-based page numbers to stamp. Null = all pages.SkipFirstPage(default false) — whenPageRangeis null, skip page 1 (for cover sheets).PageRangewins if both are set.CounterAdvancesOnSkippedPages(default true) — when true, the Bates counter advances for every page whether stamped or not (legal-production convention). When false, only stamped pages consume a number.AllowSignedInput(default false) — opt in to stamp a PDF carrying digital signatures. Stamping invalidates the signatures; a warning is added to the result.WriteXmpMetadata(default true) — write an XMP audit block (range, digit width, prefix/suffix) to the document catalog. Existing XMP metadata is merged, not replaced.
BatesStampResult: FirstNumber, LastNumber, PagesStamped, DigitsUsed (equals Digits unless auto-expanded), Warnings (non-fatal diagnostics: digit expansion, signed-input stamped anyway, etc.).
PdfOptimizer
PdfOptimizeResult Optimize(string inputPath, string outputPath, PdfOptimizeOptions? options = null);
byte[] Optimize(byte[] inputData, PdfOptimizeOptions? options = null);
byte[] Optimize(Stream input, PdfOptimizeOptions? options = null);
PdfOptimizeOptions: CompressStreams (default true), RemoveDuplicateObjects (default true), RemoveMetadata (default false), DownsampleImages (default false), MaxImageDpi (default 150). Image downsampling applies to FlateDecode (PNG-style) images; JPEG images are left untouched.
PdfSigner (net8.0+)
// Sign
byte[] Sign(byte[] inputData, PdfSignOptions options);
byte[] Sign(string inputPath, PdfSignOptions options);
void Sign(string inputPath, string outputPath, PdfSignOptions options);
byte[] Sign(Stream input, PdfSignOptions options);
// Verify first signature
PdfSignatureInfo Verify(byte[] pdfData);
PdfSignatureInfo Verify(string path);
PdfSignatureInfo Verify(Stream stream);
// Verify all signatures
List<PdfSignatureInfo> VerifyAll(byte[] pdfData);
List<PdfSignatureInfo> VerifyAll(string path);
List<PdfSignatureInfo> VerifyAll(Stream stream);
PdfSignOptions: Certificate (X509Certificate2, required), Reason, Location, ContactInfo, SignerName (defaults to certificate subject), Visible (default false — invisible signature), PageNumber (1-based, default 1), Rectangle (PdfSignatureRectangle with X/Y/Width/Height in points, defaults to bottom-left corner). When Visible = true, the signature renders as an annotation on the page showing signer name, date, reason, and location.
PdfSignatureInfo includes: IsSigned, IsValid, SignerName, SignDate, Reason, Location, CertificateSubject, CertificateIssuer, CertificateSerialNumber, CertificateNotBefore, CertificateNotAfter, HasTimestamp, TimestampDate.
PdfAConverter
// Validate (no license required)
PdfAValidationResult Validate(string path, PdfALevel level = PdfALevel.PdfA2b);
PdfAValidationResult Validate(byte[] pdfData, PdfALevel level = PdfALevel.PdfA2b);
PdfAValidationResult Validate(Stream stream, PdfALevel level = PdfALevel.PdfA2b);
// Convert
byte[] Convert(byte[] inputData, PdfALevel level = PdfALevel.PdfA2b);
byte[] Convert(string inputPath, PdfALevel level = PdfALevel.PdfA2b);
void Convert(string inputPath, string outputPath, PdfALevel level = PdfALevel.PdfA2b);
byte[] Convert(Stream input, PdfALevel level = PdfALevel.PdfA2b);
PdfALevel: PdfA1b, PdfA2b, PdfA2u, PdfA3b, PdfA3u. The "u" variants additionally require Unicode mappings (ToUnicode CMap) for all fonts.
PdfTextExtractor
// Simple extraction
PdfTextResult ExtractText(string path);
PdfTextResult ExtractText(Stream stream);
PdfTextResult ExtractText(string path, int[] pages);
PdfTextResult ExtractText(Stream stream, int[] pages);
// Structured extraction (with position and font data)
PdfStructuredTextResult ExtractStructured(string path);
PdfStructuredTextResult ExtractStructured(Stream stream);
PdfStructuredTextResult ExtractStructured(string path, int[] pages);
PdfStructuredTextResult ExtractStructured(Stream stream, int[] pages);
PdfInspector (No License Required)
// Lightweight metadata
PdfDocumentInfo Inspect(string path);
PdfDocumentInfo Inspect(Stream stream);
// Full diagnostic structure dump — for bug reports & support workflows
PdfStructureDump DumpStructure(string path);
PdfStructureDump DumpStructure(byte[] data, string? password = null);
PdfStructureDump DumpStructure(Stream stream, string? password = null);
Task<PdfStructureDump> DumpStructureAsync(string path, CancellationToken ct = default);
Inspect returns: Version, PageCount, Title, Author, Producer, Creator, CreationDate, ModificationDate, IsEncrypted, HasFormFields, FormFieldCount, FontsUsed, per-page WidthInPoints/HeightInPoints/CharacterCount.
DumpStructure returns a PdfStructureDump with: file size, PDF version, object/stream counts, encryption details (V/R/key length/permissions/Filter/SubFilter/StmF/StrF/EFF), FilterChains tally, FontSubtypes/FontEncodings tallies, catalog flags (AcroForm/signed/embedded files/portfolio), UnsupportedStreams (up to 50 streams that failed to decode, with object number, filter chain, subtype, length, and error message), and a Notes list. ToString() produces a multi-line human-readable report suitable for pasting into a bug report.
PdfXmpMetadata
// Read — license-free, safe on encrypted files (XMP only)
PdfMetadataInfo Extract(string path);
PdfMetadataInfo Extract(byte[] data);
PdfMetadataInfo Extract(Stream input);
// Replace XMP packet (null/"" writes an empty packet; use RemoveXmp to drop entirely)
void SetXmp(string inputPath, string outputPath, string xmpXml);
byte[] SetXmp(byte[] data, string xmpXml);
void SetXmp(Stream input, Stream output, string xmpXml);
// Drop Catalog /Metadata entry
void RemoveXmp(string inputPath, string outputPath);
byte[] RemoveXmp(byte[] data);
void RemoveXmp(Stream input, Stream output);
// Replace /Info dict (throws InvalidOperationException if trailer has no /Info ref)
void SetInfo(string inputPath, string outputPath, PdfInfoDict info);
byte[] SetInfo(byte[] data, PdfInfoDict info);
void SetInfo(Stream input, Stream output, PdfInfoDict info);
// Empty the /Info dict (ref is kept, contents cleared)
void RemoveInfo(string inputPath, string outputPath);
byte[] RemoveInfo(byte[] data);
void RemoveInfo(Stream input, Stream output);
// Drop XMP + empty /Info in one incremental update
void RemoveAll(string inputPath, string outputPath);
byte[] RemoveAll(byte[] data);
void RemoveAll(Stream input, Stream output);
// Every method has a <Name>Async counterpart with CancellationToken
PdfMetadataInfo — HasXmp, XmpXml, XmpByteSize, HasInfo, Info (PdfInfoDict).
PdfInfoDict — Title, Author, Subject, Keywords, Creator, Producer, CreationDate (DateTime?), ModificationDate (DateTime?), Custom (Dictionary<string,string> for non-standard /Info entries). Null-valued fields are written as missing, not as empty strings.
Mutation methods throw NotSupportedException on encrypted input — decrypt first with PdfSecurity.Decrypt.
PdfDocumentBuilder (Auto-Layout)
PdfDocumentBuilder.Create()
.PageSize(PdfPageSize.A4) // Or .PageSize(width, height)
.Margins(72) // Or .Margins(top, right, bottom, left)
.WithMetadata(m => m.Title("..."))
.Header(h => h
.AddText("Title", PdfHorizontalAlignment.Center, fontSize, options?)
.AddPageNumber("Page {page} of {pages}", alignment?, fontSize?, options?)
.AddLine(thickness?, r?, g?, b?))
.Footer(f => /* same as header */)
.AddParagraph(text, fontSize?, options?) // Auto-wrapped, auto-paginated
.AddSpacing(points)
.AddHorizontalRule(thickness?, r?, g?, b?)
.AddImage(imageBytes, width, height)
.AddTable(t => t
.Columns(2, 1, 1) // Relative widths
.ColumnsFixed(200, 100, 100) // Or fixed widths in points
.BorderWidth(0.5).BorderColor(r, g, b)
.CellPadding(4)
.AlternatingRowBackground(0.95, 0.95, 1.0) // Light blue on even rows
.HeaderRow(r => r.AddCell("...")) // Repeated on page breaks
.AddRow(r => r
.AddCell("...", o => o
.VerticalAlignment(PdfVerticalAlignment.Middle) // Top, Middle, Bottom
.PaddingLeft(8).PaddingRight(8)))) // Per-side padding
.AddPageBreak()
.Build(); // Returns byte[]
.BuildToFile(path);
.BuildToStream(stream);
Async Overloads
Every I/O API has an async counterpart directly on the main class with CancellationToken support:
// Pattern: <ClassName>.<MethodName>Async(...)
byte[] merged = await PdfMerger.MergeAsync(inputPaths, cancellationToken);
PdfTextResult text = await PdfTextExtractor.ExtractTextAsync(stream, cancellationToken);
var info = await PdfInspector.InspectAsync(path, cancellationToken);
var result = await PdfOptimizer.OptimizeAsync(data, options, cancellationToken);
var sigs = await PdfSigner.VerifyAllAsync(path, cancellationToken);
// Also: PdfFindReplace.ExecuteAsync, PdfSplitter.SplitAsync,
// PdfFormFiller.FillAsync/FlattenAsync, PdfRedactor.RedactAsync,
// PdfImageEditor.FindImagesAsync/ReplaceAsync/ReplaceAllAsync,
// PdfWatermark.AddTextAsync, PdfBatesStamp.ApplyBatesStampAsync,
// PdfAConverter.ValidateAsync/ConvertAsync,
// PdfPageEditor.RotateAsync/CropAsync/ReorderAsync/DeletePagesAsync,
// PdfStamper.OverlayAsync/UnderlayAsync,
// PdfSecurity.DecryptAsync/EncryptAsync,
// PdfXmpMetadata.ExtractAsync/SetXmpAsync/RemoveXmpAsync/
// SetInfoAsync/RemoveInfoAsync/RemoveAllAsync
Evaluation & Licensing
| Mode | Limit | How to activate |
|---|---|---|
| Trial | Full access for 14 days | ExisLicense.Initialize() (no key) |
| Evaluation | 3-page limit | After trial expires |
| Licensed | Unlimited | ExisLicense.Initialize("XXXX-XXXX-XXXX-XXXX") |
PdfInspector (including Inspect and DumpStructure), PdfAConverter.Validate, PdfSecurity.GetEncryptionInfo, and PdfXmpMetadata.Extract work without any license.
Purchase at pdfbatcheditor.com/developers.
License
Copyright (c) Exis LLC 2024-2026. All rights reserved. Commercial license required for production use. See LICENSE.md for details.
Made in USA.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.HashCode (>= 6.0.0)
- Microsoft.Win32.Registry (>= 4.7.0)
- System.Buffers (>= 4.6.0)
- System.Memory (>= 4.6.0)
- System.Security.Cryptography.ProtectedData (>= 8.0.0)
- System.Text.Json (>= 8.0.5)
-
net8.0
- Microsoft.Win32.Registry (>= 5.0.0)
- System.Security.Cryptography.Pkcs (>= 8.0.1)
- System.Security.Cryptography.ProtectedData (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.7.8 | 92 | 5/11/2026 |
| 3.7.7 | 97 | 5/9/2026 |
| 3.7.5 | 89 | 5/9/2026 |
| 3.7.4 | 102 | 5/9/2026 |
| 3.7.3 | 98 | 5/8/2026 |
| 3.7.2 | 100 | 5/8/2026 |
| 3.7.1 | 94 | 5/8/2026 |
| 3.7.0 | 116 | 4/21/2026 |
| 3.6.4 | 98 | 4/21/2026 |
| 3.6.3 | 87 | 4/20/2026 |
| 3.6.2 | 100 | 4/20/2026 |
| 3.6.0 | 97 | 4/20/2026 |
| 3.5.7 | 95 | 4/20/2026 |
| 3.5.6 | 104 | 4/17/2026 |
| 3.5.5 | 92 | 4/17/2026 |
| 3.5.4 | 100 | 4/16/2026 |
| 3.5.3 | 107 | 4/10/2026 |
| 3.5.2 | 103 | 4/9/2026 |
| 3.5.1 | 104 | 4/9/2026 |
| 3.5.0 | 102 | 4/7/2026 |