Exis.PdfEditor
3.6.2
See the version list below for details.
dotnet add package Exis.PdfEditor --version 3.6.2
NuGet\Install-Package Exis.PdfEditor -Version 3.6.2
<PackageReference Include="Exis.PdfEditor" Version="3.6.2" />
<PackageVersion Include="Exis.PdfEditor" Version="3.6.2" />
<PackageReference Include="Exis.PdfEditor" />
paket add Exis.PdfEditor --version 3.6.2
#r "nuget: Exis.PdfEditor, 3.6.2"
#:package Exis.PdfEditor@3.6.2
#addin nuget:?package=Exis.PdfEditor&version=3.6.2
#tool nuget:?package=Exis.PdfEditor&version=3.6.2
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)
- 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");
// 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.
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).
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.
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
PdfFormFillResult Fill(string inputPath, string outputPath, Dictionary<string, string> fieldValues);
byte[] Fill(byte[] inputData, Dictionary<string, string> fieldValues);
byte[] Fill(Stream input, Dictionary<string, string> fieldValues);
// 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.
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
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, and PdfSecurity.GetEncryptionInfo 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.Text.Json (>= 8.0.5)
-
net8.0
- Microsoft.Win32.Registry (>= 5.0.0)
- System.Security.Cryptography.Pkcs (>= 8.0.1)
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 | 95 | 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 |